From 3e67cc46dc983db9f047ad9ac74fae552492cc0e Mon Sep 17 00:00:00 2001 From: Zhixuan Lai Date: Sat, 23 Jan 2021 10:59:42 -0800 Subject: [PATCH] Support java record --- samples/javarecord/build.gradle | 40 +++++++++++++++ .../app/cash/tempest/javarecord/Alias.java | 31 ++++++++++++ .../app/cash/tempest/javarecord/AliasDb.java | 23 +++++++++ .../cash/tempest/javarecord/AliasItem.java | 46 +++++++++++++++++ .../cash/tempest/javarecord/AliasTable.java | 24 +++++++++ .../tempest/javarecord/JavaRecordTest.java | 45 +++++++++++++++++ .../cash/tempest/javarecord/TestModule.java | 50 +++++++++++++++++++ settings.gradle | 1 + .../kotlin/app/cash/tempest/internal/Codec.kt | 32 +++++++----- .../app/cash/tempest/internal/Reflections.kt | 8 ++- .../app/cash/tempest/internal/Schema.kt | 4 +- 11 files changed, 287 insertions(+), 17 deletions(-) create mode 100644 samples/javarecord/build.gradle create mode 100644 samples/javarecord/src/main/java/app/cash/tempest/javarecord/Alias.java create mode 100644 samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasDb.java create mode 100644 samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasItem.java create mode 100644 samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasTable.java create mode 100644 samples/javarecord/src/test/java/app/cash/tempest/javarecord/JavaRecordTest.java create mode 100644 samples/javarecord/src/test/java/app/cash/tempest/javarecord/TestModule.java 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/Alias.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/Alias.java new file mode 100644 index 000000000..1943e3bab --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/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; + +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/AliasDb.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasDb.java new file mode 100644 index 000000000..8b2de568b --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/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; + +import app.cash.tempest.LogicalDb; + +public interface AliasDb extends LogicalDb { + AliasTable aliasTable(); +} diff --git a/samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasItem.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasItem.java new file mode 100644 index 000000000..9ef8382f9 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/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; + +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/AliasTable.java b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/AliasTable.java new file mode 100644 index 000000000..9585375d1 --- /dev/null +++ b/samples/javarecord/src/main/java/app/cash/tempest/javarecord/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; + +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..f31463032 --- /dev/null +++ b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/JavaRecordTest.java @@ -0,0 +1,45 @@ +/* + * 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 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..dc7f910e8 --- /dev/null +++ b/samples/javarecord/src/test/java/app/cash/tempest/javarecord/TestModule.java @@ -0,0 +1,50 @@ +/* + * 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 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