diff --git a/samples/javarecord/build.gradle b/samples/javarecord/build.gradle new file mode 100644 index 000000000..82579a618 --- /dev/null +++ b/samples/javarecord/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'kotlin' + +dependencies { + implementation project(":tempest") + + testImplementation dep.miskAwsDynamodbTesting + testImplementation dep.assertj + testImplementation dep.miskTesting + testImplementation dep.junitApi + testImplementation dep.junitEngine +} + +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_14 + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_14 + } +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_14 + targetCompatibility = JavaVersion.VERSION_14 +} + +tasks.withType(JavaCompile) { + options.compilerArgs += "--enable-preview" +} + +tasks.withType(Test) { + jvmArgs += "--enable-preview" +} + +tasks.withType(JavaExec) { + jvmArgs += '--enable-preview' +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumInfo.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumInfo.java new file mode 100644 index 000000000..7acb12a3b --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumInfo.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import app.cash.tempest.Attribute; +import app.cash.tempest.ForIndex; +import java.time.LocalDate; +import javax.annotation.Nullable; + +public record AlbumInfo( + @Attribute(name = "partition_key") + String album_token, + String album_title, + String artist_name, + LocalDate release_date, + String genre_name, + @Attribute(prefix = "INFO_") + String sort_key +) { + + public AlbumInfo( + String album_token, + String album_title, + String artist_name, + LocalDate release_date, + String genre_name) { + this(album_token, + album_title, + artist_name, + release_date, + genre_name, + ""); + } + + public Key key() { + return new Key(album_token); + } + + public static record Key( + String album_token, + String sort_key + ) { + public Key(String album_token) { + this(album_token, ""); + } + } + + @ForIndex(name = "genre_album_index") + public static record GenreIndexOffset( + String genre_name, + @Nullable + String album_token, + // To uniquely identify an item in pagination. + @Nullable + String sort_key + ) { + + public GenreIndexOffset(String genre_name) { + this(genre_name, null, null); + } + + public GenreIndexOffset(String genre_name, String album_token) { + this(genre_name, album_token, null); + } + } + + @ForIndex(name = "artist_album_index") + public static record ArtistIndexOffset( + String artist_name, + @Nullable + String album_token, + // To uniquely identify an item in pagination. + @Nullable + String sort_key + ) { + + public ArtistIndexOffset(String artist_name) { + this(artist_name, null, null); + } + + public ArtistIndexOffset(String artist_name, String album_token) { + this(artist_name, album_token, null); + } + } +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumTrack.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumTrack.java new file mode 100644 index 000000000..4e5019da6 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/AlbumTrack.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import app.cash.tempest.Attribute; +import app.cash.tempest.ForIndex; +import java.time.Duration; +import javax.annotation.Nullable; + +public record AlbumTrack( + @Attribute(name = "partition_key") + String album_token, + @Attribute(name = "sort_key", prefix = "TRACK_") + String track_token, + String track_title, + Duration run_length +) { + + public Key key() { + return new Key(album_token, track_token); + } + + public Long track_number() { + return Long.parseLong(track_token, 16); + } + + public AlbumTrack( + String album_token, + Long track_number, + String track_title, + Duration run_length) { + this(album_token, String.format("%016x", track_number), track_title, run_length); + } + + public static record Key( + String album_token, + String track_token + ) { + + public Key(String album_token, Long track_number) { + this(album_token, String.format("%016x", track_number)); + } + + public Key(String album_token) { + this(album_token, ""); + } + + public Long track_number() { + return Long.parseLong(track_token, 16); + } + } + + @ForIndex(name = "album_track_title_index") + public static record TitleIndexOffset( + String album_token, + String track_title, + // To uniquely identify an item in pagination. + @Nullable + String track_token + ) { + + public TitleIndexOffset(String album_token, String track_title) { + this(album_token, track_title, null); + } + } +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicDb.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicDb.java new file mode 100644 index 000000000..6d9e2bc93 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicDb.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import app.cash.tempest.LogicalDb; + +public interface MusicDb extends LogicalDb { + MusicTable music(); +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicItem.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicItem.java new file mode 100644 index 000000000..cd168ecb9 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicItem.java @@ -0,0 +1,126 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import java.time.Duration; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +@DynamoDBTable(tableName = "j_music_items") +public class MusicItem { + // All Items. + @DynamoDBHashKey + @DynamoDBIndexRangeKey(globalSecondaryIndexNames = {"genre_album_index", "artist_album_index"}) + String partition_key = null; + @DynamoDBRangeKey + String sort_key = null; + + // AlbumInfo. + @DynamoDBAttribute + String album_title = null; + @DynamoDBIndexHashKey(globalSecondaryIndexName = "artist_album_index") + @DynamoDBAttribute + String artist_name = null; + @DynamoDBAttribute + @DynamoDBTypeConverted(converter = LocalDateTypeConverter.class) + LocalDate release_date = null; + @DynamoDBAttribute + @DynamoDBIndexHashKey(globalSecondaryIndexName = "genre_album_index") + String genre_name = null; + + // AlbumTrack. + @DynamoDBAttribute + @DynamoDBIndexRangeKey(localSecondaryIndexName = "album_track_title_index") + String track_title = null; + @DynamoDBAttribute + @DynamoDBTypeConverted(converter = DurationTypeConverter.class) + Duration run_length = null; + + // PlaylistInfo. + @DynamoDBAttribute + String playlist_name = null; + @DynamoDBAttribute + Integer playlist_size = null; + @DynamoDBAttribute + @DynamoDBTypeConverted(converter = AlbumTrackKeyListTypeConverter.class) + List playlist_tracks = null; + @DynamoDBAttribute + Long playlist_version = null; + + // PlaylistEntry. + @DynamoDBAttribute + String track_token = null; +} + +class DurationTypeConverter implements DynamoDBTypeConverter { + + @Override public String convert(Duration duration) { + return duration.toString(); + } + + @Override public Duration unconvert(String duration) { + return Duration.parse(duration); + } +} + +class LocalDateTypeConverter implements DynamoDBTypeConverter { + + @Override public String convert(LocalDate localDate) { + return localDate.toString(); + } + + @Override public LocalDate unconvert(String localDate) { + return LocalDate.parse(localDate); + } +} + +class AlbumTrackKeyListTypeConverter + implements DynamoDBTypeConverter> { + + @Override public AttributeValue convert(List keys) { + return new AttributeValue().withL( + keys.stream() + .map(it -> new AttributeValue().withS(convert(it))) + .collect(Collectors.toList()) + ); + } + + @Override public List unconvert(AttributeValue items) { + return items.getL().stream() + .map(it -> unconvert(it.getS())) + .collect(Collectors.toList()); + } + + private AlbumTrack.Key unconvert(String string) { + var parts = string.split("/"); + return new AlbumTrack.Key(parts[0], parts[1]); + } + + private String convert(AlbumTrack.Key key) { + return "${key.album_token}/${key.track_token}"; + } +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicTable.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicTable.java new file mode 100644 index 000000000..6b0e9e54c --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/MusicTable.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import app.cash.tempest.InlineView; +import app.cash.tempest.LogicalTable; +import app.cash.tempest.SecondaryIndex; + +public interface MusicTable extends LogicalTable { + InlineView albumInfo(); + InlineView albumTracks(); + + InlineView playlistInfo(); + + // Global Secondary Indexes. + SecondaryIndex albumInfoByGenre(); + SecondaryIndex albumInfoByArtist(); + + // Local Secondary Indexes. + SecondaryIndex albumTracksByTitle(); +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/PlaylistInfo.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/PlaylistInfo.java new file mode 100644 index 000000000..a73e6c24c --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/musiclibrary/PlaylistInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.musiclibrary; + +import app.cash.tempest.Attribute; +import java.util.List; + +public record PlaylistInfo( + @Attribute(name = "partition_key") + String playlist_token, + String playlist_name, + List playlist_tracks, + Long playlist_version, + @Attribute(prefix = "INFO_") + String sort_key +) { + + public PlaylistInfo(String playlist_token, String playlist_name, + List playlist_tracks) { + this(playlist_token, playlist_name, playlist_tracks, 1L); + } + + public PlaylistInfo(String playlist_token, String playlist_name, + List playlist_tracks, Long playlist_version) { + this(playlist_token, + playlist_name, + playlist_tracks, + playlist_version, + ""); + } + + public static record Key( + String playlist_token, + String sort_key + ) { + + public Key(String playlist_token) { + this(playlist_token, ""); + } + } +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/Alias.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/Alias.java new file mode 100644 index 000000000..6e878de92 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/Alias.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.urlshortener; + +public record Alias( + String short_url, + String destination_url +) { + public Alias.Key key() { + return new Alias.Key(short_url); + } + + public record Key( + String short_url + ) { + } +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasDb.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasDb.java new file mode 100644 index 000000000..8ccab640d --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasDb.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.urlshortener; + +import app.cash.tempest.LogicalDb; + +public interface AliasDb extends LogicalDb { + AliasTable aliasTable(); +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasItem.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasItem.java new file mode 100644 index 000000000..85cac3cf2 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasItem.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.urlshortener; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; + +@DynamoDBTable(tableName = "j_alias_items") +public class AliasItem { + private String short_url; + private String destination_url; + + @DynamoDBHashKey(attributeName = "short_url") + public String getShortUrl() { + return short_url; + } + + public void setShortUrl(String short_url) { + this.short_url = short_url; + } + + @DynamoDBAttribute(attributeName = "destination_url") + public String getDestinationUrl() { + return destination_url; + } + + public void setDestinationUrl(String destination_url) { + this.destination_url = destination_url; + } + +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasTable.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasTable.java new file mode 100644 index 000000000..387290e12 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/urlshortener/AliasTable.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord.urlshortener; + +import app.cash.tempest.InlineView; +import app.cash.tempest.LogicalTable; + +public interface AliasTable extends LogicalTable { + InlineView aliases(); +} diff --git a/samples/javarecord/src/test/java/app/cash/tempest/javarecord/JavaRecordTest.java b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/JavaRecordTest.java new file mode 100644 index 000000000..4b08e50b6 --- /dev/null +++ b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/JavaRecordTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord; + +import app.cash.tempest.javarecord.urlshortener.Alias; +import app.cash.tempest.javarecord.urlshortener.AliasDb; +import app.cash.tempest.javarecord.urlshortener.AliasTable; +import javax.inject.Inject; +import misk.testing.MiskTest; +import misk.testing.MiskTestModule; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@MiskTest(startService = true) +public class JavaRecordTest { + @MiskTestModule + TestModule module = new TestModule(); + @Inject AliasDb aliasDb; + + @Test + public void javaLogicalTypeJavaItemType() { + AliasTable aliasTable = aliasDb.aliasTable(); + Alias alias = new Alias( + "SquareCLA", + "https://docs.google.com/forms/d/e/1FAIpQLSeRVQ35-gq2vdSxD1kdh7CJwRdjmUA0EZ9gRXaWYoUeKPZEQQ/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1" + ); + aliasTable.aliases().save(alias); + Alias loadedAlias = aliasTable.aliases().load(alias.key()); + assertThat(loadedAlias).isNotNull(); + assertThat(loadedAlias.short_url()).isEqualTo(alias.short_url()); + assertThat(loadedAlias.destination_url()).isEqualTo(alias.destination_url()); + } +} diff --git a/samples/javarecord/src/test/java/app/cash/tempest/javarecord/TestModule.java b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/TestModule.java new file mode 100644 index 000000000..cf6d94382 --- /dev/null +++ b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/TestModule.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.tempest.javarecord; + +import app.cash.tempest.LogicalDb; +import app.cash.tempest.javarecord.urlshortener.AliasDb; +import app.cash.tempest.javarecord.urlshortener.AliasItem; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import kotlin.jvm.internal.Reflection; +import misk.MiskTestingServiceModule; +import misk.aws.dynamodb.testing.DynamoDbTable; +import misk.aws.dynamodb.testing.InProcessDynamoDbModule; + +public class TestModule extends AbstractModule { + + @Override protected void configure() { + install(new MiskTestingServiceModule()); + install( + new InProcessDynamoDbModule( + new DynamoDbTable( + Reflection.createKotlinClass(AliasItem.class), + (createTableRequest) -> createTableRequest + ) + ) + ); + } + + @Provides + @Singleton + AliasDb provideJAliasDb(AmazonDynamoDB amazonDynamoDB) { + var dynamoDbMapper = new DynamoDBMapper(amazonDynamoDB); + return LogicalDb.create(AliasDb.class, dynamoDbMapper); + } +} diff --git a/settings.gradle b/settings.gradle index 56466697c..25edff648 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ include 'tempest-internal' include 'tempest' include 'tempest2' include ':samples:guides' +include ':samples:javarecord' include ':samples:musiclibrary' include ':samples:musiclibrary2' include ':samples:urlshortener' diff --git a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Codec.kt b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Codec.kt index b4ea7a954..d17dcd4f1 100644 --- a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Codec.kt +++ b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Codec.kt @@ -23,7 +23,6 @@ import kotlin.reflect.KParameter import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor import kotlin.reflect.full.withNullability import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaField @@ -95,7 +94,11 @@ internal class ReflectionCodec private constructor( .map { it.parameter to it.getDb(dbItem) } .toMap() val appItem = - appItemConstructor?.callBy(constructorArgs) ?: appItemClassFactory.newInstance() + if (appItemConstructor != null && constructorArgs.size == appItemConstructor.parameters.size) { + appItemConstructor.callBy(constructorArgs) + } else { + appItemClassFactory.newInstance() + } for (binding in varBindings) { binding.setApp(appItem, binding.getDb(dbItem)) } @@ -158,18 +161,19 @@ internal class ReflectionCodec private constructor( itemAttributes: Map, rawItemType: RawItemType ): Codec { - val dbItemConstructor = requireNotNull(rawItemType.type.primaryConstructor ?: rawItemType.type.constructors.singleOrNull()) + val dbItemConstructor = requireNotNull(rawItemType.type.defaultConstructor) require(dbItemConstructor.parameters.isEmpty()) { "Expect ${rawItemType.type} to have a zero argument constructor" } - val appItemConstructorParameters = itemType.primaryConstructorParameters - val rawItemProperties = rawItemType.type.memberProperties.associateBy { it.name } - val constructorParameterBindings = mutableListOf>() + val appItemConstructorParameters = itemType.defaultConstructorParameters + val dbItemProperties = rawItemType.type.memberProperties.associateBy { it.name } + val constructorParameterBindings = + mutableListOf>() val varBindings = mutableListOf>() val valBindings = mutableListOf>() for (property in itemType.memberProperties) { val propertyName = property.name val itemAttribute = itemAttributes[propertyName] ?: continue val mappedProperties = itemAttribute.names - .map { requireNotNull(rawItemProperties[it]) { "Expect ${rawItemType.type} to have property $propertyName" } } + .map { requireNotNull(dbItemProperties[it]) { "Expect ${rawItemType.type} to have property $propertyName" } } val mappedPropertyTypes = mappedProperties.map { it.returnType }.distinct() require(mappedPropertyTypes.size == 1) { "Expect mapped properties of $propertyName to have the same type: ${mappedProperties.map { it.name }}" } val expectedReturnType = requireNotNull(mappedPropertyTypes.single()).withNullability(false) @@ -195,7 +199,7 @@ internal class ReflectionCodec private constructor( .filter { attribute -> attribute.prefix.isNotEmpty() } .flatMap { attribute -> attribute.names.map { Prefixer.AttributePrefix(it, attribute.prefix) } } return ReflectionCodec( - itemType.primaryConstructor, + itemType.defaultConstructor, ClassFactory.create(itemType.java), dbItemConstructor, ClassFactory.create(rawItemType.type.java), @@ -220,16 +224,18 @@ private sealed class Binding { abstract val appProperty: KProperty1 abstract val dbProperties: List> - fun getApp(value: A) = appProperty.get(value) + fun getApp(value: A): P { + if (!appProperty.isAccessible) { + appProperty.javaField!!.trySetAccessible() + } + return appProperty.get(value) + } fun getDb(value: D) = dbProperties[0].get(value) fun setDb(result: D, value: P) { for (rawProperty in dbProperties) { - if (!rawProperty.isAccessible) { - rawProperty.javaField?.trySetAccessible() - } - (rawProperty as KMutableProperty1).set(result, value) + rawProperty.forceSet(result, value) } } } diff --git a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Reflections.kt b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Reflections.kt index 630b44fbf..45955ee59 100644 --- a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Reflections.kt +++ b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Reflections.kt @@ -20,6 +20,7 @@ import java.lang.reflect.Method import java.lang.reflect.Modifier import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass +import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.KProperty import kotlin.reflect.KType @@ -30,8 +31,11 @@ import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaMethod -internal val KClass<*>.primaryConstructorParameters: Map - get() = primaryConstructor?.parameters?.associateBy { requireNotNull(it.name) } ?: emptyMap() +internal val KClass<*>.defaultConstructor: KFunction? + get() = primaryConstructor ?: constructors.singleOrNull() + +internal val KClass<*>.defaultConstructorParameters: Map + get() = defaultConstructor?.parameters?.associateBy { requireNotNull(it.name) } ?: emptyMap() data class ClassMember( val annotations: List, diff --git a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Schema.kt b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Schema.kt index 993de2966..73a878164 100644 --- a/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Schema.kt +++ b/tempest-internal/src/main/kotlin/app/cash/tempest/internal/Schema.kt @@ -137,7 +137,7 @@ data class KeyType( ) { fun create(keyType: KClass<*>, itemType: ItemType, rawItemType: RawItemType): KeyType { require(keyType.constructors.isNotEmpty()) { "$keyType must have a constructor" } - val constructorParameters = keyType.primaryConstructorParameters + val constructorParameters = keyType.defaultConstructorParameters val attributeNames = mutableSetOf() for (property in keyType.memberProperties) { if (property.shouldIgnore) { @@ -236,7 +236,7 @@ data class ItemType( primaryIndex: PrimaryIndex ): Map { val attributes = mutableMapOf() - val constructorParameters: Map = itemType.primaryConstructorParameters + val constructorParameters: Map = itemType.defaultConstructorParameters for (property in itemType.memberProperties) { val attribute = createAttribute(property, constructorParameters, rawItemType, itemType) ?: continue