Skip to content

Commit

Permalink
Support java record
Browse files Browse the repository at this point in the history
  • Loading branch information
zhxnlai committed Jan 23, 2021
1 parent e4ee11c commit 3e67cc4
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 17 deletions.
40 changes: 40 additions & 0 deletions samples/javarecord/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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<AliasItem> {
InlineView<Alias.Key, Alias> aliases();
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
32 changes: 19 additions & 13 deletions tempest-internal/src/main/kotlin/app/cash/tempest/internal/Codec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,7 +94,11 @@ internal class ReflectionCodec<A : Any, D : Any> 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))
}
Expand Down Expand Up @@ -158,18 +161,19 @@ internal class ReflectionCodec<A : Any, D : Any> private constructor(
itemAttributes: Map<String, ItemType.Attribute>,
rawItemType: RawItemType
): Codec<Any, Any> {
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<ConstructorParameterBinding<Any, Any, Any?>>()
val appItemConstructorParameters = itemType.defaultConstructorParameters
val dbItemProperties = rawItemType.type.memberProperties.associateBy { it.name }
val constructorParameterBindings =
mutableListOf<ConstructorParameterBinding<Any, Any, Any?>>()
val varBindings = mutableListOf<VarBinding<Any, Any, Any?>>()
val valBindings = mutableListOf<ValBinding<Any, Any, Any?>>()
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)
Expand All @@ -195,7 +199,7 @@ internal class ReflectionCodec<A : Any, D : Any> 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),
Expand All @@ -220,16 +224,18 @@ private sealed class Binding<A, D, P> {
abstract val appProperty: KProperty1<A, P>
abstract val dbProperties: List<KProperty1<D, P>>

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<D, P>).set(result, value)
rawProperty.forceSet(result, value)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,8 +31,11 @@ import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.javaMethod

internal val KClass<*>.primaryConstructorParameters: Map<String, KParameter>
get() = primaryConstructor?.parameters?.associateBy { requireNotNull(it.name) } ?: emptyMap()
internal val KClass<*>.defaultConstructor: KFunction<Any>?
get() = primaryConstructor ?: constructors.singleOrNull()

internal val KClass<*>.defaultConstructorParameters: Map<String, KParameter>
get() = defaultConstructor?.parameters?.associateBy { requireNotNull(it.name) } ?: emptyMap()

data class ClassMember(
val annotations: List<Annotation>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()
for (property in keyType.memberProperties) {
if (property.shouldIgnore) {
Expand Down Expand Up @@ -236,7 +236,7 @@ data class ItemType(
primaryIndex: PrimaryIndex
): Map<String, Attribute> {
val attributes = mutableMapOf<String, Attribute>()
val constructorParameters: Map<String, KParameter> = itemType.primaryConstructorParameters
val constructorParameters: Map<String, KParameter> = itemType.defaultConstructorParameters
for (property in itemType.memberProperties) {
val attribute =
createAttribute(property, constructorParameters, rawItemType, itemType) ?: continue
Expand Down

0 comments on commit 3e67cc4

Please sign in to comment.