diff --git a/.brazil.json b/.brazil.json index 16c8ee6367e..e1a4ab8c75e 100644 --- a/.brazil.json +++ b/.brazil.json @@ -1,11 +1,14 @@ { "dependencies": { + "org.jetbrains.kotlin:kotlin-gradle-plugin:2.*": "KotlinGradlePlugin-2.x", "org.jetbrains.kotlin:kotlin-stdlib-common:2.*.*": "KotlinStdlibCommon-2.x", "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.*.*": "KotlinStdlibJdk8-2.x", "org.jetbrains.kotlin:kotlin-stdlib:2.*.*": "KotlinStdlib-2.x", "org.jetbrains.kotlinx:atomicfu:0.*.*": "Atomicfu-0.x", "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.*": "KotlinxCoroutinesCoreJvm-1.x", - "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.*": "KotlinxCoroutinesJdk8-1.x" + "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.*": "KotlinxCoroutinesJdk8-1.x", + "com.google.devtools.ksp:symbol-processing-api:2.*": "Maven-com-google-devtools-ksp_symbol-processing-api-2.x", + "com.google.devtools.ksp:symbol-processing-gradle-plugin:2.*": "Maven-com-google-devtools-ksp_symbol-processing-gradle-plugin-2.x" }, "packageHandlingRules": { "versioning": { @@ -13,7 +16,6 @@ }, "ignore": [ "aws.sdk.kotlin:bom", - "aws.sdk.kotlin.crt:aws-crt-kotlin-android", "aws.sdk.kotlin:testing", "aws.sdk.kotlin:version-catalog" ], diff --git a/.changes/e4f3b5cb-9cdc-4bc7-b12d-e0a17b635f23.json b/.changes/e4f3b5cb-9cdc-4bc7-b12d-e0a17b635f23.json new file mode 100644 index 00000000000..2925664fa36 --- /dev/null +++ b/.changes/e4f3b5cb-9cdc-4bc7-b12d-e0a17b635f23.json @@ -0,0 +1,9 @@ +{ + "id": "e4f3b5cb-9cdc-4bc7-b12d-e0a17b635f23", + "type": "feature", + "description": "Initial release of Developer Preview of **DynamoDB Mapper** for Kotlin", + "issues": [ + "awslabs/aws-sdk-kotlin#472" + ], + "module": "dynamodb-mapper" +} diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index e5808232a81..86f4f8b4a0b 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -141,6 +141,7 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AddUserAgentMetadata } public final class aws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field DDB_MAPPER Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun getIdentifier ()Ljava/lang/String; diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index 8a0917e05cf..120fc0cd810 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -64,4 +64,5 @@ private fun formatMetrics(metrics: MutableSet): String { @InternalApi public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric { S3_EXPRESS_BUCKET("J"), + DDB_MAPPER("d"), } diff --git a/build.gradle.kts b/build.gradle.kts index fa7f4b27118..87e685c1fab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -145,6 +145,8 @@ configureNexus() val lintPaths = listOf( "**/*.{kt,kts}", "!**/generated-src/**", + "!**/generated/ksp/**", + "!**/kspCaches/**", "!**/smithyprojections/**", ) diff --git a/gradle.properties b/gradle.properties index 3102f92ea4b..8da73b5875a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,9 @@ smithyKotlinDocBaseUrl=https://sdk.amazonaws.com/kotlin/api/smithy-kotlin/api/$s # atomicfu kotlinx.atomicfu.enableJvmIrTransformation=true # FIXME - https://github.com/Kotlin/kotlinx-atomicfu/issues/274 -kotlinx.atomicfu.enableNativeIrTransformation=false \ No newline at end of file +kotlinx.atomicfu.enableNativeIrTransformation=false + +# https://github.com/google/ksp/blob/main/docs/ksp2.md +# Disable KSP2 due to a bug around subsequent invocations +# https://github.com/google/dagger/issues/4181 / https://github.com/google/ksp/issues/1678 +ksp.useKSP2=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff7f078ab27..88d44c67b11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,10 @@ [versions] kotlin-version = "2.0.21" +ksp-version = "2.0.21-1.0.25" # Keep in sync with kotlin-version + dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.13" +aws-kotlin-repo-tools-version = "0.4.14" # libs coroutines-version = "1.9.0" @@ -16,6 +18,7 @@ smithy-kotlin-codegen-version = "0.33.17" smithy-version = "1.51.0" # testing +ddb-local-version = "2.5.2" junit-version = "5.10.5" kotest-version = "5.9.1" kotlinx-benchmark-version = "0.4.12" @@ -26,8 +29,8 @@ slf4j-version = "2.0.16" [libraries] aws-kotlin-repo-tools-build-support = { module="aws.sdk.kotlin.gradle:build-support", version.ref = "aws-kotlin-repo-tools-version" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-version"} -kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin-version"} +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-version" } +kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin-version" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-version" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin-version" } dokka-core = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka-version" } @@ -42,6 +45,9 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines-version" } kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines-version" } +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp-version" } +ksp-gradle-plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp-version" } + slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" } @@ -107,6 +113,8 @@ kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark- kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-version" } mockk = { module = "io.mockk:mockk", version.ref = "mockk-version" } +ddb-local = { module = "com.amazonaws:DynamoDBLocal", version.ref = "ddb-local-version" } + [bundles] # bundle of smithy-kotlin dependencies all AWS service clients have smithy-kotlin-service-client = [ @@ -134,6 +142,8 @@ kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref = kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark-version" } kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version"} +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-version" } aws-kotlin-repo-tools-kmp = { id = "aws.sdk.kotlin.gradle.kmp", version.ref = "aws-kotlin-repo-tools-version" } aws-kotlin-repo-tools-smithybuild = { id = "aws.sdk.kotlin.gradle.smithybuild", version.ref = "aws-kotlin-repo-tools-version" } aws-kotlin-repo-tools-artifactsizemetrics = { id = "aws.sdk.kotlin.gradle.artifactsizemetrics", version.ref = "aws-kotlin-repo-tools-version" } +gradle-plugin-publish = { id = "com.gradle.plugin-publish", version = "1.2.1"} diff --git a/hll/build.gradle.kts b/hll/build.gradle.kts new file mode 100644 index 00000000000..ddd1bdcee90 --- /dev/null +++ b/hll/build.gradle.kts @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import aws.sdk.kotlin.gradle.dsl.configurePublishing +import aws.sdk.kotlin.gradle.kmp.* +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.text.ensureSuffix +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +description = "High-level libraries for the AWS SDK for Kotlin" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL" +extra["moduleName"] = "aws.sdk.kotlin.hll" + +// FIXME 🔽🔽🔽 This is all copied from :aws-runtime and should be commonized 🔽🔽🔽 + +plugins { + alias(libs.plugins.dokka) + alias(libs.plugins.kotlinx.binary.compatibility.validator) + alias(libs.plugins.aws.kotlin.repo.tools.kmp) apply false + jacoco +} + +val sdkVersion: String by project + +// capture locally - scope issue with custom KMP plugin +val libraries = libs + +val optinAnnotations = listOf( + "aws.smithy.kotlin.runtime.ExperimentalApi", + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + "kotlin.RequiresOptIn", +) + +@OptIn(InternalApi::class) +val hllPreviewVersion = if (sdkVersion.contains("-SNAPSHOT")) { // e.g. 1.3.29-beta-SNAPSHOT + sdkVersion + .removeSuffix("-SNAPSHOT") + .ensureSuffix("-beta-SNAPSHOT") +} else { + sdkVersion.ensureSuffix("-beta") // e.g. 1.3.29-beta +} + +subprojects { + group = "aws.sdk.kotlin" + version = hllPreviewVersion + configurePublishing("aws-sdk-kotlin") +} + +subprojects { + if (!needsKmpConfigured) { + return@subprojects + } + + apply { + plugin("org.jetbrains.kotlin.multiplatform") + plugin("org.jetbrains.dokka") + plugin(libraries.plugins.aws.kotlin.repo.tools.kmp.get().pluginId) + } + + kotlin { + explicitApi() + + sourceSets { + // dependencies available for all subprojects + + all { + optinAnnotations.forEach(languageSettings::optIn) + } + + named("commonTest") { + dependencies { + implementation(libraries.kotest.assertions.core) + } + } + + named("jvmTest") { + dependencies { + implementation(libraries.kotest.assertions.core.jvm) + implementation(libraries.slf4j.simple) + } + } + } + } + + dependencies { + dokkaPlugin(project(":dokka-aws")) + } + + tasks.withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + tasks.withType { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } +} + +apiValidation { + val availableSubprojects = subprojects.map { it.name }.toSet() + + ignoredProjects += listOf( + "hll-codegen", + "dynamodb-mapper-codegen", + "dynamodb-mapper-ops-codegen", + "dynamodb-mapper-schema-codegen", + "dynamodb-mapper-schema-generator-plugin-test", + ).filter { it in availableSubprojects } // Some projects may not be in the build depending on bootstrapping +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api b/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api new file mode 100644 index 00000000000..8f5c7313892 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api @@ -0,0 +1,17 @@ +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbAttribute : java/lang/annotation/Annotation { + public abstract fun name ()Ljava/lang/String; +} + +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbIgnore : java/lang/annotation/Annotation { +} + +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbItem : java/lang/annotation/Annotation { + public abstract fun converterName ()Ljava/lang/String; +} + +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbPartitionKey : java/lang/annotation/Annotation { +} + +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbSortKey : java/lang/annotation/Annotation { +} + diff --git a/hll/dynamodb-mapper/dynamodb-mapper-annotations/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper-annotations/build.gradle.kts new file mode 100644 index 00000000000..2576670bf70 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-annotations/build.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "DynamoDbMapper annotations" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Annotations" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.annotations" diff --git a/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt b/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt new file mode 100644 index 00000000000..e102c7f49c4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper + +/** + * Specifies the attribute name for a property in a [DynamoDbItem]-annotated class/interface. If this annotation is not + * included then the attribute name matches the property name. + */ +@Target(AnnotationTarget.PROPERTY) +public annotation class DynamoDbAttribute(val name: String) + +/** + * Specifies that this class/interface describes an item type in a table. All public properties of this type will be mapped to + * attributes unless they are explicitly ignored. + * @param converterName The fully qualified name of the item converter to be used for converting this class/interface. + * If not set, one will be automatically generated. + */ +// FIXME Update to take a KClass, which will require splitting codegen modules due to a circular dependency +@Target(AnnotationTarget.CLASS) +public annotation class DynamoDbItem(val converterName: String = "") + +/** + * Specifies that this property is the primary key for the item. Every top-level [DynamoDbItem] to be used in a table + * must have exactly one partition key. + */ +@Target(AnnotationTarget.PROPERTY) +public annotation class DynamoDbPartitionKey + +/** + * Specifies that this property is the sort key for the item. Every top-level [DynamoDbItem] to be used in a table may + * have at most one sort key. + */ +@Target(AnnotationTarget.PROPERTY) +public annotation class DynamoDbSortKey + +/** + * Specifies that this property should be ignored during mapping. + */ +@Target(AnnotationTarget.PROPERTY) +public annotation class DynamoDbIgnore diff --git a/hll/dynamodb-mapper/dynamodb-mapper-codegen/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper-codegen/build.gradle.kts new file mode 100644 index 00000000000..9f9b36de346 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-codegen/build.gradle.kts @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "DynamoDbMapper code generation" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Codegen" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.codegen" + +plugins { + alias(libs.plugins.kotlin.jvm) + `maven-publish` +} + +dependencies { + implementation(project(":hll:hll-codegen")) +} + +val optinAnnotations = listOf( + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + "kotlin.RequiresOptIn", +) + +kotlin { + explicitApi() + + sourceSets.all { + optinAnnotations.forEach(languageSettings::optIn) + } +} + +val sourcesJar by tasks.creating(Jar::class) { + group = "publishing" + description = "Assembles Kotlin sources jar" + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +publishing { + publications { + create("dynamodb-mapper-codegen") { + from(components["java"]) + artifact(sourcesJar) + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperPkg.kt b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperPkg.kt new file mode 100644 index 00000000000..1abbd8d1a6d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperPkg.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi + +@InternalSdkApi +public object MapperPkg { + @InternalSdkApi + public object Hl { + public val Base: String = "aws.sdk.kotlin.hll.dynamodbmapper" + public val Annotations: String = "$Base.annotations" + public val Internal: String = "$Base.internal" + public val Items: String = "$Base.items" + public val Model: String = "$Base.model" + public val Ops: String = "$Base.operations" + public val PipelineImpl: String = "$Base.pipeline.internal" + public val Values: String = "$Base.values" + public val CollectionValues: String = "$Values.collections" + public val ScalarValues: String = "$Values.scalars" + public val SmithyTypeValues: String = "$Values.smithytypes" + + @InternalSdkApi + public object Expressions { + public val Base: String = "${Hl.Base}.expressions" + public val Internal: String = "$Base.internal" + } + } + + @InternalSdkApi + public object Ll { + public val Base: String = "aws.sdk.kotlin.services.dynamodb" + public val Model: String = "$Base.model" + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt new file mode 100644 index 00000000000..6c93e1001c4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt @@ -0,0 +1,152 @@ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.model + +import aws.sdk.kotlin.hll.codegen.model.Type +import aws.sdk.kotlin.hll.codegen.model.TypeRef +import aws.sdk.kotlin.hll.codegen.model.TypeVar +import aws.sdk.kotlin.hll.codegen.model.Types +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * A container object for various DynamoDbMapper [Type] instances + */ +@InternalSdkApi +public object MapperTypes { + // Low-level types + public val AttributeValue: TypeRef = TypeRef(MapperPkg.Ll.Model, "AttributeValue") + public val AttributeMap: TypeRef = Types.Kotlin.map(Types.Kotlin.String, AttributeValue) + + // High-level types + public val DynamoDbMapper: TypeRef = TypeRef(MapperPkg.Hl.Base, "DynamoDbMapper") + + public object Annotations { + public val ManualPagination: TypeRef = TypeRef(MapperPkg.Hl.Annotations, "ManualPagination") + } + + public object Expressions { + public val BooleanExpr: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "BooleanExpr") + public val Filter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "Filter") + public val KeyFilter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "KeyFilter") + + public object Internal { + public val FilterImpl: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "FilterImpl") + public val ParameterizingExpressionVisitor: TypeRef = + TypeRef(MapperPkg.Hl.Expressions.Internal, "ParameterizingExpressionVisitor") + public val toExpression: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "toExpression") + } + } + + public object Internal { + public val withWrappedClient: TypeRef = TypeRef(MapperPkg.Hl.Internal, "withWrappedClient") + } + + public object Items { + public fun itemSchema(typeVar: String): TypeRef = + TypeRef(MapperPkg.Hl.Items, "ItemSchema", genericArgs = listOf(TypeVar(typeVar))) + + public fun itemSchemaPartitionKey(objectType: TypeRef, pkType: TypeRef): TypeRef = + TypeRef(MapperPkg.Hl.Items, "ItemSchema.PartitionKey", genericArgs = listOf(objectType, pkType)) + + public fun itemSchemaCompositeKey(objectType: TypeRef, pkType: TypeRef, skType: TypeRef): TypeRef = + TypeRef(MapperPkg.Hl.Items, "ItemSchema.CompositeKey", genericArgs = listOf(objectType, pkType, skType)) + + public fun keySpec(keyType: TypeRef): TypeRef = TypeRef(MapperPkg.Hl.Items, "KeySpec", genericArgs = listOf(keyType)) + public val KeySpecByteArray: TypeRef = TypeRef(MapperPkg.Hl.Items, "KeySpec.ByteArray") + public val KeySpecNumber: TypeRef = TypeRef(MapperPkg.Hl.Items, "KeySpec.Number") + public val KeySpecString: TypeRef = TypeRef(MapperPkg.Hl.Items, "KeySpec.String") + public val AttributeDescriptor: TypeRef = TypeRef(MapperPkg.Hl.Items, "AttributeDescriptor") + + public fun itemConverter(objectType: TypeRef): TypeRef = + TypeRef(MapperPkg.Hl.Items, "ItemConverter", genericArgs = listOf(objectType)) + + public val SimpleItemConverter: TypeRef = TypeRef(MapperPkg.Hl.Items, "SimpleItemConverter") + } + + public object Model { + public fun tablePartitionKey(objectType: TypeRef, pkType: TypeRef): TypeRef = TypeRef( + MapperPkg.Hl.Model, + "Table.PartitionKey", + genericArgs = listOf(objectType, pkType), + ) + public fun tableCompositeKey(objectType: TypeRef, pkType: TypeRef, skType: TypeRef): TypeRef = TypeRef( + MapperPkg.Hl.Model, + "Table.CompositeKey", + genericArgs = listOf(objectType, pkType, skType), + ) + public val toItem: TypeRef = TypeRef(MapperPkg.Hl.Model, "toItem") + } + + public object Values { + public fun valueConverter(value: Type): TypeRef = TypeRef(MapperPkg.Hl.Values, "ValueConverter", genericArgs = listOf(value)) + public val ItemToValueConverter: TypeRef = TypeRef(MapperPkg.Hl.Values, "ItemToValueConverter") + public val NullableConverter: TypeRef = TypeRef(MapperPkg.Hl.Values, "NullableConverter") + + public object Collections { + public val ListConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "ListConverter") + public val MapConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "MapConverter") + + public val StringSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "StringSetConverter") + public val CharSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "CharSetConverter") + public val CharArraySetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "CharArraySetConverter") + + public val ByteSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "ByteSetConverter") + public val DoubleSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "DoubleSetConverter") + public val FloatSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "FloatSetConverter") + public val IntSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "IntSetConverter") + public val LongSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "LongSetConverter") + public val ShortSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "ShortSetConverter") + + public val UByteSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "UByteSetConverter") + public val UIntSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "UIntSetConverter") + public val ULongSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "ULongSetConverter") + public val UShortSetConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "UShortSetConverter") + } + + public object Scalars { + public fun enumConverter(enumType: Type): TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "EnumConverter", genericArgs = listOf(enumType)) + + public val BooleanConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "BooleanConverter") + public val StringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "StringConverter") + public val CharConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "CharConverter") + public val CharArrayConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "CharArrayConverter") + + public val ByteConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "ByteConverter") + public val ByteArrayConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "ByteArrayConverter") + public val DoubleConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "DoubleConverter") + public val FloatConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "FloatConverter") + public val IntConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "IntConverter") + public val LongConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "LongConverter") + public val ShortConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "ShortConverter") + public val UByteConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "UByteConverter") + public val UIntConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "UIntConverter") + public val ULongConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "ULongConverter") + public val UShortConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "UShortConverter") + + public val BooleanToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "BooleanToStringConverter") + public val CharArrayToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "TextConverters.CharArrayToStringConverter") + public val CharToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "TextConverters.CharToStringConverter") + public val StringToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "TextConverters.StringToStringConverter") + public val ByteToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.ByteToStringConverter") + public val DoubleToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.DoubleToStringConverter") + public val FloatToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.FloatToStringConverter") + public val IntToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.IntToStringConverter") + public val LongToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.LongToStringConverter") + public val ShortToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.ShortToStringConverter") + public val UByteToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.UByteToStringConverter") + public val UIntToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.UIntToStringConverter") + public val ULongToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.ULongToStringConverter") + public val UShortToStringConverter: TypeRef = TypeRef(MapperPkg.Hl.ScalarValues, "NumberConverters.UShortToStringConverter") + } + + public object SmithyTypes { + public val DefaultInstantConverter: TypeRef = TypeRef(MapperPkg.Hl.SmithyTypeValues, "InstantConverter.Default") + public val UrlConverter: TypeRef = TypeRef(MapperPkg.Hl.SmithyTypeValues, "UrlConverter") + public val DefaultDocumentConverter: TypeRef = TypeRef(MapperPkg.Hl.SmithyTypeValues, "DocumentConverter.Default") + } + } + + public object PipelineImpl { + public val HReqContextImpl: TypeRef = TypeRef(MapperPkg.Hl.PipelineImpl, "HReqContextImpl") + public val MapperContextImpl: TypeRef = TypeRef(MapperPkg.Hl.PipelineImpl, "MapperContextImpl") + public val Operation: TypeRef = TypeRef(MapperPkg.Hl.PipelineImpl, "Operation") + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/build.gradle.kts new file mode 100644 index 00000000000..b9907e8c733 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/build.gradle.kts @@ -0,0 +1,49 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ + +description = "DynamoDbMapper ops code generation" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Codegen :: Ops" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.codegen.ops" + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +dependencies { + implementation(libs.ksp.api) + implementation(project(":hll:hll-codegen")) + implementation(project(":services:dynamodb")) + implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-codegen")) + + testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.kotest.assertions.core.jvm) + testImplementation(libs.kotlin.test.junit5) +} + +val optinAnnotations = listOf( + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + "kotlin.RequiresOptIn", +) + +kotlin { + explicitApi() + + sourceSets.all { + optinAnnotations.forEach(languageSettings::optIn) + } +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessor.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessor.kt new file mode 100644 index 00000000000..5e5f0d47423 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessor.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations + +import aws.sdk.kotlin.hll.codegen.core.CodeGeneratorFactory +import aws.sdk.kotlin.hll.codegen.ksp.processors.HllKspProcessor +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperPkg +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.toHighLevel +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering.HighLevelRenderer +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getDeclaredFunctions +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSFunctionDeclaration + +/** + * The high-level ops KSP processor. This class controls the overall flow of symbol processing, including resolving the + * symbols to use as codegen input (i.e., the low-level DynamoDB operations/types), wiring up rendering context, and + * starting the top-level renderer (which may in turn call other renderers). + */ +internal class HighLevelOpsProcessor(environment: SymbolProcessorEnvironment) : HllKspProcessor(environment) { + private val codeGenerator = environment.codeGenerator + private val logger = environment.logger + private val opAllowlist = environment.options["op-allowlist"]?.split(";") + private val pkg = environment.options["pkg"] ?: MapperPkg.Hl.Ops + + override fun processImpl(resolver: Resolver): List { + logger.info("Scanning low-level DDB client for operations and types") + val operations = getOperations(resolver) + val codegenFactory = CodeGeneratorFactory(codeGenerator, logger) // FIXME Pass dependencies + val ctx = RenderContext(logger, codegenFactory, pkg, "dynamodb-mapper-ops-codegen") + + HighLevelRenderer(ctx, operations).render() + + return listOf() + } + + private fun allow(func: KSFunctionDeclaration): Boolean { + val name = func.simpleName.getShortName() + val allowed = opAllowlist?.contains(name) + + when (allowed) { + false -> logger.warn("$name not in allowlist; skipping codegen") + true -> logger.info("$name in allowlist; processing...") + null -> Unit // There is no allowlist—don't log anything + } + + return allowed ?: true + } + + private fun getOperations(resolver: Resolver): List = resolver + .getClassDeclarationByName()!! + .getDeclaredFunctions() + .filter(::allow) + .map(Operation::from) + .map { it.toHighLevel(pkg) } + .toList() +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessorProvider.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessorProvider.kt new file mode 100644 index 00000000000..59c9fbfe880 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/HighLevelOpsProcessorProvider.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +/** + * The main entry point for KSP. See https://kotlinlang.org/docs/ksp-quickstart.html#create-a-processor-of-your-own for + * more details. + */ +public class HighLevelOpsProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = HighLevelOpsProcessor(environment) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/ItemSourceKind.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/ItemSourceKind.kt new file mode 100644 index 00000000000..58f67c9afe7 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/ItemSourceKind.kt @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.model.TypeRef +import aws.sdk.kotlin.hll.codegen.model.TypeVar +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperPkg + +/** + * Identifies a type in the `ItemSource` hierarchy + * @param hoistedFields Which fields should be hoisted from the low-level request type for this item source kind (e.g., + * for `TableSpec` the `tableName` field should be hoisted) + * @param parent The parent type of this type (if any) + * @param isAbstract Indicates whether this item source kind is purely abstract and should not have an implementation + * class (e.g., `ItemSource` should be abstract and non-instantiable) + */ +internal enum class ItemSourceKind( + val hoistedFields: List, + val parent: ItemSourceKind? = null, + val isAbstract: Boolean = false, +) { + /** + * Indicates the `ItemSource` interface + */ + ItemSource(listOf(), isAbstract = true), + + /** + * Indicates the `Index` interface + */ + Index(listOf("indexName", "tableName"), ItemSource), + + /** + * Indicates the `Table` interface + */ + Table(listOf("tableName"), ItemSource), + + ; + + /** + * Get the [TypeRef] for the `*Spec` type for this item source kind + * @param typeVar The type variable name to use for the generic type + */ + fun getSpecType(typeVar: String): TypeRef = TypeRef(MapperPkg.Hl.Model, "${name}Spec", listOf(TypeVar(typeVar))) +} + +/** + * Identifies the types of `ItemSource` on which an operation can be invoked (e.g., `Scan` can be invoked on a table, + * index, or any generic item source, whereas `GetItem` can only be invoked on a table) + */ +internal val Operation.itemSourceKinds: Set + get() = when (name) { + "Query", "Scan" -> setOf(ItemSourceKind.ItemSource, ItemSourceKind.Index, ItemSourceKind.Table) + else -> setOf(ItemSourceKind.Table) + } diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MapperAttributes.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MapperAttributes.kt new file mode 100644 index 00000000000..df7e979b970 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MapperAttributes.kt @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.smithy.kotlin.runtime.collections.AttributeKey + +/** + * Defines [AttributeKey] instances that relate to the data model of low-level to high-level codegen + */ +internal object MapperAttributes { + /** + * For a given [Operation], this attribute key contains relevant pagination members (if applicable) in the request + * and response + */ + val PaginationInfo: AttributeKey = AttributeKey("aws.sdk.kotlin.ddbmapper#PaginationInfo") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MemberCodegenBehavior.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MemberCodegenBehavior.kt new file mode 100644 index 00000000000..6295f09637a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/MemberCodegenBehavior.kt @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperPkg +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionArgumentsType.* +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionLiteralType.* +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.MemberCodegenBehavior.* + +/** + * Describes a behavior to apply for a given [Member] in a low-level structure when generating code for an equivalent + * high-level structure. This interface implements no behaviors on its own; it merely gives strongly-typed names to + * behaviors that will be implemented by calling code. + */ +internal sealed interface MemberCodegenBehavior { + /** + * Indicates that a member should be copied as-is from a low-level structure to a high-level equivalent (i.e., no + * changes to name, type, etc. are required) + */ + data object PassThrough : MemberCodegenBehavior + + /** + * Indicates that a member is an attribute map which may contain _all_ attributes for a data type (as opposed to + * only _key_ attributes) and should be replaced with a generic type (i.e., a `Map` member + * in a low-level structure should be replaced with a generic `T` member in a high-level structure) + */ + data object MapAll : MemberCodegenBehavior + + /** + * Indicates that a member is an attribute map which contains _key_ attributes for a data type (as opposed to _all_ + * attributes) and should be replaced with a generic type (i.e., a `Map` member in a + * low-level structure should be replaced with a generic `T` member in a high-level structure) + */ + data object MapKeys : MemberCodegenBehavior + + /** + * Indicates that a member is a list of attribute maps which may contain attributes for a data type and should be + * replaced with a generic list type (i.e., a `List>` member in a low-level structure + * should be replaced with a generic `List` member in a high-level structure) + */ + data object ListMapAll : MemberCodegenBehavior + + /** + * Indicates that a member is unsupported and should not be replicated from a low-level structure to the high-level + * equivalent (e.g., a deprecated member that has been replaced with new features need not be carried forward) + */ + data object Drop : MemberCodegenBehavior + + /** + * Indicates that a member from a low-level structure should be "hoisted" outside its high-level equivalent. This is + * similar to [Drop] but indicates that other codegen may use the member in different ways (e.g., a table name + * parameter in a low-level structure may be hoisted to a different API but not added to the equivalent high-level + * structure). + */ + data object Hoist : MemberCodegenBehavior + + /** + * Indicates that a member is a string expression parameter which should be replaced by an expression DSL + * @param type The type of expression this member models + */ + data class ExpressionLiteral(val type: ExpressionLiteralType) : MemberCodegenBehavior + + /** + * Indicates that a member is a map of expression arguments which should be automatically handled by an expression + * DSL + * @param type The type of expression arguments this member models + */ + data class ExpressionArguments(val type: ExpressionArgumentsType) : MemberCodegenBehavior +} + +/** + * Identifies a type of expression literal supported by DynamoDB APIs + */ +internal enum class ExpressionLiteralType { + Condition, + Filter, + KeyCondition, + Projection, + Update, +} + +/** + * Identifies a type of expression arguments supported by DynamoDB APIs + */ +internal enum class ExpressionArgumentsType { + AttributeNames, + AttributeValues, +} + +/** + * Identifies a [MemberCodegenBehavior] for this [Member] by way of various heuristics + */ +internal val Member.codegenBehavior: MemberCodegenBehavior + get() = rules.firstNotNullOfOrNull { it.matchedBehaviorOrNull(this) } ?: PassThrough + +private fun llType(name: String) = TypeRef(MapperPkg.Ll.Model, name) + +private data class Rule( + val namePredicate: (String) -> Boolean, + val typePredicate: (TypeRef) -> Boolean, + val behavior: MemberCodegenBehavior, +) { + constructor(name: String, type: TypeRef, behavior: MemberCodegenBehavior) : + this(name::equals, type::isEquivalentTo, behavior) + + constructor(name: Regex, type: TypeRef, behavior: MemberCodegenBehavior) : + this(name::matches, type::isEquivalentTo, behavior) + + fun matchedBehaviorOrNull(member: Member) = if (matches(member)) behavior else null + fun matches(member: Member) = namePredicate(member.name) && typePredicate(member.type as TypeRef) +} + +private fun Type.isEquivalentTo(other: Type): Boolean = when (this) { + is TypeVar -> other is TypeVar && shortName == other.shortName + is TypeRef -> + other is TypeRef && + fullName == other.fullName && + genericArgs.size == other.genericArgs.size && + genericArgs.zip(other.genericArgs).all { (thisArg, otherArg) -> thisArg.isEquivalentTo(otherArg) } +} + +/** + * Priority-ordered list of dispositions to apply to members found in structures. The first element from this list that + * successfully matches with a member will be chosen. + */ +private val rules = listOf( + // Deprecated expression members not to be carried forward into HLL + Rule("conditionalOperator", llType("ConditionalOperator"), Drop), + Rule("expected", Types.Kotlin.stringMap(llType("ExpectedAttributeValue")), Drop), + Rule("queryFilter", Types.Kotlin.stringMap(llType("Condition")), Drop), + Rule("scanFilter", Types.Kotlin.stringMap(llType("Condition")), Drop), + Rule("keyConditions", Types.Kotlin.stringMap(llType("Condition")), Drop), + Rule("attributesToGet", Types.Kotlin.list(Types.Kotlin.String), Drop), + Rule("attributeUpdates", Types.Kotlin.stringMap(llType("AttributeValueUpdate")), Drop), + + // Hoisted members + Rule("tableName", Types.Kotlin.String, Hoist), + Rule("indexName", Types.Kotlin.String, Hoist), + + // Expression literals + Rule("keyConditionExpression", Types.Kotlin.String, ExpressionLiteral(KeyCondition)), + Rule("filterExpression", Types.Kotlin.String, ExpressionLiteral(Filter)), + + // TODO add support for remaining expression types + Rule("conditionExpression", Types.Kotlin.String, Drop), + Rule("projectionExpression", Types.Kotlin.String, Drop), + Rule("updateExpression", Types.Kotlin.String, Drop), + + // Expression arguments + Rule("expressionAttributeNames", Types.Kotlin.stringMap(Types.Kotlin.String), ExpressionArguments(AttributeNames)), + Rule("expressionAttributeValues", MapperTypes.AttributeMap, ExpressionArguments(AttributeValues)), + + // Mappable members + Rule(".*".toRegex(), Types.Kotlin.list(MapperTypes.AttributeMap), ListMapAll), + Rule("key", MapperTypes.AttributeMap, MapKeys), + Rule(".*".toRegex(), MapperTypes.AttributeMap, MapAll), +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Operation.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Operation.kt new file mode 100644 index 00000000000..18ec6d05043 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Operation.kt @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.ModelAttributes +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.util.plus + +/** + * Derives a high-level [Operation] equivalent for this low-level operation + * @param pkg The Kotlin package to use for the high-level operation's request and response structures + */ +internal fun Operation.toHighLevel(pkg: String): Operation { + val llOperation = this@toHighLevel + val hlRequest = llOperation.request.toHighLevel(pkg) + val hlResponse = llOperation.response.toHighLevel(pkg) + val hlAttributes = llOperation.attributes + (ModelAttributes.LowLevelOperation to llOperation) + return Operation(llOperation.methodName, hlRequest, hlResponse, hlAttributes) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Pagination.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Pagination.kt new file mode 100644 index 00000000000..b24bf8f5bf3 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Pagination.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.Member +import aws.sdk.kotlin.hll.codegen.model.ModelParsingPlugin +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.util.plus + +/** + * Identifies the [Member] instances of an operation's request and response which control pagination + * @param inputToken The field for passing a pagination token into a request + * @param outputToken The field for receiving a pagination token from a request + * @param limit The field for limiting the number of returned results + * @param items The field for getting the low-level items from each page of results + */ +internal data class PaginationMembers( + val inputToken: Member, + val outputToken: Member, + val limit: Member, + val items: Member, +) { + internal companion object { + fun forOperationOrNull(operation: Operation): PaginationMembers? { + val inputToken = operation.request.members.find { it.name == "exclusiveStartKey" } ?: return null + val outputToken = operation.response.members.find { it.name == "lastEvaluatedKey" } ?: return null + val limit = operation.request.members.find { it.name == "limit" } ?: return null + val items = operation.response.members.find { it.name == "items" } ?: return null + + return PaginationMembers(inputToken, outputToken, limit, items) + } + } +} + +/** + * Gets the [PaginationMembers] for an operation, if applicable. If the operation does not support pagination, this + * property returns `null`. + */ +internal val Operation.paginationInfo: PaginationMembers? + get() = attributes.getOrNull(MapperAttributes.PaginationInfo) + +/** + * A codegen plugin that adds DDB-specific pagination info to operations + */ +internal class DdbPaginationPlugin : ModelParsingPlugin { + override fun postProcessOperation(operation: Operation): Operation { + val paginationMembers = PaginationMembers.forOperationOrNull(operation) ?: return operation + val newAttributes = operation.attributes + (MapperAttributes.PaginationInfo to paginationMembers) + return operation.copy(attributes = newAttributes) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Structure.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Structure.kt new file mode 100644 index 00000000000..98afcc8da8b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/model/Structure.kt @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model + +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.codegen.util.plus +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes + +/** + * Derives a high-level [Structure] equivalent for this low-level structure + * @param pkg The Kotlin package to use for the high-level structure + */ +internal fun Structure.toHighLevel(pkg: String): Structure { + val llStructure = this@toHighLevel + + val hlType = TypeRef(pkg, llStructure.type.shortName, listOf(TypeVar("T"))) + + val hlMembers = llStructure.members.mapNotNull { llMember -> + val nullable = llMember.type.nullable + + val hlMember = when (val behavior = llMember.codegenBehavior) { + MemberCodegenBehavior.PassThrough -> llMember + + MemberCodegenBehavior.MapAll, MemberCodegenBehavior.MapKeys -> + llMember.copy(type = TypeVar("T", nullable)) + + MemberCodegenBehavior.ListMapAll -> { + val llListType = llMember.type as? TypeRef ?: error("`ListMapAll` member is required to be a TypeRef") + val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T"))) + llMember.copy(type = hlListType) + } + + is MemberCodegenBehavior.ExpressionLiteral -> { + val expressionType = when (behavior.type) { + ExpressionLiteralType.Filter -> MapperTypes.Expressions.BooleanExpr + ExpressionLiteralType.KeyCondition -> MapperTypes.Expressions.KeyFilter + + // TODO add support for other expression types + else -> return@mapNotNull null + }.nullable(nullable) + + val dslInfo = when (behavior.type) { + ExpressionLiteralType.Filter -> DslInfo( + interfaceType = MapperTypes.Expressions.Filter, + implType = MapperTypes.Expressions.Internal.FilterImpl, + implSingleton = true, + ) + + // KeyCondition doesn't use a top-level DSL (SortKeyCondition is nested) + ExpressionLiteralType.KeyCondition -> null + + // TODO add support for other expression types + else -> return@mapNotNull null + } + + llMember.copy( + name = llMember.name.removeSuffix("Expression"), + type = expressionType, + attributes = llMember.attributes + (ModelAttributes.DslInfo to dslInfo), + ) + } + + else -> null + } + + hlMember?.copy(attributes = hlMember.attributes + (ModelAttributes.LowLevelMember to llMember)) + }.toSet() + + val hlAttributes = llStructure.attributes + (ModelAttributes.LowLevelStructure to llStructure) + + return Structure(hlType, hlMembers, hlAttributes) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/DataTypeGenerator.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/DataTypeGenerator.kt new file mode 100644 index 00000000000..1e636f6fe4b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/DataTypeGenerator.kt @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGenerator +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.codegen.rendering.* +import aws.sdk.kotlin.hll.codegen.util.plus + +/** + * Generates immutable data types from a [Structure] into an underlying [CodeGenerator]. These data types consist of: + * * a read-only `interface` (with a `val` field for each [Structure] member) + * * a `private data class` that serves as the default implementation + * * a `toBuilder` method which turns a built object into a builder + * * a `copy` method which modifies a copy of a built object and returns the result + * * a DSL factory function named after the generated interface for creating new instances + * + * This generator delegates to [BuilderRenderer] to also generate builders. + * + * Example of generated data type: + * + * ```kotlin + * public interface FooRequest { + * public companion object { } + * + * public val id: Int + * public val name: String + * public val foo: T + * } + * + * private data class UserImpl( + * override val id: Int, + * override val name: String, + * override val foo: T, + * ) + * + * // Builder generated by BuilderRenderer + * + * public fun FooRequest.toBuilder(): FooRequestBuilder = FooRequestBuilder.apply { + * id = this@toBuilder.id + * name = this@toBuilder.name + * foo = this@toBuilder.foo + * } + * + * public fun FooRequest.copy(block: FooRequestBuilder.() -> Unit): FooRequest = + * toBuilder.apply(block).build() + * + * public fun FooRequest(block: FooRequestBuilder.() -> Unit): FooRequest = + * FooRequestBuilder().apply(block).build() + * ``` + * @param ctx The active rendering context + * @param generator The underlying generator for the context into which the data type should be written + * @param structure The [Structure] which describes the data type for which to generate code + */ +internal class DataTypeGenerator( + private val ctx: RenderContext, + generator: CodeGenerator, + private val structure: Structure, +) : CodeGenerator by generator { + fun generate() { + write("@#T", Types.Smithy.ExperimentalApi) + withBlock("public interface #T {", "}", structure.type) { + write("@#T", Types.Smithy.ExperimentalApi) + write("public companion object { }") // leave room for future expansion + blankLine() + members { write("public val #L: #T", name, type) } + } + blankLine() + + val genericParams = structure.genericVars().asParamsList() + val genericParamsWithSpacer = if (genericParams.isEmpty()) "" else "$genericParams " + val implName = "${structure.type.shortName}Impl" + val implType = structure.type.copy(shortName = implName) + openBlock("private data class #L#L(", implName, genericParams) + members { write("override val #L: #T,", name, type) } + closeBlock("): #T", structure.type) + blankLine() + + val builderCtx = ctx.copy( + attributes = ctx.attributes + (RenderOptions.VisibilityAttribute to Visibility.PUBLIC), + ) + val builderName = BuilderRenderer.builderName(structure.type) + BuilderRenderer(this, structure.type, implType, structure.members, builderCtx).render() + + blankLine() + write("@#T", Types.Smithy.ExperimentalApi) + withBlock( + "public fun #1L#2T.toBuilder(): #3L#4L = #3L#4L().apply {", + "}", + genericParamsWithSpacer, + structure.type, + builderName, + genericParams, + ) { + members { write("#1L = this@toBuilder.#1L", name) } + } + + blankLine() + write("@#T", Types.Smithy.ExperimentalApi) + withBlock( + "public fun #1L#2T.copy(block: #3L#4L.() -> Unit): #2T =", + "", + genericParamsWithSpacer, + structure.type, + builderName, + genericParams, + ) { + write("toBuilder().apply(block).build()") + } + + blankLine() + write("@#T", Types.Smithy.ExperimentalApi) + withBlock( + "public fun #L#L(block: #L#L.() -> Unit): #T =", + "", + genericParamsWithSpacer, + structure.type.shortName, + builderName, + genericParams, + structure.type, + ) { + write("#L#L().apply(block).build()", builderName, genericParams) + } + } + + private inline fun members(crossinline block: Member.() -> Unit) { + structure.members.forEach { it.block() } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/HighLevelRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/HighLevelRenderer.kt new file mode 100644 index 00000000000..d6be663cbe4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/HighLevelRenderer.kt @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering + +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.model.Type +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ItemSourceKind +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.itemSourceKinds + +/** + * The parent renderer for all codegen from this package. This class orchestrates the various sub-renderers. + * @param ctx The active [RenderContext] + * @param operations A list of the operations in scope for codegen + */ +internal class HighLevelRenderer(private val ctx: RenderContext, private val operations: List) { + fun render() { + operations.forEach(::render) + + val kindTypes = mutableMapOf() + ItemSourceKind.entries.forEach { kind -> + val parentType = kind.parent?.let { kindTypes[it] } + val operations = this.operations.filter { kind in it.itemSourceKinds } + + val renderer = OperationsTypeRenderer(ctx, kind, parentType, operations) + renderer.render() + kindTypes += kind to renderer.interfaceType + } + } + + private fun render(operation: Operation) { + OperationRenderer(ctx, operation).render() + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationRenderer.kt new file mode 100644 index 00000000000..d6a74ff0b97 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationRenderer.kt @@ -0,0 +1,209 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering + +import aws.sdk.kotlin.hll.codegen.core.* +import aws.sdk.kotlin.hll.codegen.model.Member +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.model.Structure +import aws.sdk.kotlin.hll.codegen.model.lowLevel +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.codegen.rendering.RendererBase +import aws.sdk.kotlin.hll.codegen.rendering.info +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.* + +/** + * Renders a dedicated file for a high-level operation, including request/response types, converters between low/high + * types, and a factory method for creating instances of the DDB mapper runtime operation + * @param ctx The active [RenderContext] + * @param operation The [Operation] to codegen + */ +internal class OperationRenderer( + private val ctx: RenderContext, + private val operation: Operation, +) : RendererBase(ctx, operation.name) { + private val requestMembers = operation + .request + .also { ctx.info("For type ${it.lowLevelName}:") } + .lowLevel + .members + .groupBy { m -> m.codegenBehavior.also { ctx.info(" ${m.name} → $it") } } + + private val responseMembers = operation + .response + .also { ctx.info("For type ${it.lowLevelName}:") } + .lowLevel + .members + .groupBy { m -> m.codegenBehavior.also { ctx.info(" ${m.name} → $it") } } + + companion object { + fun factoryFunctionName(operation: Operation) = "${operation.methodName}Operation" + } + + override fun generate() { + renderRequest() + blankLine() + renderResponse() + + renderOperationFactory() + } + + private fun renderOperationFactory() { + val factoryName = factoryFunctionName(operation) + + operation.itemSourceKinds.filterNot { it.isAbstract }.forEach { itemSourceKind -> + blankLine() + withBlock( + "internal fun #L(spec: #T) = #T(", + ")", + factoryName, + itemSourceKind.getSpecType("T"), + MapperTypes.PipelineImpl.Operation, + ) { + write( + "initialize = { highLevelReq: #T -> #T(highLevelReq, spec.schema, #T(spec, #S)) },", + operation.request.type, + MapperTypes.PipelineImpl.HReqContextImpl, + MapperTypes.PipelineImpl.MapperContextImpl, + operation.name, + ) + + writeInline("serialize = { highLevelReq, schema -> highLevelReq.convert(") + requestMembers(MemberCodegenBehavior.Hoist) { + if (name in itemSourceKind.hoistedFields) { + writeInline("spec.#L, ", name) + } else { + writeInline("#L = null, ", name) + } + } + write("schema) },") + + withBlock("lowLevelInvoke = { lowLevelReq ->", "},") { + withBlock("spec.mapper.client.#T { client ->", "}", MapperTypes.Internal.withWrappedClient) { + write("client.#L(lowLevelReq)", operation.methodName) + } + } + + write("deserialize = #L::convert,", operation.response.lowLevelName) + write("interceptors = spec.mapper.config.interceptors,") + } + } + } + + private fun renderRequest() { + DataTypeGenerator(ctx, this, operation.request).generate() + blankLine() + + imports += ImportDirective(operation.request.lowLevel.type, operation.request.lowLevelName) + + openBlock("private fun #T.convert(", operation.request.type) + requestMembers(MemberCodegenBehavior.Hoist) { write("#L: #T, ", name, type) } + write("schema: #T,", MapperTypes.Items.itemSchema("T")) + closeAndOpenBlock(") = #L {", operation.request.lowLevelName) + requestMembers(MemberCodegenBehavior.PassThrough) { write("#L = this@convert.#L", name, highLevel.name) } + requestMembers(MemberCodegenBehavior.MapKeys) { + write( + "this@convert.#L?.let { #L = schema.converter.convertTo(it, schema.keyAttributeNames) }", + highLevel.name, + name, + ) + } + requestMembers(MemberCodegenBehavior.MapAll) { + write("this@convert.#L?.let { #L = schema.converter.convertTo(it) }", highLevel.name, name) + } + requestMembers(MemberCodegenBehavior.ListMapAll) { + write("#L = this@convert.#L?.map { schema.converter.convertTo(it) }", name, highLevel.name) + } + requestMembers(MemberCodegenBehavior.Hoist) { write("this.#1L = #1L", name) } + + if (requestMembers.hasExpressions) renderRequestExpressions() + + closeBlock("}") + } + + private fun renderRequestExpressions() { + blankLine() + write("val expressionVisitor = #T()", MapperTypes.Expressions.Internal.ParameterizingExpressionVisitor) + + requestMembers(MemberCodegenBehavior.ExpressionLiteral(ExpressionLiteralType.Filter)) { + write("#L = this@convert.#L?.accept(expressionVisitor)", name, highLevel.name) + } + + requestMembers(MemberCodegenBehavior.ExpressionLiteral(ExpressionLiteralType.KeyCondition)) { + write( + "#L = this@convert.#L?.#T(schema)?.accept(expressionVisitor)", + name, + highLevel.name, + MapperTypes.Expressions.Internal.toExpression, + ) + } + + requestMembers(MemberCodegenBehavior.ExpressionArguments(ExpressionArgumentsType.AttributeNames)) { + write("#L = expressionVisitor.expressionAttributeNames()", name) + } + + requestMembers(MemberCodegenBehavior.ExpressionArguments(ExpressionArgumentsType.AttributeValues)) { + write("#L = expressionVisitor.expressionAttributeValues()", name) + } + } + + private fun renderResponse() { + DataTypeGenerator(ctx, this, operation.response).generate() + blankLine() + + imports += ImportDirective(operation.response.lowLevel.type, operation.response.lowLevelName) + + withBlock( + "private fun #L.convert(schema: #T) = #T {", + "}", + operation.response.lowLevelName, + MapperTypes.Items.itemSchema("T"), + operation.response.type, + ) { + responseMembers(MemberCodegenBehavior.PassThrough) { write("#L = this@convert.#L", highLevel.name, name) } + + responseMembers(MemberCodegenBehavior.MapKeys, MemberCodegenBehavior.MapAll) { + write( + "#L = this@convert.#L?.#T()?.let(schema.converter::convertFrom)", + highLevel.name, + name, + MapperTypes.Model.toItem, + ) + } + + responseMembers(MemberCodegenBehavior.ListMapAll) { + write( + "#L = this@convert.#L?.map { schema.converter.convertFrom(it.#T()) }", + highLevel.name, + name, + MapperTypes.Model.toItem, + ) + } + } + } + + private val Member.highLevel: Member + get() = + operation.request.members.firstOrNull { it.lowLevel == this } + ?: operation.response.members.first { it.lowLevel == this } +} + +private val Map>.hasExpressions: Boolean + get() = keys.any { + it is MemberCodegenBehavior.ExpressionLiteral || it is MemberCodegenBehavior.ExpressionArguments + } + +private inline operator fun Map>.invoke( + vararg behaviors: MemberCodegenBehavior, + block: Member.() -> Unit, +) { + behaviors.forEach { behavior -> + get(behavior)?.forEach(block) + } +} + +private val Structure.lowLevelName: String + get() = "LowLevel${type.shortName}" diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationsTypeRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationsTypeRenderer.kt new file mode 100644 index 00000000000..1b6b4a428ef --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationsTypeRenderer.kt @@ -0,0 +1,151 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering + +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.codegen.rendering.BuilderRenderer +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.codegen.rendering.RendererBase +import aws.sdk.kotlin.hll.codegen.util.lowercaseFirstChar +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ItemSourceKind +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.itemSourceKinds +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.paginationInfo + +/** + * Renders the `*Operations` interface and `*OperationsImpl` class which contain a method for each codegenned + * operation and dispatches to the factory function rendered in [OperationRenderer] + * @param ctx The active [RenderContext] + * @param itemSourceKind The type of `ItemSource` for which to render operations + * @param parentType The [Type] of the direct parent interface of the to-be-generated `*Operations` interface (e.g., if + * [itemSourceKind] is [ItemSourceKind.Table], then [parentType] should be the generated `ItemSourceOperations` + * interface) + * @param operations A list of the operations in scope for codegen + */ +internal class OperationsTypeRenderer( + private val ctx: RenderContext, + private val itemSourceKind: ItemSourceKind, + private val parentType: Type?, + private val operations: List, +) : RendererBase(ctx, "${itemSourceKind.name}Operations") { + private val entityName = itemSourceKind.name.lowercaseFirstChar + private val intfName = "${itemSourceKind.name}Operations" + + val interfaceType = TypeRef(ctx.pkg, intfName, listOf(TypeVar("T"))) + + override fun generate() { + renderInterface() + renderDslOps() + renderPaginators(forResponses = true) + + if (itemSourceKind.isAbstract) { + blankLine() + renderPaginators(forItems = true) + } else { + blankLine() + renderImpl() + } + } + + private fun renderDslOps() = operations + .filterNot { it.appliesToAncestorKind() } + .forEach(::renderDslOp) + + private fun renderDslOp(op: Operation) { + val builderType = BuilderRenderer.builderType(op.request.type) + val generics = op.request.genericVars().asParamsList(" ") + + if (op.paginationInfo != null) renderManualPaginationAnnotation(op) else blankLine() + + withBlock( + "public suspend inline fun #L#T.#L(crossinline block: #T.() -> Unit): #T =", + "", + generics, + interfaceType, + op.methodName, + builderType, + op.response.type, + ) { + write("#L(#T().apply(block).build())", op.methodName, builderType) + } + + blankLine() + } + + private fun renderImpl() { + val implName = "${itemSourceKind.name}OperationsImpl" + + withBlock( + "internal class #L(private val spec: #T) : #T {", + "}", + implName, + itemSourceKind.getSpecType("T"), + interfaceType, + ) { + operations.forEach { op -> + if (op.paginationInfo != null) renderManualPaginationAnnotation(op) + + write( + "override suspend fun #L(request: #T) = #L(spec).execute(request)", + op.methodName, + op.request.type, + OperationRenderer.factoryFunctionName(op), + ) + + if (op.paginationInfo != null) blankLine() + } + } + } + + private fun renderInterface() { + withDocs { + write("Provides access to operations on a particular #L, which will invoke low-level", entityName) + write("operations after mapping objects to items and vice versa") + write("@param T The type of objects which will be read from and/or written to this #L", entityName) + } + write("@#T", Types.Smithy.ExperimentalApi) + writeInline("public interface #T ", interfaceType) + + parentType?.let { writeInline(": #T ", parentType) } + + withBlock("{", "}") { + operations.forEach(::renderOp) + } + } + + private fun renderManualPaginationAnnotation(op: Operation) { + blankLine() + write( + "@#T(paginatedEquivalent = #S)", + MapperTypes.Annotations.ManualPagination, + PaginatorRenderer.paginatorName(op), + ) + } + + private fun renderOp(op: Operation) { + val overrideModifier = if (op.appliesToAncestorKind()) " override" else "" + + if (op.paginationInfo != null) renderManualPaginationAnnotation(op) + + write( + "public#L suspend fun #L(request: #T): #T", + overrideModifier, + op.methodName, + op.request.type, + op.response.type, + ) + + if (op.paginationInfo != null) blankLine() + } + + private fun renderPaginators(forResponses: Boolean = false, forItems: Boolean = false) = operations + .filterNot { it.paginationInfo == null } + .forEach { op -> PaginatorRenderer(ctx, this, op, interfaceType, forResponses, forItems).render() } + + private fun Operation.appliesToAncestorKind() = itemSourceKind.parent?.let { appliesToKindOrAncestor(it) } ?: false +} + +private fun Operation.appliesToKindOrAncestor(kind: ItemSourceKind): Boolean = + kind in itemSourceKinds || (kind.parent?.let { appliesToKindOrAncestor(it) } ?: false) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/PaginatorRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/PaginatorRenderer.kt new file mode 100644 index 00000000000..b719ca75006 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/PaginatorRenderer.kt @@ -0,0 +1,174 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGenerator +import aws.sdk.kotlin.hll.codegen.model.Operation +import aws.sdk.kotlin.hll.codegen.model.Type +import aws.sdk.kotlin.hll.codegen.model.TypeVar +import aws.sdk.kotlin.hll.codegen.model.Types +import aws.sdk.kotlin.hll.codegen.rendering.BuilderRenderer +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.codegen.util.capitalizeFirstChar +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.paginationInfo + +/** + * Renders paginator methods for an operation. There are two types of paginators handled by this renderer: + * * response paginators (i.e., each response is an element in a `Flow`) + * * item paginators (i.e., each item from each response is an element in a `Flow`) + * + * Rendering these paginators is enabled by setting [forResponses] or [forItems], respectively. + * + * ## Response paginators + * + * When [forResponses] is true, this paginator will render code such as the following: + * + * ```kotlin + * public fun ItemSourceOperations.queryPaginated(initialRequest: QueryRequest): Flow> = flow { + * var cursor = initialRequest.exclusiveStartKey + * var hasNextPage = true + * + * while (hasNextPage) { + * val req = initialRequest.copy { exclusiveStartKey = cursor } + * + * @OptIn(ManualPagination::class) + * val res = this@queryPaginated.query(req) + * + * cursor = res.lastEvaluatedKey + * hasNextPage = cursor != null + * emit(res) + * } + * } + * + * public inline fun ItemSourceOperations.queryPaginated(crossinline block: QueryRequestBuilder.() -> Unit): Flow> = + * queryPaginated(QueryRequestBuilder().apply(block).build()) + * ``` + * + * ## Item paginators + * + * When [forItems] is true, this paginator will render code such as the following: + * + * ```kotlin + * @JvmName("queryItems") + * public fun Flow>.items(): Flow = + * transform { page -> + * page.items?.forEach { item -> + * emit(item) + * } + * } + * ``` + * + * @param ctx The context for this renderer + * @param generator The underlying code generator to use when rendering + * @param op The operation for which paginators will be rendered + * @param extensionOf The type from which paginators will be extension methods. This type is optional—if `null` is + * passed then the rendered paginators will be simple functions instead of extension methods. This only applies to + * **response** paginators. + * @param forResponses Enables generating response paginators + * @param forItems Enables generating item paginators + */ +internal class PaginatorRenderer( + private val ctx: RenderContext, + private val generator: CodeGenerator, + private val op: Operation, + private val extensionOf: Type?, + private val forResponses: Boolean, + private val forItems: Boolean, +) : CodeGenerator by generator { + init { + require(forResponses || forItems) { "One of `forResponses` or `forItems` must be set to true" } + } + + internal companion object { + fun paginatorName(op: Operation) = "${op.methodName}Paginated" + } + + private val paginationInfo = requireNotNull(op.paginationInfo) { "Operation ${op.name} is not paginatable" } + private val name = paginatorName(op) + + private val requestType = op.request.type + private val requestBuilderType = BuilderRenderer.builderType(requestType) + private val responseType = op.response.type + + private val itemFlowType = Types.Kotlinx.Coroutines.Flow.flow(TypeVar("T")) + private val pageFlowType = Types.Kotlinx.Coroutines.Flow.flow(responseType) + + fun render() { + if (forResponses) { + blankLine() + renderPaginatorWithRequest() + blankLine() + renderPaginatorWithDsl() + } + + if (forItems) { + blankLine() + renderItemsPaginator() + } + } + + private fun renderItemsPaginator() { + val jvmName = "${op.methodName}${paginationInfo.items.name.capitalizeFirstChar}" + write("@#T", Types.Smithy.ExperimentalApi) + write("@#T(#S)", Types.Kotlin.Jvm.JvmName, jvmName) + withBlock("public fun #T.items(): #T =", "", pageFlowType, itemFlowType) { + withBlock("#T { page ->", "}", Types.Kotlinx.Coroutines.Flow.transform) { + withBlock("page.#L?.forEach { item ->", "}", paginationInfo.items.name) { + write("emit(item)") + } + } + } + } + + private fun renderPaginatorWithDsl() { + write("@#T", Types.Smithy.ExperimentalApi) + writeInline("public inline fun ") + + extensionOf?.let { writeInline("#T.", extensionOf) } + + withBlock( + "#L(crossinline block: #T.() -> Unit): #T =", + "", + name, + requestBuilderType, + pageFlowType, + ) { + write("#L(#T().apply(block).build())", name, requestBuilderType) + } + } + + private fun renderPaginatorWithRequest() { + write("@#T", Types.Smithy.ExperimentalApi) + writeInline("public fun ") + + extensionOf?.let { writeInline("#T.", extensionOf) } + + withBlock( + "#L(initialRequest: #T): #T = #T {", + "}", + name, + requestType, + pageFlowType, + Types.Kotlinx.Coroutines.Flow.flow, + ) { + write("var cursor = initialRequest.#L", paginationInfo.inputToken.name) + write("var hasNextPage = true") + blankLine() + withBlock("while (hasNextPage) {", "}") { + write("val req = initialRequest.copy { #L = cursor }", paginationInfo.inputToken.name) + + blankLine() + write("@#T(#T::class)", Types.Kotlin.OptIn, MapperTypes.Annotations.ManualPagination) + write("val res = this@#L.#L(req)", name, op.methodName) + + blankLine() + write("cursor = res.#L", paginationInfo.outputToken.name) + write("hasNextPage = cursor != null") + write("emit(res)") + } + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/aws.sdk.kotlin.hll.codegen.model.ModelParsingPlugin b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/aws.sdk.kotlin.hll.codegen.model.ModelParsingPlugin new file mode 100644 index 00000000000..845d4ada987 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/aws.sdk.kotlin.hll.codegen.model.ModelParsingPlugin @@ -0,0 +1 @@ +aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.DdbPaginationPlugin diff --git a/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..1940d69db61 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1,2 @@ +aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.HighLevelOpsProcessorProvider + diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/build.gradle.kts new file mode 100644 index 00000000000..6f68b7979f1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/build.gradle.kts @@ -0,0 +1,66 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ + +description = "DynamoDbMapper schema code generation" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Codegen :: Schema" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.codegen.schema" + +plugins { + alias(libs.plugins.kotlin.jvm) + `maven-publish` +} + +dependencies { + implementation(libs.ksp.api) + implementation(project(":hll:hll-codegen")) + implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-annotations")) + implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-codegen")) + + testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.kotest.assertions.core.jvm) + testImplementation(libs.kotlin.test.junit5) +} + +val optinAnnotations = listOf( + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + "kotlin.RequiresOptIn", +) + +kotlin { + explicitApi() + + sourceSets.all { + optinAnnotations.forEach(languageSettings::optIn) + } +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} + +val sourcesJar by tasks.creating(Jar::class) { + group = "publishing" + description = "Assembles Kotlin sources jar" + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +publishing { + publications { + create("dynamodb-mapper-schema-codegen") { + from(components["java"]) + artifact(sourcesJar) + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessor.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessor.kt new file mode 100644 index 00000000000..aa633affb2e --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessor.kt @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations + +import aws.sdk.kotlin.hll.codegen.core.CodeGeneratorFactory +import aws.sdk.kotlin.hll.codegen.ksp.processors.HllKspProcessor +import aws.sdk.kotlin.hll.codegen.rendering.RenderOptions.VisibilityAttribute +import aws.sdk.kotlin.hll.codegen.rendering.Visibility +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions.DestinationPackageAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions.GenerateBuilderClassesAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions.GenerateGetTableMethodAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.rendering.HighLevelRenderer +import aws.smithy.kotlin.runtime.collections.* +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.validate + +private val annotationName = DynamoDbItem::class.qualifiedName!! + +public class AnnotationsProcessor(private val environment: SymbolProcessorEnvironment) : HllKspProcessor(environment) { + private val logger = environment.logger + + override fun processImpl(resolver: Resolver): List { + logger.info("Searching for symbols annotated with $annotationName") + val annotated = resolver.getSymbolsWithAnnotation(annotationName) + val invalid = annotated.filterNot { it.validate() }.toList() + logger.info("Found invalid classes $invalid") + + val annotatedClasses = annotated + .toList() + .also { logger.info("Found annotated classes: $it") } + .filterIsInstance() + .filter { it.validate() } + + val dependencies = Dependencies(aggregating = true, *(annotatedClasses.mapNotNull { it.containingFile }.toTypedArray())) + val codeGeneratorFactory = CodeGeneratorFactory(environment.codeGenerator, logger, dependencies) + + HighLevelRenderer(annotatedClasses, logger, codeGeneratorFactory, getCodegenAttributes()).render() + + return invalid + } + + /** + * Parse and validate the KSP environment options, turning them into valid attribute values + */ + private fun getCodegenAttributes(): Attributes { + val generateGetTableMethod = environment.options.getOrDefault(GenerateGetTableMethodAttribute.name, "true") + check(generateGetTableMethod.equals("true", ignoreCase = true) || generateGetTableMethod.equals("false", ignoreCase = true)) { "Unsupported value for ${GenerateGetTableMethodAttribute.name}, expected \"true\" or \"false\", got $generateGetTableMethod" } + + return attributesOf { + GenerateBuilderClassesAttribute to GenerateBuilderClasses.valueOf(environment.options[GenerateBuilderClassesAttribute.name] ?: GenerateBuilderClasses.WHEN_REQUIRED.name) + VisibilityAttribute to Visibility.valueOf(environment.options.getOrDefault(VisibilityAttribute.name, Visibility.PUBLIC.name)) + DestinationPackageAttribute to DestinationPackage.fromString(environment.options.getOrDefault(DestinationPackageAttribute.name, "relative=aws.sdk.kotlin.hll.dynamodbmapper.generatedschemas")) + GenerateGetTableMethodAttribute to generateGetTableMethod.equals("true", ignoreCase = true) + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorOptions.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorOptions.kt new file mode 100644 index 00000000000..aa8954422a9 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorOptions.kt @@ -0,0 +1,81 @@ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations + +import aws.smithy.kotlin.runtime.collections.AttributeKey + +/** + * Options for configuring annotation processor codegen + */ +public object AnnotationsProcessorOptions { + /** + * Determines when a builder class should be generated for user classes. Defaults to [GenerateBuilderClasses.WHEN_REQUIRED]. + * With this setting, builder classes will not be generated for user classes which consist of only public mutable members + * and have a zero-arg constructor. + */ + public val GenerateBuilderClassesAttribute: AttributeKey = AttributeKey("GenerateBuilderClasses") + + /** + * Determines the package where code-generated classes / objects will be placed. + * Defaults to [DestinationPackage.Relative] from the package of the class being processed, suffixed with `dynamodbmapper.generatedschemas`. + */ + public val DestinationPackageAttribute: AttributeKey = AttributeKey("DestinationPackage") + + /** + * Determines whether a `DynamoDbMapper.getTable` convenience extension function will be generated. Defaults to true. + */ + public val GenerateGetTableMethodAttribute: AttributeKey = AttributeKey("GenerateGetTableMethod") +} + +/** + * Determines when a builder class should be generated for user classes. Defaults to "WHEN_REQUIRED". + * With this setting, builder classes will not be generated for user classes which consist of only public mutable members + * and have a zero-arg constructor. + */ +public enum class GenerateBuilderClasses { + /** + * Builders will be generated when a buildable structure cannot be inferred for a class (e.g., the class has immutable members or is missing a zero-arg constructor) + */ + WHEN_REQUIRED, + + /** + * Builders will always be generated + */ + ALWAYS, +} + +/** + * Determines the package where code-generated classes / objects will be placed. + * Defaults to [DestinationPackage.Relative] from the package of the class being processed, suffixed with `dynamodbmapper.generatedschemas`. + */ +public sealed class DestinationPackage { + /** + * The package where code-generated classes / objects will be placed. + */ + public abstract val pkg: String + + /** + * Constructs should be code-generated into a package relative to the class being processed. Defaults to `aws.sdk.kotlin.hll.dynamodbmapper.generatedschemas`. + */ + public class Relative(override val pkg: String = "dynamodbmapper.generatedschemas") : DestinationPackage() + + /** + * Constructs should be code-generated into an absolute package. Defaults to `aws.sdk.kotlin.hll.dynamodbmapper.generatedschemas`. + */ + public class Absolute(override val pkg: String = "aws.sdk.kotlin.hll.dynamodbmapper.generatedschemas") : DestinationPackage() + + override fun toString(): String = when (this) { + is Relative -> "relative=$pkg" + is Absolute -> "absolute=$pkg" + } + + public companion object { + public fun fromString(value: String): DestinationPackage { + val (type, pkg) = value.split("=") + + return when (type.lowercase()) { + "relative" -> Relative(pkg) + "absolute" -> Absolute(pkg) + else -> throw IllegalStateException("Expected DestinationPackage type to be `relative` or `absolute`, got $type") + } + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorProvider.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorProvider.kt new file mode 100644 index 00000000000..01a46415e7e --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/AnnotationsProcessorProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations + +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +public class AnnotationsProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): AnnotationsProcessor = AnnotationsProcessor(environment) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/HighLevelRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/HighLevelRenderer.kt new file mode 100644 index 00000000000..51f3fd12253 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/HighLevelRenderer.kt @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGeneratorFactory +import aws.sdk.kotlin.hll.codegen.rendering.RenderContext +import aws.sdk.kotlin.hll.codegen.util.plus +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.DestinationPackage +import aws.smithy.kotlin.runtime.collections.* +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.KSClassDeclaration + +/** + * The parent renderer for all codegen from this package. This class orchestrates the various sub-renderers. + * @param annotatedClasses A list of annotated classes + */ +internal class HighLevelRenderer( + private val annotatedClasses: List, + private val logger: KSPLogger, + private val codegenFactory: CodeGeneratorFactory, + private val codegenAttributes: Attributes = emptyAttributes(), +) { + internal fun render() { + annotatedClasses.forEach { annotated -> + logger.info("Processing annotation on ${annotated.simpleName}") + + val codegenPkg = when (val dstPkg = codegenAttributes[AnnotationsProcessorOptions.DestinationPackageAttribute]) { + is DestinationPackage.Relative -> "${annotated.packageName.asString()}.${dstPkg.pkg}" + is DestinationPackage.Absolute -> dstPkg.pkg + } + + val attributes = codegenAttributes + (SchemaAttributes.ShouldRenderValueConverterAttribute to annotated.shouldRenderValueConverter) + + val renderCtx = RenderContext( + logger, + codegenFactory, + codegenPkg, + "dynamodb-mapper-annotation-processor", + attributes, + ) + + val annotation = SchemaRenderer(annotated, renderCtx) + annotation.render() + } + } + + // Value converters must be generated for any DynamoDbItem which is referenced by another DynamoDbItem + private val KSClassDeclaration.shouldRenderValueConverter: Boolean + get() = annotatedClasses.any { otherClass -> + val name = requireNotNull(qualifiedName).asString() + + otherClass.getAllProperties().any { prop -> + val propType = prop.type.resolve() + val propName = requireNotNull(propType.declaration.qualifiedName).asString() + + // If the property OR any of its arguments reference the annotated type + propName == name || + propType.arguments.any { arg -> + val argType = arg.type?.resolve() + val argName = requireNotNull(argType?.declaration?.qualifiedName).asString() + argName == name + } + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaAttributes.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaAttributes.kt new file mode 100644 index 00000000000..0195309ed81 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaAttributes.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.rendering + +import aws.smithy.kotlin.runtime.collections.AttributeKey + +/** + * Internal schema code generation attributes + */ +internal object SchemaAttributes { + /** + * Whether a value converter should be generated for the class being processed + */ + internal val ShouldRenderValueConverterAttribute: AttributeKey = AttributeKey("ShouldRenderValueConverter") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt new file mode 100644 index 00000000000..9655c3dccca --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt @@ -0,0 +1,366 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.rendering + +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.codegen.rendering.* +import aws.sdk.kotlin.hll.codegen.util.visibility +import aws.sdk.kotlin.hll.dynamodbmapper.* +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.GenerateBuilderClasses +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes +import aws.smithy.kotlin.runtime.collections.get +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.getConstructors +import com.google.devtools.ksp.isAnnotationPresent +import com.google.devtools.ksp.symbol.* + +/** + * Renders the classes and objects required to make a class usable with the DynamoDbMapper such as schemas, builders, and converters. + * @param classDeclaration the [KSClassDeclaration] of the class + * @param ctx the [RenderContext] of the renderer + */ +@OptIn(KspExperimental::class) +internal class SchemaRenderer( + private val classDeclaration: KSClassDeclaration, + private val ctx: RenderContext, +) : RendererBase(ctx, "${classDeclaration.qualifiedName!!.getShortName()}Schema") { + private val className = classDeclaration.qualifiedName!!.getShortName() + private val classType = Type.from(classDeclaration) + + private val builderName = "${className}Builder" + private val converterName = "${className}Converter" + private val schemaName = "${className}Schema" + + private val dynamoDbItemAnnotation = classDeclaration.getAnnotationsByType(DynamoDbItem::class).single() + + private val itemConverter: Type = dynamoDbItemAnnotation + .converterName + .takeIf { it.isNotBlank() } + ?.let { + val pkg = it.substringBeforeLast(".") + val shortName = it.removePrefix("$pkg.") + TypeRef(pkg, shortName) + } ?: TypeRef(ctx.pkg, converterName) + + private val properties = classDeclaration + .getAllProperties() + .filterNot { it.modifiers.contains(Modifier.PRIVATE) || it.isAnnotationPresent(DynamoDbIgnore::class) } + + init { + check(properties.count { it.isPk } == 1) { + "Expected exactly one @DynamoDbPartitionKey annotation on a property" + } + check(properties.count { it.isSk } <= 1) { + "Expected at most one @DynamoDbSortKey annotation on a property" + } + } + + private val partitionKeyProp = properties.single { it.isPk } + private val partitionKeyName = partitionKeyProp + .getAnnotationsByType(DynamoDbAttribute::class) + .singleOrNull()?.name ?: partitionKeyProp.name + + private val sortKeyProp = properties.singleOrNull { it.isSk } + private val sortKeyName = sortKeyProp + ?.getAnnotationsByType(DynamoDbAttribute::class) + ?.singleOrNull()?.name ?: sortKeyProp?.name + + /** + * Skip rendering a class builder if: + * - the user has configured GenerateBuilders to WHEN_REQUIRED (default value) AND + * - the class has all mutable members AND + * - the class has a zero-arg constructor + */ + private val shouldRenderBuilder: Boolean = run { + val alwaysGenerateBuilders = ctx.attributes[AnnotationsProcessorOptions.GenerateBuilderClassesAttribute] == GenerateBuilderClasses.ALWAYS + val hasAllMutableMembers = properties.all { it.isMutable } + val hasZeroArgConstructor = classDeclaration.getConstructors().any { constructor -> constructor.parameters.all { it.hasDefault } } + + !(!alwaysGenerateBuilders && hasAllMutableMembers && hasZeroArgConstructor) + } + + override fun generate() { + if (shouldRenderBuilder) { + renderBuilder() + } + + if (dynamoDbItemAnnotation.converterName.isBlank()) { + renderItemConverter() + } + + if (ctx.attributes[SchemaAttributes.ShouldRenderValueConverterAttribute]) { + renderValueConverter() + } + + renderSchema() + + if (ctx.attributes[AnnotationsProcessorOptions.GenerateGetTableMethodAttribute]) { + renderGetTable() + } + } + + private fun renderBuilder() { + val members = properties.map(Member.Companion::from).toSet() + BuilderRenderer(this, classType, classType, members, ctx).render() + } + + private fun renderItemConverter() { + write("@#T", Types.Smithy.ExperimentalApi) + withBlock("#Lobject #L : #T by #T(", ")", ctx.attributes.visibility, converterName, MapperTypes.Items.itemConverter(classType), MapperTypes.Items.SimpleItemConverter) { + if (shouldRenderBuilder) { + write("builderFactory = ::#L,", builderName) + write("build = #L::build,", builderName) + } else { + write("builderFactory = { $className() },") + write("build = { this },") + } + + withBlock("descriptors = arrayOf(", "),") { + properties.forEach { + renderAttributeDescriptor(it) + } + } + } + blankLine() + } + + /** + * Render a [ValueConverter] for the current class by wrapping the generated/user-provided [ItemConverter] + * with our [ItemToValueConverter] + */ + private fun renderValueConverter() { + // TODO Offer alternate serialization options besides AttributeValue.M? + write("@#T", Types.Smithy.ExperimentalApi) + write( + "#Lval #L : #T = #T.#T(#T)", + ctx.attributes.visibility, + "${className}ValueConverter", + MapperTypes.Values.valueConverter(classType), + itemConverter, + TypeRef("aws.sdk.kotlin.hll.mapping.core.converters", "andThenTo"), + MapperTypes.Values.ItemToValueConverter, + ) + blankLine() + } + + private fun renderAttributeDescriptor(prop: KSPropertyDeclaration) { + withBlock("#T(", "),", MapperTypes.Items.AttributeDescriptor) { + write("#S,", prop.ddbName) // key + write("#L,", "$className::${prop.name}") // getter + + // setter + if (shouldRenderBuilder) { + write("#L,", "$builderName::${prop.name}::set") + } else { + write("#L,", "$className::${prop.name}::set") + } + + // converter + renderValueConverter(prop.type.resolve()) + write("") + } + } + + /** + * Renders a ValueConverter for the [ksType]. + * + * Note: The ValueConverter(s) will be rendered without a newline in order to support deep recursion. + * Callers are responsible for adding a newline after the top-level invocation of this function. + */ + private fun renderValueConverter(ksType: KSType) { + val type = Type.from(ksType) + + when { + ksType.isEnum -> writeInline("#T()", MapperTypes.Values.Scalars.enumConverter(type)) + + // FIXME Handle multi-module codegen rather than assuming nested classes will be in the same [ctx.pkg] + ksType.isUserClass -> writeInline("#T", TypeRef(ctx.pkg, "${ksType.declaration.simpleName.asString()}ValueConverter")) + + type.isGenericFor(Types.Kotlin.Collections.List) -> { + val listElementType = ksType.singleArgument() + writeInline("#T(", MapperTypes.Values.Collections.ListConverter) + renderValueConverter(listElementType) + writeInline(")") + } + + type.isGenericFor(Types.Kotlin.Collections.Map) -> { + check(ksType.arguments.size == 2) { "Expected map type ${ksType.declaration.qualifiedName?.asString()} to have 2 arguments, got ${ksType.arguments.size}" } + + val (keyType, valueType) = ksType.arguments.map { + checkNotNull(it.type?.resolve()) { "Failed to resolved argument type for $it" } + } + + writeInline("#T(#T, ", MapperTypes.Values.Collections.MapConverter, keyType.mapKeyConverter) + renderValueConverter(valueType) + writeInline(")") + } + + type.isGenericFor(Types.Kotlin.Collections.Set) -> writeInline("#T", ksType.singleArgument().setValueConverter) + + type.nullable -> { + writeInline("#T(", MapperTypes.Values.NullableConverter) + renderValueConverter(ksType.makeNotNullable()) + writeInline(")") + } + + else -> writeInline( + "#T", + when (type) { + Types.Smithy.Instant -> MapperTypes.Values.SmithyTypes.DefaultInstantConverter + Types.Smithy.Url -> MapperTypes.Values.SmithyTypes.UrlConverter + Types.Smithy.Document -> MapperTypes.Values.SmithyTypes.DefaultDocumentConverter + + Types.Kotlin.Boolean -> MapperTypes.Values.Scalars.BooleanConverter + Types.Kotlin.String -> MapperTypes.Values.Scalars.StringConverter + Types.Kotlin.CharArray -> MapperTypes.Values.Scalars.CharArrayConverter + Types.Kotlin.Char -> MapperTypes.Values.Scalars.CharConverter + Types.Kotlin.Byte -> MapperTypes.Values.Scalars.ByteConverter + Types.Kotlin.ByteArray -> MapperTypes.Values.Scalars.ByteArrayConverter + Types.Kotlin.Short -> MapperTypes.Values.Scalars.ShortConverter + Types.Kotlin.Int -> MapperTypes.Values.Scalars.IntConverter + Types.Kotlin.Long -> MapperTypes.Values.Scalars.LongConverter + Types.Kotlin.Double -> MapperTypes.Values.Scalars.DoubleConverter + Types.Kotlin.Float -> MapperTypes.Values.Scalars.FloatConverter + Types.Kotlin.UByte -> MapperTypes.Values.Scalars.UByteConverter + Types.Kotlin.UInt -> MapperTypes.Values.Scalars.UIntConverter + Types.Kotlin.UShort -> MapperTypes.Values.Scalars.UShortConverter + Types.Kotlin.ULong -> MapperTypes.Values.Scalars.ULongConverter + + else -> error("Unsupported attribute type $type") + }, + ) + } + } + + private val KSType.mapKeyConverter: Type + get() = when (val type = Type.from(this)) { + // String + Types.Kotlin.ByteArray -> MapperTypes.Values.Scalars.CharArrayToStringConverter + Types.Kotlin.Char -> MapperTypes.Values.Scalars.CharToStringConverter + Types.Kotlin.String -> MapperTypes.Values.Scalars.StringToStringConverter + + // Number + Types.Kotlin.Byte -> MapperTypes.Values.Scalars.ByteToStringConverter + Types.Kotlin.Double -> MapperTypes.Values.Scalars.DoubleToStringConverter + Types.Kotlin.Float -> MapperTypes.Values.Scalars.FloatToStringConverter + Types.Kotlin.Int -> MapperTypes.Values.Scalars.IntToStringConverter + Types.Kotlin.Long -> MapperTypes.Values.Scalars.LongToStringConverter + Types.Kotlin.Short -> MapperTypes.Values.Scalars.ShortToStringConverter + Types.Kotlin.UByte -> MapperTypes.Values.Scalars.UByteToStringConverter + Types.Kotlin.UInt -> MapperTypes.Values.Scalars.UIntToStringConverter + Types.Kotlin.ULong -> MapperTypes.Values.Scalars.ULongToStringConverter + Types.Kotlin.UShort -> MapperTypes.Values.Scalars.UShortToStringConverter + + // Boolean + Types.Kotlin.Boolean -> MapperTypes.Values.Scalars.BooleanToStringConverter + else -> error("Unsupported key type: $type") + } + + private fun KSType.singleArgument(): KSType = checkNotNull(arguments.single().type?.resolve()) { + "Failed to resolve single argument type for ${this.declaration.qualifiedName?.asString()}" + } + + private val KSType.setValueConverter: Type + get() = when (Type.from(this)) { + Types.Kotlin.String -> MapperTypes.Values.Collections.StringSetConverter + Types.Kotlin.Char -> MapperTypes.Values.Collections.CharSetConverter + Types.Kotlin.CharArray -> MapperTypes.Values.Collections.CharArraySetConverter + Types.Kotlin.Byte -> MapperTypes.Values.Collections.ByteSetConverter + Types.Kotlin.Double -> MapperTypes.Values.Collections.DoubleSetConverter + Types.Kotlin.Float -> MapperTypes.Values.Collections.FloatSetConverter + Types.Kotlin.Int -> MapperTypes.Values.Collections.IntSetConverter + Types.Kotlin.Long -> MapperTypes.Values.Collections.LongSetConverter + Types.Kotlin.Short -> MapperTypes.Values.Collections.ShortSetConverter + Types.Kotlin.UByte -> MapperTypes.Values.Collections.UByteSetConverter + Types.Kotlin.UInt -> MapperTypes.Values.Collections.UIntSetConverter + Types.Kotlin.ULong -> MapperTypes.Values.Collections.ULongSetConverter + Types.Kotlin.UShort -> MapperTypes.Values.Collections.UShortSetConverter + else -> error("Unsupported set element $this") + } + + private fun renderSchema() { + val schemaType = if (sortKeyProp != null) { + MapperTypes.Items.itemSchemaCompositeKey(classType, partitionKeyProp.typeRef, sortKeyProp.typeRef) + } else { + MapperTypes.Items.itemSchemaPartitionKey(classType, partitionKeyProp.typeRef) + } + + write("@#T", Types.Smithy.ExperimentalApi) + withBlock("#Lobject #L : #T {", "}", ctx.attributes.visibility, schemaName, schemaType) { + write("override val converter : #1T = #1T", itemConverter) + write("override val partitionKey: #T = #T(#S)", MapperTypes.Items.keySpec(partitionKeyProp.keySpec), partitionKeyProp.keySpecType, partitionKeyName) + if (sortKeyProp != null) { + write("override val sortKey: #T = #T(#S)", MapperTypes.Items.keySpec(sortKeyProp.keySpec), sortKeyProp.keySpecType, sortKeyName!!) + } + } + blankLine() + } + + private val KSPropertyDeclaration.keySpec: TypeRef + get() = when (typeName) { + "kotlin.ByteArray" -> Types.Kotlin.ByteArray + "kotlin.Int" -> Types.Kotlin.Number + "kotlin.String" -> Types.Kotlin.String + else -> error("Unsupported key type $typeName, expected ByteArray, Int, or String") + } + + private val KSPropertyDeclaration.keySpecType: TypeRef + get() = when (typeName) { + "kotlin.ByteArray" -> MapperTypes.Items.KeySpecByteArray + "kotlin.Int" -> MapperTypes.Items.KeySpecNumber + "kotlin.String" -> MapperTypes.Items.KeySpecString + else -> error("Unsupported key type $typeName, expected ByteArray, Int, or String") + } + + private fun renderGetTable() { + docs("Returns a reference to a table named [name] containing items representing [#T]", classType) + + val fnName = "get${className}Table" + write("@#T", Types.Smithy.ExperimentalApi) + write( + "#Lfun #T.#L(name: String): #T = #L(name, #L)", + ctx.attributes.visibility, + MapperTypes.DynamoDbMapper, + fnName, + if (sortKeyProp != null) { + MapperTypes.Model.tableCompositeKey(classType, partitionKeyProp.typeRef, sortKeyProp.typeRef) + } else { + MapperTypes.Model.tablePartitionKey(classType, partitionKeyProp.typeRef) + }, + "getTable", + schemaName, + ) + } +} + +@OptIn(KspExperimental::class) +private val KSType.isUserClass: Boolean + get() = declaration.isAnnotationPresent(DynamoDbItem::class) + +private val KSPropertyDeclaration.typeName: String + get() = checkNotNull(getter?.returnType?.resolve()?.declaration?.qualifiedName?.asString()) { "Failed to determine type name for $this" } + +@OptIn(KspExperimental::class) +private val KSPropertyDeclaration.isPk: Boolean + get() = isAnnotationPresent(DynamoDbPartitionKey::class) + +@OptIn(KspExperimental::class) +private val KSPropertyDeclaration.isSk: Boolean + get() = isAnnotationPresent(DynamoDbSortKey::class) + +private val KSPropertyDeclaration.name: String + get() = simpleName.getShortName() + +private val KSPropertyDeclaration.typeRef: TypeRef + get() = Type.from(type) + +@OptIn(KspExperimental::class) +private val KSPropertyDeclaration.ddbName: String + get() = getAnnotationsByType(DynamoDbAttribute::class).singleOrNull()?.name ?: name + +private val KSType.isEnum: Boolean + get() = (declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..8b069324654 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1,2 @@ +aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorProvider + diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/api/dynamodb-mapper-schema-generator-plugin.api b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/api/dynamodb-mapper-schema-generator-plugin.api new file mode 100644 index 00000000000..0e5ab1eafff --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/api/dynamodb-mapper-schema-generator-plugin.api @@ -0,0 +1,28 @@ +public final class aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin : org/gradle/api/Plugin { + public fun ()V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin$apply$1$1$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public class aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginExtension { + public fun ()V + public final fun getDestinationPackage ()Laws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/DestinationPackage; + public final fun getGenerateBuilderClasses ()Laws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/GenerateBuilderClasses; + public final fun getGenerateGetTableExtension ()Z + public final fun getVisibility ()Laws/sdk/kotlin/hll/codegen/rendering/Visibility; + public final fun setDestinationPackage (Laws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/DestinationPackage;)V + public final fun setGenerateBuilderClasses (Laws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/GenerateBuilderClasses;)V + public final fun setGenerateGetTableExtension (Z)V + public final fun setVisibility (Laws/sdk/kotlin/hll/codegen/rendering/Visibility;)V +} + diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/build.gradle.kts new file mode 100644 index 00000000000..e8ea582c245 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/build.gradle.kts @@ -0,0 +1,215 @@ +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.text.ensureSuffix +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +description = "Plugin used to generate DynamoDbMapper schemas from user classes" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Schema Generator Plugin" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.plugins" + +plugins { + `kotlin-dsl` + `java-gradle-plugin` + alias(libs.plugins.gradle.plugin.publish) +} + +kotlin { + explicitApi() +} + +dependencies { + implementation(kotlin("gradle-plugin", version = kotlin.coreLibrariesVersion)) + implementation(libs.ksp.gradle.plugin) + + implementation(project(":hll:hll-codegen")) // for RenderOptions + implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-schema-codegen")) // for AnnotationsProcessorOptions + implementation(libs.smithy.kotlin.runtime.core) // for AttributeKey + + testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.kotlin.test) +} + +gradlePlugin { + website = "https://github.com/awslabs/aws-sdk-kotlin" + vcsUrl = "https://github.com/awslabs/aws-sdk-kotlin.git" + plugins { + create("dynamodb-mapper-schema-generator") { + id = "aws.sdk.kotlin.hll.dynamodbmapper.schema.generator" + displayName = "DynamoDbMapper Schema Generator" + description = "Plugin used to generate DynamoDbMapper schemas from user classes" + tags = setOf("kotlin", "dynamodb", "aws") + implementationClass = "aws.sdk.kotlin.hll.dynamodbmapper.plugins.SchemaGeneratorPlugin" + } + } +} + +publishing { + publications { + create("dynamodb-mapper-schema-generator-plugin") { + from(components["java"]) + } + } +} + +/** + * The `java-gradle-plugin` plugin creates a javadoc jar by default, conflicting with the empty javadoc jar (emptyJar) + * created in aws-kotlin-repo-tools. Configure dependencies and disable the emptyJar task to avoid conflicts. + */ +afterEvaluate { + tasks.withType { + dependsOn(tasks.named("javadocJar")) + } + + tasks.named("publishDynamodb-mapper-schema-generatorPluginMarkerMavenPublicationToMavenLocal") { + dependsOn(tasks.named("javadocJar")) + } + + tasks.findByName("signDynamodb-mapper-schema-generatorPluginMarkerMavenPublication") + ?.dependsOn(tasks.named("javadocJar")) + + tasks.named("emptyJar") { + enabled = false + } +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} + +// FIXME Commonize the following functions into the aws-kotlin-repo-tools build-support +val sdkVersion: String by project + +@OptIn(InternalApi::class) +val hllPreviewVersion = if (sdkVersion.contains("-SNAPSHOT")) { // e.g. 1.3.29-beta-SNAPSHOT + sdkVersion + .removeSuffix("-SNAPSHOT") + .ensureSuffix("-beta-SNAPSHOT") +} else { + sdkVersion.ensureSuffix("-beta") // e.g. 1.3.29-beta +} + +/** + * Create a file containing the sdkVersion to use as a resource + * This saves us from having to manually change version numbers in multiple places + */ +val generateSdkVersionFile by tasks.registering { + val resourcesDir = layout.buildDirectory.dir("resources/main/aws/sdk/kotlin/hll/dynamodbmapper/plugins").get() + val versionFile = file("$resourcesDir/sdk-version.txt") + val gradlePropertiesFile = rootProject.file("gradle.properties") + inputs.file(gradlePropertiesFile) + outputs.file(versionFile) + sourceSets.main.get().output.dir(resourcesDir) + doLast { + versionFile.writeText(hllPreviewVersion) + } +} + +/** + * Create a file containing the Kotlin version to use as a resource + * This saves us from having to manually change version numbers in multiple places + */ +val generateKotlinVersionFile by tasks.registering { + val resourcesDir = layout.buildDirectory.dir("resources/main/aws/sdk/kotlin/hll/dynamodbmapper/plugins").get() + val versionFile = file("$resourcesDir/kotlin-version.txt") + val versionCatalogFile = rootProject.file("gradle/libs.versions.toml") + inputs.file(versionCatalogFile) + outputs.file(versionFile) + sourceSets.main.get().output.dir(resourcesDir) + doLast { + versionFile.writeText(kotlin.coreLibrariesVersion) + } +} + +/** + * Create a file containing the smithy-kotlin version to use as a resource + * This saves us from having to manually change version numbers in multiple places + */ +val generateSmithyKotlinVersionFile by tasks.registering { + val resourcesDir = layout.buildDirectory.dir("resources/main/aws/sdk/kotlin/hll/dynamodbmapper/plugins").get() + val versionFile = file("$resourcesDir/smithy-kotlin-version.txt") + val versionCatalogFile = rootProject.file("gradle/libs.versions.toml") + inputs.file(versionCatalogFile) + outputs.file(versionFile) + sourceSets.main.get().output.dir(resourcesDir) + doLast { + versionFile.writeText(libs.smithy.kotlin.runtime.core.get().version.toString()) + } +} + +tasks.withType { + dependsOn(generateSdkVersionFile) + dependsOn(generateKotlinVersionFile) + dependsOn(generateSmithyKotlinVersionFile) +} + +tasks.withType { + dependsOn(generateSdkVersionFile) + dependsOn(generateKotlinVersionFile) + dependsOn(generateSmithyKotlinVersionFile) +} + +/** + * Set up Maven Local dependencies to be used in the Gradle TestKit + */ +tasks.register("publishSmithyKotlinToMavenLocal") { + if (gradle.includedBuilds.none { it.name == "smithy-kotlin" }) { + return@register + } + + val smithyKotlin = gradle.includedBuild("smithy-kotlin") + + // FIXME Simply Depend on root project's publishToMavenLocal once https://github.com/gradle/gradle/issues/22335 is fixed + // dependsOn(smithyKotlin.task("publishToMavenLocal")) + + dependsOn(smithyKotlin.task(":runtime:auth:aws-credentials:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:aws-signing-common:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:aws-signing-default:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:http-auth-api:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:http-auth-aws:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:http-auth:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:auth:identity-api:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:observability:logging-slf4j2:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:observability:telemetry-api:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:observability:telemetry-defaults:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:aws-json-protocols:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:aws-protocol-core:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:aws-xml-protocols:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:http-client-engines:http-client-engine-default:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:http-client-engines:http-client-engine-okhttp:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:http-client:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:protocol:http:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:runtime-core:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:serde:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:serde:serde-form-url:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:serde:serde-json:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:serde:serde-xml:publishToMavenLocal")) + dependsOn(smithyKotlin.task(":runtime:smithy-client:publishToMavenLocal")) +} + +tasks.withType { + dependsOn("publishSmithyKotlinToMavenLocal") + + dependsOn(":aws-runtime:aws-config:publishToMavenLocal") + dependsOn(":aws-runtime:aws-core:publishToMavenLocal") + dependsOn(":aws-runtime:aws-endpoint:publishToMavenLocal") + dependsOn(":aws-runtime:aws-http:publishToMavenLocal") + dependsOn(":hll:dynamodb-mapper:dynamodb-mapper-annotations:publishToMavenLocal") + dependsOn(":hll:dynamodb-mapper:dynamodb-mapper-codegen:publishToMavenLocal") + dependsOn(":hll:dynamodb-mapper:dynamodb-mapper-schema-codegen:publishToMavenLocal") + dependsOn(":hll:dynamodb-mapper:dynamodb-mapper-schema-generator-plugin:publishToMavenLocal") + dependsOn(":hll:dynamodb-mapper:dynamodb-mapper:publishToMavenLocal") + dependsOn(":hll:hll-codegen:publishToMavenLocal") + dependsOn(":hll:hll-mapping-core:publishToMavenLocal") + dependsOn(":services:dynamodb:publishToMavenLocal") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin.kt new file mode 100644 index 00000000000..93fdeb18312 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPlugin.kt @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.plugins + +import aws.sdk.kotlin.hll.codegen.rendering.RenderOptions +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions +import aws.smithy.kotlin.runtime.ExperimentalApi +import com.google.devtools.ksp.gradle.KspExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.create + +@ExperimentalApi +public class SchemaGeneratorPlugin : Plugin { + override fun apply(project: Project): Unit = project.run { + val extension = createExtension() + configureDependencies() + + project.afterEvaluate { + extensions.configure { + arg(AnnotationsProcessorOptions.GenerateBuilderClassesAttribute.name, extension.generateBuilderClasses.name) + arg(RenderOptions.VisibilityAttribute.name, extension.visibility.name) + arg(AnnotationsProcessorOptions.DestinationPackageAttribute.name, extension.destinationPackage.toString()) + arg(AnnotationsProcessorOptions.GenerateGetTableMethodAttribute.name, extension.generateGetTableExtension.toString()) + } + } + } + + private fun Project.createExtension(): SchemaGeneratorPluginExtension = extensions.create(SCHEMA_GENERATOR_PLUGIN_EXTENSION) + + private fun Project.configureDependencies() { + logger.info("Configuring dependencies for schema generation...") + pluginManager.apply("com.google.devtools.ksp") + + extensions.configure { + excludeProcessor("aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.HighLevelOpsProcessorProvider") + } + + val sdkVersion = getSdkVersion() + dependencies.add("ksp", "aws.sdk.kotlin:dynamodb-mapper-schema-codegen:$sdkVersion") + } + + // Reads sdk-version.txt for the SDK version to add dependencies on. The file is created in this module's build.gradle.kts + private fun getSdkVersion(): String = checkNotNull(this::class.java.getResource("sdk-version.txt")?.readText()) { "Could not read sdk-version.txt" } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginExtension.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginExtension.kt new file mode 100644 index 00000000000..590d848e463 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginExtension.kt @@ -0,0 +1,34 @@ +package aws.sdk.kotlin.hll.dynamodbmapper.plugins + +import aws.sdk.kotlin.hll.codegen.rendering.Visibility +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.DestinationPackage +import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.GenerateBuilderClasses +import aws.smithy.kotlin.runtime.ExperimentalApi + +internal const val SCHEMA_GENERATOR_PLUGIN_EXTENSION = "dynamoDbMapper" + +@ExperimentalApi +public open class SchemaGeneratorPluginExtension { + /** + * Determines when a builder class should be generated for user classes. Defaults to "WHEN_REQUIRED". + * With this setting, builder classes will not be generated for user classes which consist of only public mutable members + * and have a zero-arg constructor. + */ + public var generateBuilderClasses: GenerateBuilderClasses = GenerateBuilderClasses.WHEN_REQUIRED + + /** + * Determines the visibility of code-generated classes / objects. Defaults to [Visibility.PUBLIC]. + */ + public var visibility: Visibility = Visibility.PUBLIC + + /** + * Determines the package where code-generated classes / objects will be placed. + * Defaults to [DestinationPackage.Relative] from the package of the class being processed, suffixed with `dynamodbmapper.generatedschemas`. + */ + public var destinationPackage: DestinationPackage = DestinationPackage.Relative() + + /** + * Determines whether a `DynamoDbMapper.getTable` convenience extension function will be generated. Defaults to true. + */ + public var generateGetTableExtension: Boolean = true +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt new file mode 100644 index 00000000000..efdf2aa35d6 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt @@ -0,0 +1,579 @@ +package aws.sdk.kotlin.hll.dynamodbmapper.plugins + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.CleanupMode +import org.junit.jupiter.api.io.TempDir +import java.io.File +import kotlin.test.assertContains +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SchemaGeneratorPluginTest { + @TempDir(cleanup = CleanupMode.ON_SUCCESS) + lateinit var testProjectDir: File + + private lateinit var settingsFile: File + private lateinit var buildFile: File + private lateinit var runner: GradleRunner + + private fun getResource(resourceName: String): String = checkNotNull(this::class.java.getResource(resourceName)?.readText()) { "Could not read $resourceName" } + private val kotlinVersion = getResource("kotlin-version.txt") + private val sdkVersion = getResource("sdk-version.txt") + private val smithyKotlinVersion = getResource("smithy-kotlin-version.txt") + + @BeforeEach + fun setup() { + settingsFile = File(testProjectDir, "settings.gradle.kts").also { it.writeText("") } + + buildFile = File(testProjectDir, "build.gradle.kts").also { it.writeText("") } + + // Apply the plugin and necessary dependencies + val buildFileContent = """ + repositories { + mavenCentral() + mavenLocal() + } + + plugins { + id("org.jetbrains.kotlin.jvm") version "$kotlinVersion" + id("aws.sdk.kotlin.hll.dynamodbmapper.schema.generator") + } + + dependencies { + implementation("aws.sdk.kotlin:dynamodb-mapper:$sdkVersion") + implementation("aws.sdk.kotlin:dynamodb-mapper-annotations:$sdkVersion") + implementation("aws.sdk.kotlin:dynamodb-mapper-schema-generator-plugin:$sdkVersion") + } + + """.trimIndent() + buildFile.writeText(buildFileContent) + + runner = GradleRunner + .create() + .withProjectDir(testProjectDir) + .withPluginClasspath() + .withGradleVersion("8.5") // TODO parameterize + .forwardOutput() + .withArguments("--info", "build") + } + + private fun File.prependText(text: String) { + val existingContent = readText() + writeText(text) + appendText(existingContent) + } + + private fun createClassFile(className: String, path: String = "src/main/kotlin/org/example") { + val classFile = File(testProjectDir, "$path/$className.kt") + classFile.ensureParentDirsCreated() + classFile.createNewFile() + classFile.writeText(getResource("/$className.kt")) + } + + @Test + fun testDefaultOptions() { + createClassFile("User") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // Builder + assertContains(schemaContents, "public class UserBuilder") + assertContains(schemaContents, "public var id: Int? = null") + assertContains(schemaContents, "public var givenName: String? = null") + assertContains(schemaContents, "public var surname: String? = null") + assertContains(schemaContents, "public var age: Int? = null") + assertContains(schemaContents, "public fun build(): User") + + // Converter + assertContains( + schemaContents, + """ + object UserConverter : ItemConverter by SimpleItemConverter( + builderFactory = ::UserBuilder, + build = UserBuilder::build, + descriptors = arrayOf( + AttributeDescriptor( + "id", + User::id, + UserBuilder::id::set, + IntConverter + ), + AttributeDescriptor( + "fName", + User::givenName, + UserBuilder::givenName::set, + StringConverter + ), + AttributeDescriptor( + "lName", + User::surname, + UserBuilder::surname::set, + StringConverter + ), + AttributeDescriptor( + "age", + User::age, + UserBuilder::age::set, + IntConverter + ), + ), + ) + """.trimIndent(), + ) + + // Schema + assertContains( + schemaContents, + """ + object UserSchema : ItemSchema.PartitionKey { + override val converter : UserConverter = UserConverter + override val partitionKey: KeySpec = aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec.Number("id") + } + """.trimIndent(), + ) + + // GetTable + assertContains(schemaContents, "fun DynamoDbMapper.getUserTable(name: String): Table.PartitionKey = getTable(name, UserSchema)".trimIndent()) + } + + @Test + fun testBuilderNotRequired() { + createClassFile("BuilderNotRequired") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/BuilderNotRequiredSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // Assert a builder is not generated, because it contains all mutable members with default values and a zero-arg constructor + assertFalse(schemaContents.contains("public class BuilderNotRequiredBuilder {")) + + // Assert that the class itself is used as a builder + assertContains( + schemaContents, + """ + object BuilderNotRequiredConverter : ItemConverter by SimpleItemConverter( + builderFactory = { BuilderNotRequired() }, + build = { this }, + descriptors = arrayOf( + AttributeDescriptor( + "id", + BuilderNotRequired::id, + BuilderNotRequired::id::set, + IntConverter + ), + AttributeDescriptor( + "fName", + BuilderNotRequired::givenName, + BuilderNotRequired::givenName::set, + StringConverter + ), + AttributeDescriptor( + "lName", + BuilderNotRequired::surname, + BuilderNotRequired::surname::set, + StringConverter + ), + AttributeDescriptor( + "age", + BuilderNotRequired::age, + BuilderNotRequired::age::set, + IntConverter + ), + ), + ) + """.trimIndent(), + ) + } + + @Test + fun testGenerateBuilderOption() { + val pluginConfiguration = """ + import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.GenerateBuilderClasses + import aws.smithy.kotlin.runtime.ExperimentalApi + + @OptIn(ExperimentalApi::class) + dynamoDbMapper { + generateBuilderClasses = GenerateBuilderClasses.ALWAYS + } + + """.trimIndent() + buildFile.prependText(pluginConfiguration) + + createClassFile("BuilderNotRequired") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/BuilderNotRequiredSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // Assert a builder is still generated, because we configured GenerateBuilderClasses.ALWAYS + assertContains(schemaContents, "public class BuilderNotRequiredBuilder") + assertContains(schemaContents, "public var id: Int? = null") + assertContains(schemaContents, "public var givenName: String? = null") + assertContains(schemaContents, "public var surname: String? = null") + assertContains(schemaContents, "public var age: Int? = null") + assertContains(schemaContents, "public fun build(): BuilderNotRequired") + } + + @Test + fun testVisibilityOption() { + val pluginConfiguration = """ + import aws.sdk.kotlin.hll.codegen.rendering.Visibility + import aws.smithy.kotlin.runtime.ExperimentalApi + + @OptIn(ExperimentalApi::class) + dynamoDbMapper { + visibility = Visibility.INTERNAL + } + + """.trimIndent() + buildFile.prependText(pluginConfiguration) + + createClassFile("User") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // All codegenerated constructs should be `internal` + assertContains(schemaContents, "internal class UserBuilder") + assertContains(schemaContents, "internal object UserConverter") + assertContains(schemaContents, "internal object UserSchema") + assertContains(schemaContents, "internal fun DynamoDbMapper.getUserTable") + } + + @Test + fun testGenerateGetTableFunctionOption() { + val pluginConfiguration = """ + import aws.smithy.kotlin.runtime.ExperimentalApi + + @OptIn(ExperimentalApi::class) + dynamoDbMapper { + generateGetTableExtension = false + } + + """.trimIndent() + buildFile.prependText(pluginConfiguration) + + createClassFile("User") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // getUserTable should not be generated + assertContains(schemaContents, "public class UserBuilder") + assertContains(schemaContents, "public object UserConverter") + assertContains(schemaContents, "public object UserSchema") + assertFalse(schemaContents.contains("public fun DynamoDbMapper.getUserTable")) + } + + @Test + fun testRelativeDestinationPackage() { + val pluginConfiguration = """ + import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.DestinationPackage + import aws.smithy.kotlin.runtime.ExperimentalApi + + @OptIn(ExperimentalApi::class) + dynamoDbMapper { + destinationPackage = DestinationPackage.Relative("hello.moto") + } + + """.trimIndent() + buildFile.prependText(pluginConfiguration) + + createClassFile("User") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/hello/moto/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + assertContains(schemaContents, "package org.example.hello.moto") + } + + @Test + fun testAbsoluteDestinationPackage() { + val pluginConfiguration = """ + import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.DestinationPackage + import aws.smithy.kotlin.runtime.ExperimentalApi + + @OptIn(ExperimentalApi::class) + dynamoDbMapper { + destinationPackage = DestinationPackage.Absolute("absolutely.my.`package`") + } + + """.trimIndent() + buildFile.prependText(pluginConfiguration) + + createClassFile("User") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/absolutely/my/`package`/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + assertContains(schemaContents, "package absolutely.my.`package`") + } + + @Test + fun testGeneratedItemConverter() { + buildFile.appendText( + """ + dependencies { + testImplementation(kotlin("test")) + } + + """.trimIndent(), + ) + + createClassFile("User") + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/UserTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/tests/UserTest.kt")) + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/UserSchema.kt") + assertTrue(schemaFile.exists()) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testDynamoDbIgnore() { + createClassFile("IgnoredProperty") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/IgnoredPropertySchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + assertContains(schemaContents, "public class IgnoredProperty") + assertContains(schemaContents, "public var id: Int? = null") + assertContains(schemaContents, "public var givenName: String? = null") + assertContains(schemaContents, "public var surname: String? = null") + assertContains(schemaContents, "public var age: Int? = null") + assertContains(schemaContents, "public fun build(): IgnoredProperty") + + // ssn is annotated with DynamoDbIgnore + assertFalse(schemaContents.contains("public var ssn: String? = null")) + } + + @Test + fun testDynamoDbItemConverter() { + createClassFile("custom-item-converter/CustomUser") + createClassFile("custom-item-converter/CustomItemConverter", "src/main/kotlin/my/custom/item/converter") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/CustomUserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + assertFalse(schemaContents.contains("public object CustomUserItemConverter : ItemConverter by SimpleItemConverter")) + assertContains( + schemaContents, + """ + public object CustomUserSchema : ItemSchema.PartitionKey { + override val converter : MyCustomUserConverter = MyCustomUserConverter + override val partitionKey: KeySpec = aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec.Number("id") + } + """.trimIndent(), + ) + } + + @Test + fun testPrimitives() { + buildFile.appendText( + """ + dependencies { + implementation("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion") + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/Primitives") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/PrimitivesSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/PrimitivesTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/PrimitivesTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testNullableTypes() { + buildFile.appendText( + """ + dependencies { + implementation("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion") + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/NullableItem") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/NullableItemSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/NullableItemTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/NullableItemTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testLists() { + buildFile.appendText( + """ + dependencies { + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/Lists") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/ListsSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/ListsTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/ListsTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testSets() { + buildFile.appendText( + """ + dependencies { + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/Sets") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/SetsSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/SetsTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/SetsTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testMaps() { + buildFile.appendText( + """ + dependencies { + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/Maps") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/MapsSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/MapsTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/MapsTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + + @Test + fun testRenamedPartitionKey() { + createClassFile("RenamedPartitionKey") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/RenamedPartitionKeySchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // Schema should use the renamed partition key + assertContains( + schemaContents, + """ + object RenamedPartitionKeySchema : ItemSchema.PartitionKey { + override val converter : RenamedPartitionKeyConverter = RenamedPartitionKeyConverter + override val partitionKey: KeySpec = aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec.Number("user_id") + } + """.trimIndent(), + ) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/BuilderNotRequired.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/BuilderNotRequired.kt new file mode 100644 index 00000000000..2861b2c8f6d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/BuilderNotRequired.kt @@ -0,0 +1,13 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class BuilderNotRequired( + @DynamoDbPartitionKey var id: Int = 1, + @DynamoDbAttribute("fName") var givenName: String = "Johnny", + @DynamoDbAttribute("lName") var surname: String = "Appleseed", + var age: Int = 0, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt new file mode 100644 index 00000000000..ff9c091258a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt @@ -0,0 +1,17 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbIgnore +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class IgnoredProperty( + @DynamoDbPartitionKey var id: Int, + @DynamoDbAttribute("fName") var givenName: String, + @DynamoDbAttribute("lName") var surname: String, + var age: Int, + + @DynamoDbIgnore + var ssn: String? = null, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt new file mode 100644 index 00000000000..e507c9af815 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt @@ -0,0 +1,16 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class RenamedPartitionKey( + @DynamoDbPartitionKey + @DynamoDbAttribute("user_id") + var id: Int, + + @DynamoDbAttribute("fName") var givenName: String, + @DynamoDbAttribute("lName") var surname: String, + var age: Int, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/User.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/User.kt new file mode 100644 index 00000000000..65cbdea0620 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/User.kt @@ -0,0 +1,13 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class User( + @DynamoDbPartitionKey var id: Int, + @DynamoDbAttribute("fName") var givenName: String, + @DynamoDbAttribute("lName") var surname: String, + var age: Int, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt new file mode 100644 index 00000000000..abe58305992 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt @@ -0,0 +1,41 @@ +package my.custom.item.converter + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.smithy.kotlin.runtime.ExperimentalApi +import org.example.CustomUser + +@OptIn(ExperimentalApi::class) +public object MyCustomUserConverter : ItemConverter by SimpleItemConverter( + builderFactory = { CustomUser() }, + build = { this }, + descriptors = arrayOf( + AttributeDescriptor( + "id", + CustomUser::id, + CustomUser::id::set, + IntConverter, + ), + AttributeDescriptor( + "myCustomFirstName", + CustomUser::givenName, + CustomUser::givenName::set, + StringConverter, + ), + AttributeDescriptor( + "myCustomLastName", + CustomUser::surname, + CustomUser::surname::set, + StringConverter, + ), + AttributeDescriptor( + "myCustomAge", + CustomUser::age, + CustomUser::age::set, + IntConverter, + ), + ), +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt new file mode 100644 index 00000000000..75fe3234cdb --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt @@ -0,0 +1,12 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem("my.custom.item.converter.MyCustomUserConverter") +public data class CustomUser( + @DynamoDbPartitionKey var id: Int = 1, + var givenName: String = "Johnny", + var surname: String = "Appleseed", + var age: Int = 0, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Lists.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Lists.kt new file mode 100644 index 00000000000..566cf45a57d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Lists.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +enum class EnumAnimals { + CAT, + DOG, + SHEEP, +} + +@DynamoDbItem +public data class Lists( + @DynamoDbPartitionKey var id: Int, + var listBoolean: List, + var listString: List, + var listCharArray: List, + var listChar: List, + var listByte: List, + var listByteArray: List, + var listShort: List, + var listInt: List, + var listLong: List, + var listDouble: List, + var listFloat: List, + var listUByte: List, + var listUInt: List, + var listUShort: List, + var listULong: List, + var listEnum: List, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Lists) return false + + if (id != other.id) return false + if (listBoolean != other.listBoolean) return false + if (listString != other.listString) return false + if (listCharArray.size != other.listCharArray.size) return false + if (!listCharArray.zip(other.listCharArray).all { (a, b) -> a.contentEquals(b) }) return false + if (listChar != other.listChar) return false + if (listByte != other.listByte) return false + if (listByteArray.size != other.listByteArray.size) return false + if (!listByteArray.zip(other.listByteArray).all { (a, b) -> a.contentEquals(b) }) return false + if (listShort != other.listShort) return false + if (listInt != other.listInt) return false + if (listLong != other.listLong) return false + if (listDouble != other.listDouble) return false + if (listFloat != other.listFloat) return false + if (listUByte != other.listUByte) return false + if (listUInt != other.listUInt) return false + if (listUShort != other.listUShort) return false + if (listULong != other.listULong) return false + if (listEnum != other.listEnum) return false + + return true + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Maps.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Maps.kt new file mode 100644 index 00000000000..8b67f3d2c59 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Maps.kt @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +enum class EnumAnimals { + CAT, + DOG, + SHEEP, +} + +@DynamoDbItem +public data class Maps( + @DynamoDbPartitionKey var id: Int, + var mapStringString: Map, + var mapStringInt: Map, + var mapIntString: Map, + var mapLongInt: Map, + var mapStringBoolean: Map, + var mapStringListString: Map>, + var mapStringListMapStringString: Map>>, + var mapEnum: Map, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt new file mode 100644 index 00000000000..62576330896 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey +import aws.smithy.kotlin.runtime.time.Instant + +@DynamoDbItem +public data class NullableItem( + @DynamoDbPartitionKey var id: Int, + + /** + * A selection of nullable types + */ + var string: String?, + var byte: Byte?, + var int: Int?, + var instant: Instant?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NullableItem) return false + + if (id != other.id) return false + if (string != other.string) return false + if (byte != other.byte) return false + if (int != other.int) return false + if (instant?.epochSeconds != other.instant?.epochSeconds) return false + + return true + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Primitives.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Primitives.kt new file mode 100644 index 00000000000..ff2915ac50b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Primitives.kt @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey +import aws.smithy.kotlin.runtime.content.Document +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant + +enum class EnumAnimals { + CAT, + DOG, + SHEEP, +} + +@DynamoDbItem +public data class Primitives( + @DynamoDbPartitionKey var id: Int, + + /** + * Enums + */ + var animal: EnumAnimals, + + /** + * Primitives + */ + var boolean: Boolean, + var string: String, + var charArray: CharArray, + var char: Char, + var byte: Byte, + var byteArray: ByteArray, + var short: Short, + var int: Int, + var long: Long, + var double: Double, + var float: Float, + var uByte: UByte, + var uInt: UInt, + var uShort: UShort, + var uLong: ULong, + + /** + * Smithy types + */ + var instant: Instant, + var url: Url, + var document: Document, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Primitives) return false + + if (id != other.id) return false + if (animal != other.animal) return false + if (boolean != other.boolean) return false + if (string != other.string) return false + if (!charArray.contentEquals(other.charArray)) return false + if (char != other.char) return false + if (byte != other.byte) return false + if (!byteArray.contentEquals(other.byteArray)) return false + if (short != other.short) return false + if (int != other.int) return false + if (long != other.long) return false + if (double != other.double) return false + if (float != other.float) return false + if (uByte != other.uByte) return false + if (uInt != other.uInt) return false + if (uShort != other.uShort) return false + if (uLong != other.uLong) return false + if (instant.epochSeconds != other.instant.epochSeconds) return false + if (url != other.url) return false + if (document != other.document) return false + + return true + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Sets.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Sets.kt new file mode 100644 index 00000000000..65b1dec69dd --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/Sets.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class Sets( + @DynamoDbPartitionKey var id: Int, + + /** + * Sets + */ + var setString: Set, + var setCharArray: Set, + var setChar: Set, + var setByte: Set, + var setDouble: Set, + var setFloat: Set, + + var setInt: Set, + var setLong: Set, + var setShort: Set, + var setUByte: Set, + var setUInt: Set, + var setULong: Set, + var setUShort: Set, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sets) return false + + if (id != other.id) return false + if (setString != other.setString) return false + if (setCharArray.size != other.setCharArray.size) return false + if (!setCharArray.all { thisArray -> other.setCharArray.any { otherArray -> thisArray.contentEquals(otherArray) } }) return false + if (setChar != other.setChar) return false + if (setByte != other.setByte) return false + if (setDouble != other.setDouble) return false + if (setFloat != other.setFloat) return false + if (setInt != other.setInt) return false + if (setLong != other.setLong) return false + if (setShort != other.setShort) return false + if (setUByte != other.setUByte) return false + if (setUInt != other.setUInt) return false + if (setULong != other.setULong) return false + if (setUShort != other.setUShort) return false + + return true + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/ListsTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/ListsTest.kt new file mode 100644 index 00000000000..eb68e343e9b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/ListsTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import org.example.dynamodbmapper.generatedschemas.ListsConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class ListsTest { + @Test + fun converterTest() { + val lists = Lists( + id = 1, + listBoolean = listOf(false, true, false, true), + listString = listOf("foo", "bar", "baz"), + listCharArray = listOf(charArrayOf('a', 'b'), charArrayOf('c', 'd'), charArrayOf('e', 'f')), + listChar = listOf('a', 'b', 'c'), + listByte = listOf(1, 2, 3), + listByteArray = listOf(byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6)), + listShort = listOf(Short.MIN_VALUE, 0, Short.MAX_VALUE), + listInt = listOf(Int.MIN_VALUE, 0, Int.MAX_VALUE), + listLong = listOf(Long.MIN_VALUE, 0L, Long.MAX_VALUE), + listDouble = listOf(Double.MIN_VALUE, 0.0, Double.MAX_VALUE), + listFloat = listOf(Float.MIN_VALUE, 0f, Float.MAX_VALUE), + listUByte = listOf(UByte.MIN_VALUE, UByte.MAX_VALUE), + listUInt = listOf(UInt.MIN_VALUE, UInt.MAX_VALUE), + listUShort = listOf(UShort.MIN_VALUE, UShort.MAX_VALUE), + listULong = listOf(ULong.MIN_VALUE, ULong.MAX_VALUE), + listEnum = listOf(EnumAnimals.CAT, EnumAnimals.DOG, EnumAnimals.SHEEP), + ) + + val item = ListsConverter.convertTo(lists) + val converted = ListsConverter.convertFrom(item) + assertEquals(lists, converted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/MapsTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/MapsTest.kt new file mode 100644 index 00000000000..3136f21deed --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/MapsTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import org.example.dynamodbmapper.generatedschemas.MapsConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class MapsTest { + @Test + fun converterTest() { + val maps = Maps( + id = 1, + mapStringString = mapOf("key1" to "value1", "key2" to "value2"), + mapStringInt = mapOf("one" to 1, "two" to 2, "three" to 3), + mapIntString = mapOf(1 to "one", 2 to "two", 3 to "three"), + mapLongInt = mapOf(1L to 10, 2L to 20, 3L to 30), + mapStringBoolean = mapOf("true" to true, "false" to false), + mapStringListString = mapOf( + "fruits" to listOf("apple", "banana", "cherry"), + "colors" to listOf("red", "green", "blue"), + ), + mapStringListMapStringString = mapOf( + "person1" to listOf( + mapOf("name" to "John", "age" to "30"), + mapOf("city" to "New York", "country" to "USA"), + ), + "person2" to listOf( + mapOf("name" to "Alice", "age" to "25"), + mapOf("city" to "London", "country" to "UK"), + ), + ), + mapEnum = mapOf("pet1" to EnumAnimals.CAT, "pet2" to EnumAnimals.DOG, "pet3" to EnumAnimals.SHEEP), + ) + + val item = MapsConverter.convertTo(maps) + val converted = MapsConverter.convertFrom(item) + assertEquals(maps, converted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt new file mode 100644 index 00000000000..1c5b89d5a90 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.time.Instant +import org.example.dynamodbmapper.generatedschemas.NullableItemConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class NullableItemTest { + @Test + fun converterTest() { + val nullable = NullableItem( + id = 1, + string = null, + byte = null, + int = 5, + instant = Instant.now(), + ) + + val item = NullableItemConverter.convertTo(nullable) + val converted = NullableItemConverter.convertFrom(item) + assertEquals(nullable, converted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/PrimitivesTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/PrimitivesTest.kt new file mode 100644 index 00000000000..7c62575deef --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/PrimitivesTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.content.Document +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant +import org.example.dynamodbmapper.generatedschemas.PrimitivesConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class PrimitivesTest { + @Test + fun converterTest() { + val primitive = Primitives( + id = 1, + animal = EnumAnimals.CAT, + boolean = true, + string = "string", + charArray = charArrayOf('a', 'b', 'c'), + char = 'b', + byte = 42, + byteArray = byteArrayOf(5, 4, 3, 2, 1), + short = Short.MAX_VALUE, + int = Int.MAX_VALUE, + long = Long.MAX_VALUE, + double = Double.MAX_VALUE, + float = Float.MAX_VALUE, + uByte = UByte.MAX_VALUE, + uInt = UInt.MAX_VALUE, + uShort = UShort.MAX_VALUE, + uLong = ULong.MAX_VALUE, + instant = Instant.now(), + url = Url.parse("https://aws.amazon.com"), + document = Document.Number(5), + ) + + val item = PrimitivesConverter.convertTo(primitive) + val converted = PrimitivesConverter.convertFrom(item) + assertEquals(primitive, converted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/SetsTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/SetsTest.kt new file mode 100644 index 00000000000..124de3405c8 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/SetsTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import org.example.dynamodbmapper.generatedschemas.SetsConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class SetsTest { + @Test + fun converterTest() { + val sets = Sets( + id = 1, + setString = setOf("one", "two", "three"), + setCharArray = setOf(charArrayOf('a', 'b'), charArrayOf('c', 'd')), + setChar = setOf('x', 'y', 'z'), + setByte = setOf(10, 20, 30), + setDouble = setOf(1.1, 2.2, 3.3), + setFloat = setOf(1.0f, 2.0f, 3.0f), + setInt = setOf(100, 200, 300), + setLong = setOf(1000L, 2000L, 3000L), + setShort = setOf(1000.toShort(), 2000.toShort(), 3000.toShort()), + setUByte = setOf(10u, 20u, 30u), + setUInt = setOf(100u, 200u, 300u), + setULong = setOf(1000uL, 2000uL, 3000uL), + setUShort = setOf(1000u.toUShort(), 2000u.toUShort(), 3000u.toUShort()), + ) + + val item = SetsConverter.convertTo(sets) + val converted = SetsConverter.convertFrom(item) + assertEquals(sets, converted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/tests/UserTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/tests/UserTest.kt new file mode 100644 index 00000000000..bead735690a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/tests/UserTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import org.example.dynamodbmapper.generatedschemas.UserConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +class UserTest { + @Test + fun testConversion() { + val user = User(123, "Steve", "Rogers", 84) + val converted = UserConverter.convertTo(user) + + assertEquals( + itemOf( + "id" to AttributeValue.N("123"), + "fName" to AttributeValue.S("Steve"), + "lName" to AttributeValue.S("Rogers"), + "age" to AttributeValue.N("84"), + ), + converted, + ) + + val unconverted = UserConverter.convertFrom(converted) + + assertEquals(user, unconverted) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/api/dynamodb-mapper.api b/hll/dynamodb-mapper/dynamodb-mapper/api/dynamodb-mapper.api new file mode 100644 index 00000000000..6f000c92e6a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/api/dynamodb-mapper.api @@ -0,0 +1,1410 @@ +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Companion; + public abstract fun getClient ()Laws/sdk/kotlin/services/dynamodb/DynamoDbClient; + public abstract fun getConfig ()Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config; + public abstract fun getTable (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Table$CompositeKey; + public abstract fun getTable (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Table$PartitionKey; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Companion { + public final fun Config (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config; + public static synthetic fun Config$default (Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Companion;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config$Companion; + public abstract fun getInterceptors ()Ljava/util/List; + public abstract fun toBuilder ()Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config$Builder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config$Builder { + public abstract fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config; + public abstract fun getInterceptors ()Ljava/util/List; + public abstract fun setInterceptors (Ljava/util/List;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config$Companion { + public final fun Builder ()Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper$Config$Builder; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapperKt { + public static final fun DynamoDbMapper (Laws/sdk/kotlin/services/dynamodb/DynamoDbClient;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper; + public static synthetic fun DynamoDbMapper$default (Laws/sdk/kotlin/services/dynamodb/DynamoDbClient;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper; +} + +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/annotations/ManualPagination : java/lang/annotation/Annotation { + public abstract fun paginatedEquivalent ()Ljava/lang/String; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getOperands ()Ljava/util/List; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExprKt { + public static final fun AndExpr (Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement$Index : aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement { + public abstract fun getIndex ()I +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement$Name : aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement { + public abstract fun getName ()Ljava/lang/String; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath : aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getElement ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttrPathElement; + public abstract fun getParent ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePathKt { + public static final fun AttributePath (ILaws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; + public static final fun AttributePath (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; + public static synthetic fun AttributePath$default (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType : java/lang/Enum { + public static final field Binary Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field BinarySet Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field Boolean Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field List Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field Map Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field Null Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field Number Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field NumberSet Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field String Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static final field StringSet Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public final fun getAbbreviation ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; + public static fun values ()[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr, aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getMax ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; + public abstract fun getMin ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; + public abstract fun getValue ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExprKt { + public static final fun BetweenExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc : java/lang/Enum { + public static final field ATTRIBUTE_EXISTS Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static final field ATTRIBUTE_NOT_EXISTS Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static final field ATTRIBUTE_TYPE Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static final field BEGINS_WITH Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static final field CONTAINS Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getExprString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public static fun values ()[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr, aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getAdditionalOperands ()Ljava/util/List; + public abstract fun getFunc ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc; + public abstract fun getPath ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExprKt { + public static final fun BooleanFuncExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr; + public static final fun BooleanFuncExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr; + public static synthetic fun BooleanFuncExpr$default (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/List;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator : java/lang/Enum { + public static final field EQUALS Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static final field GREATER_THAN Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static final field GREATER_THAN_OR_EQUAL Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static final field LESS_THAN Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static final field LESS_THAN_OR_EQUAL Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static final field NOT_EQUALS Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getExprString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public static fun values ()[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr, aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getComparator ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator; + public abstract fun getLeft ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; + public abstract fun getRight ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExprKt { + public static final fun ComparisonExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor { + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr;)Ljava/lang/Object; + public abstract fun visit (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr;)Ljava/lang/Object; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter { + public abstract fun and (Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun and ([Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun attr (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; + public abstract fun contains (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun contains (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Boolean;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Void;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq-FrkygD8 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UInt;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq-X7ZSXPM (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UShort;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq-Zf_Lc9A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/ULong;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eq-tA8902A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UByte;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun eqSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun exists (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun get (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; + public abstract fun get (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; + public abstract fun getSize (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun gte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[B[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionExpression (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionList (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionMap (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInCollectionUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isInRangeUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun isOfType (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun lte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Boolean;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Void;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq-FrkygD8 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UInt;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq-X7ZSXPM (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UShort;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq-Zf_Lc9A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/ULong;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neq-tA8902A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UByte;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun neqSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun not (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun notExists (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun or (Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun or ([Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter$DefaultImpls { + public static fun and (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun contains (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Boolean;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Void;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq-FrkygD8 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UInt;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq-X7ZSXPM (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UShort;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq-Zf_Lc9A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/ULong;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eq-tA8902A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UByte;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun eqSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun gte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[B[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionList (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionMap (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInCollectionUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun isInRangeUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun lte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Boolean;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/lang/Void;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq-FrkygD8 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UInt;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq-X7ZSXPM (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UShort;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq-Zf_Lc9A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/ULong;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neq-tA8902A (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Lkotlin/UByte;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetByteArray (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun neqSetUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun or (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public static fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getSet ()Ljava/util/Collection; + public abstract fun getValue ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExprKt { + public static final fun InExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;Ljava/util/Collection;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter { + public abstract fun getPartitionKey ()Ljava/lang/Object; + public abstract fun getSortKey ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilterKt { + public static final fun KeyFilter (Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter; + public static final fun KeyFilter (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getValue ()Laws/sdk/kotlin/services/dynamodb/model/AttributeValue; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExprKt { + public static final fun LiteralExpr (Laws/sdk/kotlin/services/dynamodb/model/AttributeValue;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/lang/Boolean;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/lang/Void;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr (Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr ([B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr-3swpYEE (Lkotlin/UByte;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr-ADd3fzo (Lkotlin/ULong;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr-ExVfyTY (Lkotlin/UInt;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExpr-ffyZV3s (Lkotlin/UShort;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetByteArray (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetNumber (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetString (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetUByte (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetUInt (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetULong (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; + public static final fun LiteralExprSetUShort (Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getOperand ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExprKt { + public static final fun NotExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getOperands ()Ljava/util/List; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExprKt { + public static final fun OrExpr (Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc : java/lang/Enum { + public static final field SIZE Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getExprString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc; + public static fun values ()[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr { + public abstract fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; + public abstract fun getAdditionalOperands ()Ljava/util/List; + public abstract fun getFunc ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc; + public abstract fun getPath ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr$DefaultImpls { + public static fun accept (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExprKt { + public static final fun ScalarFuncExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/List;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr; + public static final fun ScalarFuncExpr (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;[Laws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr; + public static synthetic fun ScalarFuncExpr$default (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath;Ljava/util/List;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr : aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expression { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter { + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun eq-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun getSortKey ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun gte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun isInRangeUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun lte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun neq-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public abstract fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter$DefaultImpls { + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun eq-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun gte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isBetween (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeNumber (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeString (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeUByte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeUInt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeULong (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun isInRangeUShort (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Lkotlin/ranges/ClosedRange;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lt-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun lte-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/Number;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq-2TYgG_w (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;J)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq-EK-6454 (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq-Qn1smSk (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;I)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun neq-i8woANY (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;S)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; + public static fun startsWith (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter;Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKey;[B)Laws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyExpr; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor { + public abstract fun getConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public abstract fun getGetter ()Lkotlin/jvm/functions/Function1; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getSetter ()Lkotlin/jvm/functions/Function2; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptorKt { + public static final fun AttributeDescriptor (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/DocumentConverter : aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/items/DocumentConverter; + public fun convertFrom (Laws/sdk/kotlin/hll/dynamodbmapper/model/Item;)Laws/smithy/kotlin/runtime/content/Document; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Laws/smithy/kotlin/runtime/content/Document;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public fun convertTo (Laws/smithy/kotlin/runtime/content/Document;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public synthetic fun convertTo (Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun convertTo (Ljava/lang/Object;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/HeterogeneousItemConverter : aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/util/Map;)V + public fun convertFrom (Laws/sdk/kotlin/hll/dynamodbmapper/model/Item;)Ljava/lang/Object; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Object;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public final fun getSubConverters ()Ljava/util/Map; + public final fun getTypeAttribute ()Ljava/lang/String; + public final fun getTypeMapper ()Lkotlin/jvm/functions/Function1; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter : aws/sdk/kotlin/hll/mapping/core/converters/Converter { + public abstract fun convertTo (Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public abstract fun convertTo (Ljava/lang/Object;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter$DefaultImpls { + public static fun convertTo (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public static synthetic fun convertTo$default (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Ljava/lang/Object;Ljava/util/Set;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema { + public abstract fun getConverter ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter; + public abstract fun getKeyAttributeNames ()Ljava/util/Set; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey { + public abstract fun getKeyAttributeNames ()Ljava/util/Set; + public abstract fun getSortKey ()Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey$DefaultImpls { + public static fun getKeyAttributeNames (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey;)Ljava/util/Set; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema { + public abstract fun getKeyAttributeNames ()Ljava/util/Set; + public abstract fun getPartitionKey ()Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey$DefaultImpls { + public static fun getKeyAttributeNames (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey;)Ljava/util/Set; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchemaKt { + public static final fun ItemSchema (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;)Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey; + public static final fun ItemSchema (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;)Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey; + public static final fun withKeySpec (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;)Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey; + public static final fun withKeySpec (Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec;)Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$Companion; + public abstract fun getName ()Ljava/lang/String; + public abstract fun toField (Ljava/lang/Object;)Lkotlin/Pair; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$ByteArray : aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$Companion { + public final fun ByteArray (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$ByteArray; + public final fun Number (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$Number; + public final fun String (Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$String; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$Number : aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec$String : aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverter : aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter { + public fun (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;[Laws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor;)V + public fun convertFrom (Laws/sdk/kotlin/hll/dynamodbmapper/model/Item;)Ljava/lang/Object; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Object;Ljava/util/Set;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public final fun getDescriptors ()Ljava/util/Map; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Index : aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec, aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource, aws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Index$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/Index, aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$CompositeKey { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Index$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/Index, aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$PartitionKey { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec : aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec { + public abstract fun getIndexName ()Ljava/lang/String; + public abstract fun getTableName ()Ljava/lang/String; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Item : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/model/ItemKt { + public static final fun buildItem (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public static final fun itemOf ([Lkotlin/Pair;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public static final fun toItem (Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public static final fun toMutableItem (Laws/sdk/kotlin/hll/dynamodbmapper/model/Item;)Laws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource : aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec, aws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource, aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$CompositeKey { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource, aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$PartitionKey { +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/model/MutableItemKt { + public static final fun mutableItemOf ([Lkotlin/Pair;)Laws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem; + public static final fun toItem (Laws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public static final fun toMutableItem (Ljava/util/Map;)Laws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec { + public abstract fun getMapper ()Laws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper; + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Table : aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource, aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec, aws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations { + public abstract fun getIndex (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Index$CompositeKey; + public abstract fun getIndex (Ljava/lang/String;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Index$PartitionKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Table$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource$CompositeKey, aws/sdk/kotlin/hll/dynamodbmapper/model/Table { + public abstract fun getItem (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/Table$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource$PartitionKey, aws/sdk/kotlin/hll/dynamodbmapper/model/Table { + public abstract fun getItem (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec : aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec { + public abstract fun getTableName ()Ljava/lang/String; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec$CompositeKey : aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$CompositeKey; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec$PartitionKey : aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec { + public abstract fun getSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema$PartitionKey; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemKt { + public static final fun DeleteItemRequest (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest; + public static final fun DeleteItemResponse (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequestBuilder; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponseBuilder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest$Companion; + public abstract fun getKey ()Ljava/lang/Object; + public abstract fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public abstract fun getReturnItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics; + public abstract fun getReturnValues ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValue; + public abstract fun getReturnValuesOnConditionCheckFailure ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequestBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest; + public final fun getKey ()Ljava/lang/Object; + public final fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public final fun getReturnItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics; + public final fun getReturnValues ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValue; + public final fun getReturnValuesOnConditionCheckFailure ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure; + public final fun setKey (Ljava/lang/Object;)V + public final fun setReturnConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity;)V + public final fun setReturnItemCollectionMetrics (Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics;)V + public final fun setReturnValues (Laws/sdk/kotlin/services/dynamodb/model/ReturnValue;)V + public final fun setReturnValuesOnConditionCheckFailure (Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse$Companion; + public abstract fun getAttributes ()Ljava/lang/Object; + public abstract fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public abstract fun getItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponseBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemResponse; + public final fun getAttributes ()Ljava/lang/Object; + public final fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public final fun getItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics; + public final fun setAttributes (Ljava/lang/Object;)V + public final fun setConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity;)V + public final fun setItemCollectionMetrics (Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemKt { + public static final fun GetItemRequest (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest; + public static final fun GetItemResponse (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequestBuilder; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponseBuilder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest$Companion; + public abstract fun getConsistentRead ()Ljava/lang/Boolean; + public abstract fun getKey ()Ljava/lang/Object; + public abstract fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequestBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest; + public final fun getConsistentRead ()Ljava/lang/Boolean; + public final fun getKey ()Ljava/lang/Object; + public final fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public final fun setConsistentRead (Ljava/lang/Boolean;)V + public final fun setKey (Ljava/lang/Object;)V + public final fun setReturnConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse$Companion; + public abstract fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public abstract fun getItem ()Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponseBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemResponse; + public final fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public final fun getItem ()Ljava/lang/Object; + public final fun setConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity;)V + public final fun setItem (Ljava/lang/Object;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations : aws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations { + public abstract fun query (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun scan (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperationsKt { + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/IndexOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations { + public abstract fun query (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun scan (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperationsKt { + public static final fun query (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun queryItems (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun scan (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun scanItems (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemKt { + public static final fun PutItemRequest (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest; + public static final fun PutItemResponse (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequestBuilder; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponseBuilder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest$Companion; + public abstract fun getItem ()Ljava/lang/Object; + public abstract fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public abstract fun getReturnItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics; + public abstract fun getReturnValues ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValue; + public abstract fun getReturnValuesOnConditionCheckFailure ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequestBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest; + public final fun getItem ()Ljava/lang/Object; + public final fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public final fun getReturnItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics; + public final fun getReturnValues ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValue; + public final fun getReturnValuesOnConditionCheckFailure ()Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure; + public final fun setItem (Ljava/lang/Object;)V + public final fun setReturnConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity;)V + public final fun setReturnItemCollectionMetrics (Laws/sdk/kotlin/services/dynamodb/model/ReturnItemCollectionMetrics;)V + public final fun setReturnValues (Laws/sdk/kotlin/services/dynamodb/model/ReturnValue;)V + public final fun setReturnValuesOnConditionCheckFailure (Laws/sdk/kotlin/services/dynamodb/model/ReturnValuesOnConditionCheckFailure;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse$Companion; + public abstract fun getAttributes ()Ljava/lang/Object; + public abstract fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public abstract fun getItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponseBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemResponse; + public final fun getAttributes ()Ljava/lang/Object; + public final fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public final fun getItemCollectionMetrics ()Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics; + public final fun setAttributes (Ljava/lang/Object;)V + public final fun setConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity;)V + public final fun setItemCollectionMetrics (Laws/sdk/kotlin/services/dynamodb/model/ItemCollectionMetrics;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryKt { + public static final fun QueryRequest (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest; + public static final fun QueryResponse (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequestBuilder; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponseBuilder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest$Companion; + public abstract fun getConsistentRead ()Ljava/lang/Boolean; + public abstract fun getExclusiveStartKey ()Ljava/lang/Object; + public abstract fun getFilter ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun getKeyCondition ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter; + public abstract fun getLimit ()Ljava/lang/Integer; + public abstract fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public abstract fun getScanIndexForward ()Ljava/lang/Boolean; + public abstract fun getSelect ()Laws/sdk/kotlin/services/dynamodb/model/Select; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequestBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest; + public final fun filter (Lkotlin/jvm/functions/Function1;)V + public final fun getConsistentRead ()Ljava/lang/Boolean; + public final fun getExclusiveStartKey ()Ljava/lang/Object; + public final fun getFilter ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public final fun getKeyCondition ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter; + public final fun getLimit ()Ljava/lang/Integer; + public final fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public final fun getScanIndexForward ()Ljava/lang/Boolean; + public final fun getSelect ()Laws/sdk/kotlin/services/dynamodb/model/Select; + public final fun setConsistentRead (Ljava/lang/Boolean;)V + public final fun setExclusiveStartKey (Ljava/lang/Object;)V + public final fun setFilter (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)V + public final fun setKeyCondition (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter;)V + public final fun setLimit (Ljava/lang/Integer;)V + public final fun setReturnConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity;)V + public final fun setScanIndexForward (Ljava/lang/Boolean;)V + public final fun setSelect (Laws/sdk/kotlin/services/dynamodb/model/Select;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse$Companion; + public abstract fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public abstract fun getCount ()I + public abstract fun getItems ()Ljava/util/List; + public abstract fun getLastEvaluatedKey ()Ljava/lang/Object; + public abstract fun getScannedCount ()I +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponseBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryResponse; + public final fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public final fun getCount ()Ljava/lang/Integer; + public final fun getItems ()Ljava/util/List; + public final fun getLastEvaluatedKey ()Ljava/lang/Object; + public final fun getScannedCount ()Ljava/lang/Integer; + public final fun setConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity;)V + public final fun setCount (Ljava/lang/Integer;)V + public final fun setItems (Ljava/util/List;)V + public final fun setLastEvaluatedKey (Ljava/lang/Object;)V + public final fun setScannedCount (Ljava/lang/Integer;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanKt { + public static final fun ScanRequest (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest; + public static final fun ScanResponse (Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest; + public static final fun copy (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequestBuilder; + public static final fun toBuilder (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse;)Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponseBuilder; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest$Companion; + public abstract fun getConsistentRead ()Ljava/lang/Boolean; + public abstract fun getExclusiveStartKey ()Ljava/lang/Object; + public abstract fun getFilter ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public abstract fun getLimit ()Ljava/lang/Integer; + public abstract fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public abstract fun getSegment ()Ljava/lang/Integer; + public abstract fun getSelect ()Laws/sdk/kotlin/services/dynamodb/model/Select; + public abstract fun getTotalSegments ()Ljava/lang/Integer; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequestBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest; + public final fun filter (Lkotlin/jvm/functions/Function1;)V + public final fun getConsistentRead ()Ljava/lang/Boolean; + public final fun getExclusiveStartKey ()Ljava/lang/Object; + public final fun getFilter ()Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr; + public final fun getLimit ()Ljava/lang/Integer; + public final fun getReturnConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity; + public final fun getSegment ()Ljava/lang/Integer; + public final fun getSelect ()Laws/sdk/kotlin/services/dynamodb/model/Select; + public final fun getTotalSegments ()Ljava/lang/Integer; + public final fun setConsistentRead (Ljava/lang/Boolean;)V + public final fun setExclusiveStartKey (Ljava/lang/Object;)V + public final fun setFilter (Laws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanExpr;)V + public final fun setLimit (Ljava/lang/Integer;)V + public final fun setReturnConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ReturnConsumedCapacity;)V + public final fun setSegment (Ljava/lang/Integer;)V + public final fun setSelect (Laws/sdk/kotlin/services/dynamodb/model/Select;)V + public final fun setTotalSegments (Ljava/lang/Integer;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse$Companion; + public abstract fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public abstract fun getCount ()I + public abstract fun getItems ()Ljava/util/List; + public abstract fun getLastEvaluatedKey ()Ljava/lang/Object; + public abstract fun getScannedCount ()I +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse$Companion { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponseBuilder { + public fun ()V + public final fun build ()Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanResponse; + public final fun getConsumedCapacity ()Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity; + public final fun getCount ()Ljava/lang/Integer; + public final fun getItems ()Ljava/util/List; + public final fun getLastEvaluatedKey ()Ljava/lang/Object; + public final fun getScannedCount ()Ljava/lang/Integer; + public final fun setConsumedCapacity (Laws/sdk/kotlin/services/dynamodb/model/ConsumedCapacity;)V + public final fun setCount (Ljava/lang/Integer;)V + public final fun setItems (Ljava/util/List;)V + public final fun setLastEvaluatedKey (Ljava/lang/Object;)V + public final fun setScannedCount (Ljava/lang/Integer;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations : aws/sdk/kotlin/hll/dynamodbmapper/operations/ItemSourceOperations { + public abstract fun deleteItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun putItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun query (Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun scan (Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperationsKt { + public static final fun deleteItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun putItem (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/QueryRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun queryPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Laws/sdk/kotlin/hll/dynamodbmapper/operations/ScanRequest;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanPaginated (Laws/sdk/kotlin/hll/dynamodbmapper/operations/TableOperations;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput { + public abstract fun getDeserializeSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema; + public abstract fun getLowLevelResponse ()Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInputKt { + public static final fun DeserializeInput (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext : aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput { + public abstract fun getError ()Ljava/lang/Throwable; + public abstract fun getMapperContext ()Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContextKt { + public static final fun HReqContext (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Throwable;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext; + public static synthetic fun HReqContext$default (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Throwable;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext : aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext { + public abstract fun getHighLevelResponse ()Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContextKt { + public static final fun HResContext (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Ljava/lang/Object;Ljava/lang/Throwable;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext; + public static synthetic fun HResContext$default (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor { + public abstract fun modifyBeforeCompletion (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)Ljava/lang/Object; + public abstract fun modifyBeforeDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput; + public abstract fun modifyBeforeInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)Ljava/lang/Object; + public abstract fun modifyBeforeSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput; + public abstract fun readAfterDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)V + public abstract fun readAfterInitialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)V + public abstract fun readAfterInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)V + public abstract fun readAfterSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)V + public abstract fun readBeforeCompletion (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)V + public abstract fun readBeforeDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)V + public abstract fun readBeforeInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)V + public abstract fun readBeforeSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)V +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor$DefaultImpls { + public static fun modifyBeforeCompletion (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)Ljava/lang/Object; + public static fun modifyBeforeDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput; + public static fun modifyBeforeInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)Ljava/lang/Object; + public static fun modifyBeforeSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput; + public static fun readAfterDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)V + public static fun readAfterInitialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)V + public static fun readAfterInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)V + public static fun readAfterSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)V + public static fun readBeforeCompletion (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext;)V + public static fun readBeforeDeserialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext;)V + public static fun readBeforeInvocation (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext;)V + public static fun readBeforeSerialization (Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext;)V +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext : aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext { + public abstract fun getLowLevelRequest ()Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContextKt { + public static final fun LReqContext (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Throwable;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext; + public static synthetic fun LReqContext$default (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext : aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput, aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext { +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContextKt { + public static final fun LResContext (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Ljava/lang/Throwable;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext; + public static synthetic fun LResContext$default (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext;Ljava/lang/Object;Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;Ljava/lang/Throwable;ILjava/lang/Object;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext { + public abstract fun getOperation ()Ljava/lang/String; + public abstract fun getPersistenceSpec ()Laws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContextKt { + public static final fun MapperContext (Laws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec;Ljava/lang/String;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext; +} + +public abstract interface class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput { + public abstract fun getHighLevelRequest ()Ljava/lang/Object; + public abstract fun getSerializeSchema ()Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInputKt { + public static final fun SerializeInput (Ljava/lang/Object;Laws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema;)Laws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/ItemToValueConverter : aws/sdk/kotlin/hll/mapping/core/converters/Converter { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/values/ItemToValueConverter; + public fun convertFrom (Laws/sdk/kotlin/services/dynamodb/model/AttributeValue;)Laws/sdk/kotlin/hll/dynamodbmapper/model/Item; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Laws/sdk/kotlin/hll/dynamodbmapper/model/Item;)Laws/sdk/kotlin/services/dynamodb/model/AttributeValue; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter : aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter { + public fun (Lkotlin/reflect/KClass;)V + public fun convertFrom (Laws/sdk/kotlin/services/dynamodb/model/AttributeValue;)Laws/sdk/kotlin/hll/mapping/core/util/Either; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Object;)Laws/sdk/kotlin/hll/mapping/core/util/Either; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverterKt { + public static final fun ListConverter (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getListConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverterKt { + public static final fun MapConverter (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun MapConverter (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getMapConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConverters; + public final fun getStringListToAttributeValueNumberSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getStringSetToAttributeValueNumberSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConvertersKt { + public static final fun getByteSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getDoubleSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getFloatSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getIntSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getLongSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getShortSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUByteSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUIntSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getULongSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUShortSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/collections/PrimitiveSetConvertersKt { + public static final fun getByteArraySetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getCharArraySetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getCharSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getStringListToAttributeValueStringSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getStringSetConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/BooleanConverterKt { + public static final fun getBooleanConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getBooleanToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ByteArrayConverterKt { + public static final fun getByteArrayConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/EnumConverter : aws/sdk/kotlin/hll/mapping/core/converters/Converter { + public fun (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)V + public fun convertFrom (Laws/sdk/kotlin/services/dynamodb/model/AttributeValue;)Ljava/lang/Enum; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Ljava/lang/Enum;)Laws/sdk/kotlin/services/dynamodb/model/AttributeValue; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConverters; + public final fun getAutoNumberToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getByteToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getDoubleToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getFloatToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getIntToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getLongToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getShortToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getStringToAttributeValueNumberConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getUByteToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getUIntToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getULongToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getUShortToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConvertersKt { + public static final fun getAutoNumberConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getByteConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getDoubleConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getFloatConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getIntConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getLongConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getShortConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUByteConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUIntConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getULongConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUShortConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConverters; + public final fun getCharArrayToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getCharToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getStringToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConvertersKt { + public static final fun getCharArrayConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getCharConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter : aws/sdk/kotlin/hll/mapping/core/converters/Converter { + public static final field Companion Laws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter$Companion; + public fun ()V + public fun (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)V + public synthetic fun (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun convertFrom (Laws/sdk/kotlin/services/dynamodb/model/AttributeValue;)Laws/smithy/kotlin/runtime/content/Document; + public synthetic fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; + public fun convertTo (Laws/smithy/kotlin/runtime/content/Document;)Laws/sdk/kotlin/services/dynamodb/model/AttributeValue; + public synthetic fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter$Companion { + public final fun getDefault ()Laws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConverter { + public static final field INSTANCE Laws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConverter; + public final fun getDefault ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getEpochMs ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getEpochS ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun getIso8601 ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverterKt { + public static final fun getUrlConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun getUrlToStringConverter ()Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + diff --git a/hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts b/hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts new file mode 100644 index 00000000000..df60c718184 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts @@ -0,0 +1,161 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import aws.sdk.kotlin.gradle.kmp.NATIVE_ENABLED +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer +import com.google.devtools.ksp.gradle.KspTaskJvm +import com.google.devtools.ksp.gradle.KspTaskMetadata +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +description = "High level DynamoDbMapper client" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper" +extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper" + +buildscript { + dependencies { + classpath(libs.ddb.local) + } +} + +plugins { + alias(libs.plugins.ksp) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":aws-runtime:aws-http")) + api(project(":services:dynamodb")) + api(project(":hll:hll-mapping-core")) + api(libs.kotlinx.coroutines.core) + } + } + + commonTest { + dependencies { + implementation(libs.kotlinx.coroutines.test) + } + } + + jvmTest { + dependencies { + implementation(libs.mockk) + implementation(libs.kotest.assertions.core) + implementation(libs.kotest.runner.junit5) + } + } + } +} + +ksp { + arg("pkg", "aws.sdk.kotlin.hll.dynamodbmapper.operations") + + val allowlist = listOf( + "deleteItem", + "getItem", + "putItem", + "query", + "scan", + ) + arg("op-allowlist", allowlist.joinToString(";")) +} + +if (project.NATIVE_ENABLED) { + // Configure KSP for multiplatform: https://kotlinlang.org/docs/ksp-multiplatform.html + // https://github.com/google/ksp/issues/963#issuecomment-1894144639 + // https://github.com/google/ksp/issues/965 + dependencies.kspCommonMainMetadata(project(":hll:dynamodb-mapper:dynamodb-mapper-ops-codegen")) + + kotlin.sourceSets.commonMain { + tasks.withType { + // Wire up the generated source to the commonMain source set + kotlin.srcDir(destinationDirectory) + } + } +} else { + // FIXME This is a dirty hack for JVM-only builds which KSP doesn't consider to be "multiplatform". Explanation of + // hack follows in narrative, minimally-opinionated comments. + + // Start by invoking the JVM-only KSP configuration + dependencies.kspJvm(project(":hll:dynamodb-mapper:dynamodb-mapper-ops-codegen")) + + // Then we need to move the generated source from jvm to common + val moveGenSrc by tasks.registering { + // Can't move src until the src is generated + dependsOn(tasks.named("kspKotlinJvm")) + + // Detecting these paths programmatically is complex; just hardcode them + val srcDir = file("build/generated/ksp/jvm/jvmMain") + val destDir = file("build/generated/ksp/common/commonMain") + + inputs.dir(srcDir) + outputs.dirs(srcDir, destDir) + + doLast { + if (destDir.exists()) { + // Clean out the existing destination, otherwise move fails + require(destDir.deleteRecursively()) { "Failed to delete $destDir before moving from $srcDir" } + } else { + // Create the destination directories, otherwise move fails + require(destDir.mkdirs()) { "Failed to create path $destDir" } + } + + Files.move(srcDir.toPath(), destDir.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + + tasks.named("jvmSourcesJar") { + dependsOn(moveGenSrc) + } + + tasks.withType> { + if (this !is KspTaskJvm) { + // Ensure that any **non-KSP** compile tasks depend on the generated src move + dependsOn(moveGenSrc) + } + } + + // Finally, wire up the generated source to the commonMain source set + kotlin.sourceSets.commonMain { + kotlin.srcDir("build/generated/ksp/common/commonMain/kotlin") + } +} + +open class DynamoDbLocalInstance : DefaultTask() { + private val port = 44212 // Keep in sync with DdbLocalTest.kt + + @Internal + var runner: DynamoDBProxyServer? = null + private set + + @TaskAction + fun exec() { + println("Running DynamoDB local instance on port $port") + runner = ServerRunner + .createServerFromCommandLineArgs(arrayOf("-inMemory", "-port", port.toString(), "-disableTelemetry")) + .also { it.start() } + } + + fun stop() { + runner?.let { + println("Stopping DynamoDB local instance on port $port") + it.stop() + } + } +} + +val startDdbLocal = task("startDdbLocal") + +tasks.withType { + dependsOn(startDdbLocal) +} + +gradle.buildFinished { + startDdbLocal.stop() +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper.kt new file mode 100644 index 00000000000..2a7f590862b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper.kt @@ -0,0 +1,119 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper + +import aws.sdk.kotlin.hll.dynamodbmapper.internal.DynamoDbMapperImpl +import aws.sdk.kotlin.hll.dynamodbmapper.internal.MapperConfigBuilderImpl +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.Table +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.Interceptor +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.InterceptorAny +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * A high-level client for DynamoDB which maps custom data types into DynamoDB attributes and vice versa. + */ +@ExperimentalApi +public interface DynamoDbMapper { + @ExperimentalApi + public companion object { + /** + * Instantiate a new [Config] object + * @param config A DSL block for setting properties of the config + */ + public fun Config(config: Config.Builder.() -> Unit = { }): Config = + Config.Builder().apply(config).build() + } + + /** + * The low-level DynamoDB client used for underlying calls to the service + */ + public val client: DynamoDbClient + + /** + * The active configuration for this mapper + */ + public val config: Config + + /** + * Get a [Table] reference for performing table operations + * @param T The type of objects which will be read from and/or written to this table + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param name The name of the table + * @param schema The [ItemSchema] which describes the table, its keys, and how items are converted + */ + public fun getTable( + name: String, + schema: ItemSchema.PartitionKey, + ): Table.PartitionKey + + /** + * Get a [Table] reference for performing table operations + * @param T The type of objects which will be read from and/or written to this table + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + * @param name The name of the table + * @param schema The [ItemSchema] which describes the table, its keys, and how items are converted + */ + public fun getTable( + name: String, + schema: ItemSchema.CompositeKey, + ): Table.CompositeKey + + // TODO add multi-table operations like batchGetItem, transactWriteItems, etc. + + /** + * The immutable configuration for a [DynamoDbMapper] instance + */ + public interface Config { + public companion object { + /** + * Instantiate a new [Builder] object + */ + public fun Builder(): Builder = MapperConfigBuilderImpl() + } + + /** + * A list of [Interceptor] instances which will be applied to operations as they move through the request + * pipeline. + */ + public val interceptors: List + + /** + * Convert this immutable configuration into a mutable [Builder] object. Updates made to the mutable builder + * properties will not affect this instance. + */ + public fun toBuilder(): Builder + + /** + * A mutable configuration builder for a [DynamoDbMapper] instance + */ + public interface Builder { + /** + * A list of [Interceptor] instances which will be applied to operations as they move through the request + * pipeline. + */ + public var interceptors: MutableList + + /** + * Builds this mutable [Builder] object into an immutable [Config] object. Changes made to this instance do + * not affect the built instance. + */ + public fun build(): Config + } + } +} + +/** + * Instantiate a new [DynamoDbMapper] + * @param client The low-level DynamoDB client to use for underlying calls to the service + * @param config A DSL configuration block + */ +@ExperimentalApi +public fun DynamoDbMapper( + client: DynamoDbClient, + config: DynamoDbMapper.Config.Builder.() -> Unit = { }, +): DynamoDbMapper = DynamoDbMapperImpl(client, DynamoDbMapper.Config(config)) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/annotations/ManualPagination.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/annotations/ManualPagination.kt new file mode 100644 index 00000000000..3f0583a1dd8 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/annotations/ManualPagination.kt @@ -0,0 +1,15 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.annotations + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Indicates that an operation invocation method (e.g., `query`) does not provide automatic pagination. While desirable + * in advanced scenarios, the typical use case should favor the paginated equivalent (e.g., `queryPaginated`). + */ +@ExperimentalApi +@RequiresOptIn(message = "This method does not provide automatic pagination over results and should only be used in advanced scenarios. Where possible, consider using the paginated equivalent. To explicitly opt into using this method, annotate the call site with `@OptIn(ManualPagination::class)`.") +public annotation class ManualPagination(val paginatedEquivalent: String) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr.kt new file mode 100644 index 00000000000..f19a5bdef39 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AndExpr.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.AndExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents an `AND` expression as described in + * [DynamoDB's **logical evaluations** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.LogicalEvaluations). + * This expression will be true if `(operand[0] && operand[1] && ... && operand[n - 1])`. + */ +@ExperimentalApi +public interface AndExpr : BooleanExpr { + /** + * A list of 2 or more [BooleanExpr] conditions which are ANDed together + */ + public val operands: List + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new [AndExpr] with the given [operands] + * @param operands A list of 2 or more [BooleanExpr] conditions which are ANDed together + */ +@ExperimentalApi +public fun AndExpr(operands: List): AndExpr = AndExprImpl(operands) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath.kt new file mode 100644 index 00000000000..7c6c3cff2d5 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributePath.kt @@ -0,0 +1,82 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.AttrPathIndexImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.AttrPathNameImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.AttributePathImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents an element in an [AttributePath] + */ +@ExperimentalApi +public sealed interface AttrPathElement { + /** + * Represents the name of a top-level attribute or a key in a map + */ + @ExperimentalApi + public interface Name : AttrPathElement { + /** + * The name or key of this element + */ + public val name: String + } + + /** + * Represents an index into a list/set + */ + @ExperimentalApi + public interface Index : AttrPathElement { + /** + * The index (starting at `0`) + */ + public val index: Int + } +} + +/** + * Represents an expression that consists of an attribute. Attributes are referenced by attribute paths, analogous to + * [document paths in DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html#Expressions.Attributes.NestedElements.DocumentPathExamples). + * Attribute paths consist of one or more elements, which are either names (e.g., of a top-level attribute or a nested + * key in a map attribute) or indices (i.e., into a list). The first (and often only) element of an attribute path is a + * name. + * + * See [Filter] for more information about creating references to attributes. + */ +@ExperimentalApi +public interface AttributePath : Expression { + /** + * The [AttrPathElement] for this path + */ + public val element: AttrPathElement + + /** + * The parent [AttributePath] (if any). If [parent] is `null` then this instance represents a top-level attribute + * and [element] must be a name (not an index). + */ + public val parent: AttributePath? + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new [AttributePath] reference with the given name and optional parent path + * @param name The name or key of this element + * @param parent The parent [AttributePath] (if any) of this element. If [parent] is `null` then this instance + * represents a top-level attribute. + */ +@ExperimentalApi +public fun AttributePath(name: String, parent: AttributePath? = null): AttributePath = + AttributePathImpl(AttrPathNameImpl(name), parent) + +/** + * Creates a new [AttributePath] reference with the given index and parent path + * @param index The index (starting at `0`) of this element + * @param parent The parent [AttributePath] of this element + */ +@ExperimentalApi +public fun AttributePath(index: Int, parent: AttributePath): AttributePath = + AttributePathImpl(AttrPathIndexImpl(index), parent) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType.kt new file mode 100644 index 00000000000..b183cc71b05 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/AttributeType.kt @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a + * [DynamoDB attribute data type](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + * @param abbreviation The DynamoDB type name + */ +@ExperimentalApi +public enum class AttributeType(public val abbreviation: kotlin.String) { + /** + * Binary data type, denoted in DynamoDB as `B` + */ + Binary("B"), + + /** + * Binary set data type, denoted in DynamoDB as `BS` + */ + BinarySet("BS"), + + /** + * Boolean data type, denoted in DynamoDB as `BOOL` + */ + Boolean("BOOL"), + + /** + * List data type, denoted in DynamoDB as `L` + */ + List("L"), + + /** + * Map data type, denoted in DynamoDB as `M` + */ + Map("M"), + + /** + * Null data type, denoted in DynamoDB as `NULL` + */ + Null("NULL"), + + /** + * Number data type, denoted in DynamoDB as `N` + */ + Number("N"), + + /** + * Number set data type, denoted in DynamoDB as `NS` + */ + NumberSet("NS"), + + /** + * String data type, denoted in DynamoDB as `S` + */ + String("S"), + + /** + * String set data type, denoted in DynamoDB as `SS` + */ + StringSet("SS"), +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr.kt new file mode 100644 index 00000000000..2c7f3554b2f --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BetweenExpr.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.BetweenExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a `BETWEEN` expression as described in + * [DynamoDB's **making comparisons** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Comparators). + * This expression will be true if `value >= min && value <= max`. + */ +@ExperimentalApi +public interface BetweenExpr : + BooleanExpr, + SortKeyExpr { + /** + * The value being compared to the [min] and [max] + */ + public val value: Expression + + /** + * The minimum bound for the comparison + */ + public val min: Expression + + /** + * The maximum bound for the comparison + */ + public val max: Expression + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new [BetweenExpr] for the given [value] and range bounded by [min] and [max] + * @param value The value being compared to the [min] and [max] + * @param min The minimum bound for the comparison + * @param max The maximum bound for the comparison + */ +@ExperimentalApi +public fun BetweenExpr(value: Expression, min: Expression, max: Expression): BetweenExpr = + BetweenExprImpl(value, min, max) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc.kt new file mode 100644 index 00000000000..306384770b9 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFunc.kt @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Identifies a + * [DynamoDB expression function](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions) + * which returns a boolean value + * @param exprString The literal name of the function to use in expression strings + */ +@ExperimentalApi +public enum class BooleanFunc(public val exprString: String) { + /** + * The `attribute_exist` function + */ + ATTRIBUTE_EXISTS("attribute_exists"), + + /** + * The `attribute_not_exists` function + */ + ATTRIBUTE_NOT_EXISTS("attribute_not_exists"), + + /** + * The `attribute_type` function + */ + ATTRIBUTE_TYPE("attribute_type"), + + /** + * The `begins_with` function + */ + BEGINS_WITH("begins_with"), + + /** + * The `contains` function + */ + CONTAINS("contains"), +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr.kt new file mode 100644 index 00000000000..3c02bc1f294 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/BooleanFuncExpr.kt @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.BooleanFuncExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a function expression that yields a boolean result as described in + * [DynamoDB's **function** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions) + */ +@ExperimentalApi +public interface BooleanFuncExpr : + BooleanExpr, + SortKeyExpr { + /** + * The specific boolean function to use + */ + public val func: BooleanFunc + + /** + * The attribute path to pass as the function's first argument + */ + public val path: AttributePath + + /** + * Any additional arguments used by the function + */ + public val additionalOperands: List + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new boolean function expression + * @param func The specific boolean function to use + * @param path The attribute path to pass as the function's first argument + * @param additionalOperands Any additional arguments used by the function + */ +@ExperimentalApi +public fun BooleanFuncExpr( + func: BooleanFunc, + path: AttributePath, + additionalOperands: List = listOf(), +): BooleanFuncExpr = BooleanFuncExprImpl(func, path, additionalOperands) + +/** + * Creates a new boolean function expression + * @param func The specific boolean function to use + * @param path The attribute path to pass as the function's first argument + * @param additionalOperands Any additional arguments used by the function + */ +@ExperimentalApi +public fun BooleanFuncExpr( + func: BooleanFunc, + path: AttributePath, + vararg additionalOperands: Expression, +): BooleanFuncExpr = BooleanFuncExprImpl(func, path, additionalOperands.toList()) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator.kt new file mode 100644 index 00000000000..d6af9ab2426 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Comparator.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Identifies a comparison operator to use in an expression + * @param exprString The literal value of the operator to use in an expression string + */ +@ExperimentalApi +public enum class Comparator(public val exprString: String) { + /** + * An equality comparison, equivalent to `==` in Kotlin and to `=` in DynamoDB + */ + EQUALS("="), + + /** + * An inequality comparison, equivalent to `!=` in Kotlin and to `<>` in DynamoDB + */ + NOT_EQUALS("<>"), + + /** + * A less-than comparison, equivalent to `<` in Kotlin and DynamoDB + */ + LESS_THAN("<"), + + /** + * A less-than-or-equal-to comparison, equivalent to `<=` in Kotlin and DynamoDB + */ + LESS_THAN_OR_EQUAL("<="), + + /** + * A greater-than comparison, equivalent to `>` in Kotlin and DynamoDB + */ + GREATER_THAN(">"), + + /** + * A greater-than-or-equal-to comparison, equivalent to `>=` in Kotlin and DynamoDB + */ + GREATER_THAN_OR_EQUAL(">="), +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr.kt new file mode 100644 index 00000000000..4c428f6893a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ComparisonExpr.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.ComparisonExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a comparison expression as described in + * [DynamoDB's **making comparisons** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Comparators). + * The specific type of comparison is identified by the [comparator] field. + */ +@ExperimentalApi +public interface ComparisonExpr : + BooleanExpr, + SortKeyExpr { + /** + * The [Comparator] to use in the expression + */ + public val comparator: Comparator + + /** + * The left value being compared + */ + public val left: Expression + + /** + * The right value being compared + */ + public val right: Expression + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new comparison expression + * @param comparator The [Comparator] to use in the expression + * @param left The left value being compared + * @param right The right value being compared + */ +@ExperimentalApi +public fun ComparisonExpr(comparator: Comparator, left: Expression, right: Expression): ComparisonExpr = + ComparisonExprImpl(comparator, left, right) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor.kt new file mode 100644 index 00000000000..7619089b85b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ExpressionVisitor.kt @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * A [visitor](https://en.wikipedia.org/wiki/Visitor_pattern) that can traverse an [Expression] + * @param T The type of value used for state tracking by this visitor + */ +@ExperimentalApi +public interface ExpressionVisitor { + /** + * Visit an [AndExpr] + * @param expr The expression to visit + */ + public fun visit(expr: AndExpr): T + + /** + * Visit an [AttributePath] + * @param expr The expression to visit + */ + public fun visit(expr: AttributePath): T + + /** + * Visit a [BetweenExpr] + * @param expr The expression to visit + */ + public fun visit(expr: BetweenExpr): T + + /** + * Visit a [BooleanFuncExpr] + * @param expr The expression to visit + */ + public fun visit(expr: BooleanFuncExpr): T + + /** + * Visit a [ComparisonExpr] + * @param expr The expression to visit + */ + public fun visit(expr: ComparisonExpr): T + + /** + * Visit a [LiteralExpr] + * @param expr The expression to visit + */ + public fun visit(expr: LiteralExpr): T + + /** + * Visit an [InExpr] + * @param expr The expression to visit + */ + public fun visit(expr: InExpr): T + + /** + * Visit a [NotExpr] + * @param expr The expression to visit + */ + public fun visit(expr: NotExpr): T + + /** + * Visit an [OrExpr] + * @param expr The expression to visit + */ + public fun visit(expr: OrExpr): T + + /** + * Visit a [ScalarFuncExpr] + * @param expr The expression to visit + */ + public fun visit(expr: ScalarFuncExpr): T +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expressions.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expressions.kt new file mode 100644 index 00000000000..f212d18e1ac --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Expressions.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents any kind of expression. This is an abstract top-level interface and describes no details about an + * expression on its own. Expressions may be of various specific types (e.g., [AttributePath], [ComparisonExpr], + * [AndExpr], etc.) each of which have specific data detailing the expression. + * + * [Expression] and its derivatives support the [visitor design pattern](https://en.wikipedia.org/wiki/Visitor_pattern) + * by way of an [accept] method. + */ +@ExperimentalApi +public sealed interface Expression { + /** + * Accepts a visitor that is traversing an expression tree by dispatching to a subtype implementation. Subtype + * implementations MUST call the [ExpressionVisitor.visit] overload for their concrete type (effectively forming a + * [double dispatch](https://en.wikipedia.org/wiki/Double_dispatch)) and MUST return the resulting value. + * @param visitor The [ExpressionVisitor] that is traversing an expression + */ + public fun accept(visitor: ExpressionVisitor): T +} + +/** + * A subtype of [Expression] that represents a condition with a boolean value, such as would be used for filtering + * items. This is a [marker interface](https://en.wikipedia.org/wiki/Marker_interface_pattern) which adds no additional + * declarations. + */ +@ExperimentalApi +public sealed interface BooleanExpr : Expression + +/** + * A subtype of [Expression] that represents a key condition on a sort key, such as would be used for specifying a Query + * key. This is a [marker interface](https://en.wikipedia.org/wiki/Marker_interface_pattern) which adds no additional + * declarations. + */ +@ExperimentalApi +public sealed interface SortKeyExpr : Expression diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter.kt new file mode 100644 index 00000000000..6889ee93cdb --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/Filter.kt @@ -0,0 +1,951 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.util.dynamicAttr +import aws.smithy.kotlin.runtime.ExperimentalApi +import kotlin.jvm.JvmName + +/** + * A DSL interface providing support for "low-level" filter expressions. Implementations of this interface provide + * methods and properties which create boolean expressions to filter item results (e.g., in Scan or Query operations). + * Expressions are typically formed by getting a reference to an attribute path and then exercising some operation or + * function upon it. + * + * For example: + * + * ```kotlin + * filter { + * attr("foo") eq 42 + * } + * ``` + * + * This example creates an expression which checks whether an attribute named `foo` is equal to the value `42`. + * + * ## (Non-)Relationship to schema + * + * The expressions formed by [Filter] are referred to as a "low-level" filter expression. This is because they are not + * restricted by or adherent to any defined schema. Instead, they are a DSL convenience layer over [literal DynamoDB + * expression strings and expression attribute value maps](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.html). + * As such they provide **minimal type correctness** and may allow you to form expressions which are invalid given the + * shape of your data, such as attributes which don't exist, comparisons with mismatched data types, etc. + * + * # Attributes + * + * Every filter condition contains at least one attribute. Attributes are referenced by attribute paths, analogous to + * [document paths in DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html#Expressions.Attributes.NestedElements.DocumentPathExamples). + * Attribute paths consist of one or more elements, which are either names (e.g., of a top-level attribute or a nested + * key in a map attribute) or indices (i.e., into a list). The first (and often only) element of an attribute path is a + * name. + * + * ## Getting a top-level attribute + * + * All attribute paths start with a top-level attribute expression, created by the [attr] function: + * + * ```kotlin + * attr("foo") // References the top-level attribute "foo" + * ``` + * + * Note, the attribute `foo` may not exist for a given item or for an entire table. + * + * ## Nesting + * + * Sometimes values are nested inside other attributes like lists and maps. Filter expressions can operate on those + * nested values by forming a more detailed attribute path using the `[]` operator or [get] functions on a path. + * + * For example, consider an item structure such as: + * + * ```json + * { + * "foo": "Hello", + * "bar": { + * "baz": [ + * "Yay", + * null, + * 42, + * true + * ] + * } + * } + * ``` + * + * The value `"Yay"` can be referenced with the following DSL syntax: + * + * ```kotlin + * attr("bar")["baz"][0] + * ``` + * + * That is, in the top-level attribute `bar`, in the value keyed by `baz`, the element at index `0`. + * + * # Equalities/inequalities + * + * A very common filter condition is verifying whether some attribute is equal (or unequal) to another value—either a + * literal value or the value of another attribute. These comparisons are available by using the following functions: + * * [eq] — checks if two values are equal (equivalent to Kotlin's `==` operator) + * * [neq] — checks if two values are _not_ equal (equivalent to Kotlin's `!=` operator) + * * [lt] — checks if a value is less than another value (equivalent to Kotlin's `<` operator) + * * [lte] — checks if a value is less than _or equal to_ another value (equivalent to Kotlin's `<=` operator) + * * [gt] — checks if a value is greater than another value (equivalent to Kotlin's `>` operator) + * * [gte] — checks if a value is greater than _or equal to_ another value (equivalent to Kotlin's `>=` operator) + * + * For example: + * + * ```kotlin + * attr("foo") eq 5 // Checks whether the value of attribute `foo` is `5` + * attr("bar") gt attr("baz") // Checks whether the value of attribute `bar` is greater than attribute `baz` + * ``` + * + * # Ranges and sets + * + * Expressions can check whether some attribute value is in a given range or set of possible values. These checks are + * available via the [isBetween] or [isIn] functions: + * + * ```kotlin + * // Checks whether the value of attribute `foo` is between 40 and 60 (inclusive) + * attr("foo") isIn 40..60 + * + * // Checks whether the value of attribute `foo` is either 1, 2, 4, 8, 16, or 32 + * attr("bar") isIn setOf(1, 2, 4, 8, 16, 32) + * + * // Checks whether the value of attribute `baz` is between the value of `foo` and the value of `baz` + * attr("baz").isBetween(attr("foo"), attr("baz")) + * ``` + * + * # Boolean logic + * + * The previous sections dealt with singular conditions (e.g., `a == b`, `c in (d, e, f)`, etc.). But complex queries + * may involve multiple conditions or negative conditions akin to the boolean operations AND (`&&`), OR (`||`, and NOT + * (`!`). This logic is available via the [and], [or], and [not] functions: + * + * ```kotlin + * and( + * attr("foo") eq 42, + * attr("bar") neq 42, + * ) + * ``` + * + * This block checks for value of the attribute `foo` equalling `42` and the value of attribute `bar` _not_ equalling + * `42`. This is logically equivalent to the Kotlin syntax: + * + * ```kotlin + * (foo == 42 && bar != 42) + * ``` + * + * These boolean functions can be composed in various ways: + * + * ```kotlin + * or( + * attr("foo") eq "apple", + * and( + * attr("bar") lt attr("baz"), + * attr("baz") gte 42, + * ), + * not( + * attr("qux") in setOf("ready", "set", "go"), + * ), + * ) + * ``` + * + * This complex DSL code checks that at least one of three conditions is met (boolean OR): + * * The value of attribute `foo` is `"apple"` + * * The value of attribute `bar` is less than the value of `baz` **–and–** the value of `baz` is greater/equal to `42` + * * The value of attribute `qux` is not one of `"ready"`, `"steady"`, or `"go"` + * + * This is logically equivalent to the Kotlin syntax: + * + * ```kotlin + * (foo == "apple") || (bar < baz && baz >= 42) || qux !in setOf("ready", "steady", "go") + * ``` + * + * # Other functions/properties + * + * Several additional filter expressions are possible via the following methods/properties: + * + * * [contains] — Checks if a string/list attribute value contains the given value + * * [exists] — Checks if _any value_ (including `null`) exists for an attribute. The low-level DynamoDB function for + * this is `attribute_exists`. + * * [notExists] — Checks if no value is present for an attribute (i.e., the attribute is "undefined" for an item). The + * low-level DynamoDB function for this is `attribute_not_exists`. + * * [isOfType] — Checks if an attribute value is of the given type. The low-level DynamoDB function for this is + * `attribute_type`. + * * [size] — Gets the size of an attribute (e.g., number of elements in list/map/set, the length of a string, etc.) + * * [startsWith] — Checks if a string attribute value starts with the given value. The low-level DynamoDB function for + * this is `begins_with`. + * + * For example: + * + * ```kotlin + * attr("foo") contains 13 // Checks whether the value of attribute `foo` contains the value `13` + * attr("bar").exists() // Checks whether any value exists for `bar` (including `null`) + * ``` + */ +@ExperimentalApi +public interface Filter { + // ATTRIBUTES + + /** + * Creates an attribute path reference from a top-level attribute name. To reference nested properties or indexed + * elements, use the `[]` operator or [AttributePath.get]. + * @param name The top-level attribute name + */ + public fun attr(name: String): AttributePath + + /** + * Creates an attribute path reference for an index into a list or set + * @param index The index to dereference + */ + public operator fun AttributePath.get(index: Int): AttributePath + + /** + * Creates an attribute path reference for a key in map + * @param key The key to dereference + */ + public operator fun AttributePath.get(key: String): AttributePath + + // BINARY OPERATORS + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param expr The other expression in the comparison + */ + public infix fun Expression.eq(expr: Expression): BooleanExpr + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: Boolean?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: ByteArray?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: List?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: Map?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: Nothing?): BooleanExpr = eq(LiteralExpr(null)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: Number?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetByteArray") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetNumber") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetString") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetUByte") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetUInt") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetULong") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("eqSetUShort") + public infix fun Expression.eq(value: Set?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: String?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: UByte?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: UInt?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: ULong?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying two expressions are equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.eq(value: UShort?): BooleanExpr = eq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param expr The other expression in the comparison + */ + public infix fun Expression.neq(expr: Expression): BooleanExpr + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: Boolean?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: ByteArray?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: List?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: Map?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: Nothing?): BooleanExpr = neq(LiteralExpr(null)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: Number?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetByteArray") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetNumber") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetString") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetUByte") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetUInt") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetULong") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("neqSetUShort") + public infix fun Expression.neq(value: Set?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: String?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: UByte?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: UInt?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: ULong?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying two expressions are not equal to each other + * @param value The other value in the comparison + */ + public infix fun Expression.neq(value: UShort?): BooleanExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param expr The other expression in the comparison + */ + public infix fun Expression.lt(expr: Expression): BooleanExpr + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: ByteArray): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: Number): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: String): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: UByte): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: UInt): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: ULong): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lt(value: UShort): BooleanExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun Expression.lte(expr: Expression): BooleanExpr + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: ByteArray): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: Number): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: String): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: UByte): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: UInt): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: ULong): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.lte(value: UShort): BooleanExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param expr The other expression in the comparison + */ + public infix fun Expression.gt(expr: Expression): BooleanExpr + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: ByteArray): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: Number): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: String): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: UByte): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: UInt): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: ULong): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gt(value: UShort): BooleanExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun Expression.gte(expr: Expression): BooleanExpr + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: ByteArray): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: Number): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: String): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: UByte): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: UInt): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: ULong): BooleanExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying this expression is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun Expression.gte(value: UShort): BooleanExpr = gte(LiteralExpr(value)) + + // RANGES & SETS + + /** + * Creates a range expression for verifying this expression is between two other expressions + * @param min The lower bound expression + * @param max The upper bound expression (inclusive) + */ + public fun AttributePath.isBetween(min: Expression, max: Expression): BooleanExpr + + /** + * Creates a range expression for verifying this expression is between two other expressions + * @param min The lower bound value + * @param max The upper bound value (inclusive) + */ + public fun AttributePath.isBetween(min: ByteArray, max: ByteArray): BooleanExpr = + isBetween(LiteralExpr(min), LiteralExpr(max)) + + // TODO The following overloads support [ClosedRange] but [OpenEndRange] also exists. DynamoDB expressions don't + // support it directly but we may be able to cheese it with two inequalities ANDed together. + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeNumber") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr where N : Number, N : Comparable = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeString") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUByte") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUInt") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeULong") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying this expression is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUShort") + public infix fun AttributePath.isIn(range: ClosedRange): BooleanExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionExpression") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionByteArray") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionList") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionMap") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionNumber") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetByteArray") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetNumber") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetString") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetUByte") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetUInt") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetULong") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionSetUShort") + public infix fun AttributePath.isIn(set: Collection?>): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionString") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionUByte") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionUInt") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionULong") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + /** + * Creates a contains expression for verifying this expression is in the given set of elements + * @param set The collection to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInCollectionUShort") + public infix fun AttributePath.isIn(set: Collection): BooleanExpr = isIn(set.map(::LiteralExpr)) + + // FUNCTIONS + + /** + * Creates a contains expression for verifying this expression contains the given expression + * @param expr The expression which may be contained + */ + public infix fun AttributePath.contains(expr: Expression): BooleanExpr + + /** + * Creates a contains expression for verifying this expression contains the given expression + * @param value The value which may be contained + */ + public infix fun AttributePath.contains(value: Any?): BooleanExpr = contains(LiteralExpr(dynamicAttr(value))) + + /** + * Creates an expression for verifying an attribute exists + */ + public fun AttributePath.exists(): BooleanExpr + + /** + * Creates an expression for verifying an attribute does not exist + */ + public fun AttributePath.notExists(): BooleanExpr + + /** + * Creates an expression for verifying an attribute exists + * @param type The [AttributeType] to test for + */ + public infix fun AttributePath.isOfType(type: AttributeType): BooleanExpr + + /** + * Creates an expression for getting the size (or length) of an attribute + */ + public val AttributePath.size: Expression + + /** + * Creates an expression for verifying this attribute starts with the given expression + * @param expr The expression to test for at the beginning of this attribute + */ + public infix fun AttributePath.startsWith(expr: Expression): BooleanExpr + + /** + * Creates an expression for verifying this attribute starts with the given expression + * @param value The value to test for at the beginning of this attribute + */ + public infix fun AttributePath.startsWith(value: ByteArray): BooleanExpr = startsWith(LiteralExpr(value)) + + /** + * Creates an expression for verifying this attribute starts with the given expression + * @param value The value to test for at the beginning of this attribute + */ + public infix fun AttributePath.startsWith(value: String): BooleanExpr = startsWith(LiteralExpr(value)) + + // BOOLEAN LOGIC + + /** + * Creates a boolean expression for verifying that multiple conditions are all met + * @param conditions A list of 2 or more conditions + */ + public fun and(conditions: List): BooleanExpr + + /** + * Creates a boolean expression for verifying that multiple conditions are all met + * @param conditions A list of 2 or more conditions + */ + public fun and(vararg conditions: BooleanExpr): BooleanExpr = and(conditions.toList()) + + /** + * Creates a boolean expression for verifying the opposite of a condition is met + * @param condition The condition to negate + */ + public fun not(condition: BooleanExpr): BooleanExpr + + /** + * Creates a boolean expression for verifying that at least one of several conditions is met + * @param conditions A list of 2 or more conditions + */ + public fun or(conditions: List): BooleanExpr + + /** + * Creates a boolean expression for verifying that at least one of several conditions is met + * @param conditions A list of 2 or more conditions + */ + public fun or(vararg conditions: BooleanExpr): BooleanExpr = or(conditions.toList()) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr.kt new file mode 100644 index 00000000000..6c2d009f6d8 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/InExpr.kt @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.InExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents an `IN` expression as described in + * [DynamoDB's **making comparisons** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Comparators). + * This expression will be true if `value in set` (or, equivalently, if `set.contains(value)`). + */ +@ExperimentalApi +public interface InExpr : BooleanExpr { + /** + * The value to check for in [set] + */ + public val value: Expression + + /** + * The set of values to compare against [value] + */ + public val set: Collection + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new `IN` expression + * @param value The value to check for in [set] + * @param set The set of values to compare against [value] + */ +@ExperimentalApi +public fun InExpr(value: Expression, set: Collection): InExpr = InExprImpl(value, set) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter.kt new file mode 100644 index 00000000000..b807ac912e3 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilter.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.KeyFilterImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.SortKeyFilterImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a filter which limits a Query operation to a specific partition key and optional sort key criteria (if + * applicable) + */ +@ExperimentalApi +public interface KeyFilter { + /** + * The required value of the partition key + */ + public val partitionKey: Any + + /** + * The sort key expression (if set) + */ + public val sortKey: SortKeyExpr? +} + +/** + * Creates a new [KeyFilter] for a partition key + * @param partitionKey The value required for the partition key. This must be set to a byte array, string, or number + * (including unsigned numbers). + */ +@ExperimentalApi +public fun KeyFilter(partitionKey: Any): KeyFilter = KeyFilterImpl(partitionKey, null) + +/** + * Creates a new [KeyFilter] for a partition key and sort key. Note that using this overload requires a schema with a + * composite key. + * @param partitionKey The value required for the partition key. This must be set to a byte array, string, or number + * (including unsigned numbers). + * @param sortKey A DSL block that sets the condition for the sort key. See [SortKeyFilter] for more details. + */ +@ExperimentalApi +public fun KeyFilter(partitionKey: Any, sortKey: SortKeyFilter.() -> SortKeyExpr): KeyFilter = + KeyFilterImpl(partitionKey, SortKeyFilterImpl.run(sortKey)) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr.kt new file mode 100644 index 00000000000..13caf15dc15 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/LiteralExpr.kt @@ -0,0 +1,167 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.LiteralExprImpl +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import kotlin.jvm.JvmName + +/** + * Represents an expression that consists of a single literal value + */ +@ExperimentalApi +public interface LiteralExpr : Expression { + /** + * The low-level DynamoDB representation of the literal value + */ + public val value: AttributeValue + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new literal expression + * @param value The low-level DynamoDB representation of the literal value + */ +@ExperimentalApi +public fun LiteralExpr(value: AttributeValue): LiteralExpr = LiteralExprImpl(value) + +private val NULL_LITERAL = LiteralExpr(attr(null)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: Boolean?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: ByteArray?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: List?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: Map?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@Suppress("UNUSED_PARAMETER") +public fun LiteralExpr(value: Nothing?): LiteralExpr = NULL_LITERAL + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: Number?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetByteArray") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetNumber") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetString") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetUByte") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetUInt") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetULong") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +@JvmName("LiteralExprSetUShort") +public fun LiteralExpr(value: Set?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: String?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: UByte?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: UInt?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: ULong?): LiteralExpr = LiteralExpr(attr(value)) + +/** + * Creates a new literal expression + * @param value The literal value which will be converted to an [AttributeValue] + */ +@ExperimentalApi +public fun LiteralExpr(value: UShort?): LiteralExpr = LiteralExpr(attr(value)) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr.kt new file mode 100644 index 00000000000..fce372ff7f5 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/NotExpr.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.NotExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a `NOT` expression as described in + * [DynamoDB's **logical evaluations** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.LogicalEvaluations). + * This expression will be true if `!operand` (i.e., `operand` evaluates to `false`). + */ +@ExperimentalApi +public interface NotExpr : BooleanExpr { + /** + * The condition to negate + */ + public val operand: BooleanExpr + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new `NOT` expression + * @param operand The condition to negate + */ +@ExperimentalApi +public fun NotExpr(operand: BooleanExpr): NotExpr = NotExprImpl(operand) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr.kt new file mode 100644 index 00000000000..a7b9705dab8 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/OrExpr.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.OrExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents an `OR` expression as described in + * [DynamoDB's **logical evaluations** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.LogicalEvaluations). + * This expression will be true if `(operand[0] || operand[1] || ... || operand[n - 1])`. + */ +@ExperimentalApi +public interface OrExpr : BooleanExpr { + /** + * A list of 2 or more [BooleanExpr] conditions which are ANDed together + */ + public val operands: List + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new [OrExpr] with the given [operands] + * @param operands A list of 2 or more [BooleanExpr] conditions which are ORed together + */ +@ExperimentalApi +public fun OrExpr(operands: List): OrExpr = OrExprImpl(operands) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc.kt new file mode 100644 index 00000000000..e1a32b09f17 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFunc.kt @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Identifies a + * [DynamoDB expression function](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions) + * which returns a non-boolean value + * @param exprString The literal name of the function to use in expression strings + */ +@ExperimentalApi +public enum class ScalarFunc(public val exprString: String) { + /** + * The `size` function + */ + SIZE("size"), +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr.kt new file mode 100644 index 00000000000..b700c19391b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/ScalarFuncExpr.kt @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.ScalarFuncExprImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a function expression that yields a non-boolean result as described in + * [DynamoDB's **function** documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions) + */ +@ExperimentalApi +public interface ScalarFuncExpr : BooleanExpr { + /** + * The specific non-boolean function to use + */ + public val func: ScalarFunc + + /** + * The attribute path to pass as the function's first argument + */ + public val path: AttributePath + + /** + * Any additional arguments used by the function + */ + public val additionalOperands: List + + override fun accept(visitor: ExpressionVisitor): T = visitor.visit(this) +} + +/** + * Creates a new non-boolean function expression + * @param func The specific non-boolean function to use + * @param path The attribute path to pass as the function's first argument + * @param additionalOperands Any additional arguments used by the function + */ +@ExperimentalApi +public fun ScalarFuncExpr( + func: ScalarFunc, + path: AttributePath, + additionalOperands: List = listOf(), +): ScalarFuncExpr = ScalarFuncExprImpl(func, path, additionalOperands) + +/** + * Creates a new non-boolean function expression + * @param func The specific non-boolean function to use + * @param path The attribute path to pass as the function's first argument + * @param additionalOperands Any additional arguments used by the function + */ +@ExperimentalApi +public fun ScalarFuncExpr( + func: ScalarFunc, + path: AttributePath, + vararg additionalOperands: Expression, +): ScalarFuncExpr = ScalarFuncExprImpl(func, path, additionalOperands.toList()) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter.kt new file mode 100644 index 00000000000..0dd7fa804f3 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilter.kt @@ -0,0 +1,459 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.smithy.kotlin.runtime.ExperimentalApi +import kotlin.jvm.JvmName + +/** + * Represents a sort key independent of schema + */ +@ExperimentalApi +public interface SortKey + +/** + * A DSL interface providing support for "low-level" sort key filter expressions. Implementations of this interface + * provide methods and properties which create sort key expressions to narrow results in Query operations. Expressions + * are formed by referencing [sortKey] and then exercising some function upon it. + * + * For example: + * + * ```kotlin + * { sortKey eq 42 } + * ``` + * + * This example creates an expression which checks whether the sort key is equal to the value `42`. + * + * ## (Non-)Relationship to schema + * + * The expressions formed by [SortKeyFilter] are referred to as a "low-level" filter expression. This is because they + * are not restricted by or adherent to any defined schema. Instead, they are a DSL convenience layer over [literal + * DynamoDB expression strings and expression attribute value maps](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.KeyConditionExpressions.html). + * As such they provide **minimal type correctness** and may allow you to form expressions which are invalid given the + * shape of your data, such with mismatched data types. + * + * # Equalities/inequalities + * + * A very common filter condition is verifying whether the value of the sort key is equal (or unequal) to another a + * literal value. These comparisons are available by using the following functions: + * * [eq] — checks if the sort key is equal to another value (equivalent to Kotlin's `==` operator) + * * [neq] — checks if two the sort key is _not_ equal to another value (equivalent to Kotlin's `!=` operator) + * * [lt] — checks if the sort key is less than another value (equivalent to Kotlin's `<` operator) + * * [lte] — checks if the sort key is less than _or equal to_ another value (equivalent to Kotlin's `<=` operator) + * * [gt] — checks if the sort key is greater than another value (equivalent to Kotlin's `>` operator) + * * [gte] — checks if the sort key is greater than _or equal to_ another value (equivalent to Kotlin's `>=` operator) + * + * For example: + * + * ```kotlin + * sortKey eq 5 // Checks whether the value of the sort key is `5` + * sortKey gt "baz" // Checks whether the value of the sort key is greater than `"baz"` + * ``` + * + * # Ranges and sets + * + * Expressions can check whether the value of the sort key is in a given range possible values. These checks are + * available via the [isBetween] or [isIn] functions: + * + * ```kotlin + * // Checks whether the value of the sort key is between 40 and 60 (inclusive) + * sortKey isIn 40..60 + * + * // Checks whether the value of the sort key is between two binary values (inclusive) + * val minBinary: ByteArray = ... + * val maxBinary: ByteArray = ... + * sortKey.isBetween(minBinary, maxBinary) + * ``` + * + * # Prefixes + * + * The [startsWith] function checks for a prefix in the value of the sort key. For example: + * + * ```kotlin + * sortKey startsWith "abc" // Checks whether the value of the sort key starts with `"abc"` + * ``` + */ +@ExperimentalApi +public interface SortKeyFilter { + /** + * Gets an attribute reference to the sort key + */ + public val sortKey: SortKey + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.eq(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: ByteArray): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: Number): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: String): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: UByte): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: UInt): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: ULong): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an equality expression for verifying the sort key is equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.eq(value: UShort): SortKeyExpr = eq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.neq(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: ByteArray): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: Number): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: String): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: UByte): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: UInt): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: ULong): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is not equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.neq(value: UShort): SortKeyExpr = neq(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.lt(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: ByteArray): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: Number): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: String): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: UByte): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: UInt): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: ULong): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lt(value: UShort): SortKeyExpr = lt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.lte(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.lte(value: ByteArray): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: Number): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: String): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: UByte): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: UInt): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: ULong): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is less than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.lte(value: UShort): SortKeyExpr = lte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.gt(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: ByteArray): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: Number): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: String): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: UByte): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: UInt): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: ULong): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gt(value: UShort): SortKeyExpr = gt(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param expr The other expression in the comparison + */ + public infix fun SortKey.gte(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: ByteArray): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: Number): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: String): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: UByte): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: UInt): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: ULong): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates an inequality expression for verifying the sort key is greater than or equal to another expression + * @param value The other value in the comparison + */ + public infix fun SortKey.gte(value: UShort): SortKeyExpr = gte(LiteralExpr(value)) + + /** + * Creates a range expression for verifying the sort key is between two other expressions + * @param min The lower bound expression + * @param max The upper bound expression (inclusive) + */ + public fun SortKey.isBetween(min: LiteralExpr, max: LiteralExpr): SortKeyExpr + + /** + * Creates a range expression for verifying the sort key is between two other expressions + * @param min The lower bound value + * @param max The upper bound value (inclusive) + */ + public fun SortKey.isBetween(min: ByteArray, max: ByteArray): SortKeyExpr = + isBetween(LiteralExpr(min), LiteralExpr(max)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeNumber") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr where N : Number, N : Comparable = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeString") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUByte") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUInt") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeULong") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates a range expression for verifying the sort key is in the given range + * @param range The range to check + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("isInRangeUShort") + public infix fun SortKey.isIn(range: ClosedRange): SortKeyExpr = + isBetween(LiteralExpr(range.start), LiteralExpr(range.endInclusive)) + + /** + * Creates an expression for verifying the sort key starts with the given expression + * @param expr The expression to test for at the beginning of this attribute + */ + public infix fun SortKey.startsWith(expr: LiteralExpr): SortKeyExpr + + /** + * Creates an expression for verifying the sort key starts with the given expression + * @param value The value to test for at the beginning of this attribute + */ + public infix fun SortKey.startsWith(value: ByteArray): SortKeyExpr = startsWith(LiteralExpr(value)) + + /** + * Creates an expression for verifying the sort key starts with the given expression + * @param value The value to test for at the beginning of this attribute + */ + public infix fun SortKey.startsWith(value: String): SortKeyExpr = startsWith(LiteralExpr(value)) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AndExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AndExprImpl.kt new file mode 100644 index 00000000000..a190d854265 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AndExprImpl.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.AndExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BooleanExpr + +internal data class AndExprImpl(override val operands: List) : AndExpr { + init { + require(operands.size > 1) { "AND operations require two or more operands" } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AttributePathImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AttributePathImpl.kt new file mode 100644 index 00000000000..fda3ec91ebb --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/AttributePathImpl.kt @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.AttrPathElement +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.AttributePath + +internal data class AttrPathNameImpl(override val name: String) : AttrPathElement.Name + +internal data class AttrPathIndexImpl(override val index: Int) : AttrPathElement.Index + +internal data class AttributePathImpl( + override val element: AttrPathElement, + override val parent: AttributePath? = null, +) : AttributePath { + init { + require(element is AttrPathElement.Name || parent != null) { + "Top-level attribute paths must be names (not indices)" + } + } +} + +/** + * An abstract attribute path that represents the sort key in a given schema. This isn't a concrete path and will be + * replaced by the schema's _actual_ sort key in [KeyFilterImpl.toExpression]. + */ +internal data object SkAttrPathImpl : AttributePath { + override val element get() = error("Unsupported") + override val parent get() = error("Unsupported") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BetweenExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BetweenExprImpl.kt new file mode 100644 index 00000000000..af5b9f4e2b4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BetweenExprImpl.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BetweenExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.Expression + +internal data class BetweenExprImpl( + override val value: Expression, + override val min: Expression, + override val max: Expression, +) : BetweenExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BooleanFuncExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BooleanFuncExprImpl.kt new file mode 100644 index 00000000000..2799ac5e14e --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/BooleanFuncExprImpl.kt @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.AttributePath +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BooleanFunc +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BooleanFuncExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.Expression + +internal data class BooleanFuncExprImpl( + override val func: BooleanFunc, + override val path: AttributePath, + override val additionalOperands: List = listOf(), +) : BooleanFuncExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ComparisonExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ComparisonExprImpl.kt new file mode 100644 index 00000000000..4cfbc0c6fb6 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ComparisonExprImpl.kt @@ -0,0 +1,15 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.Comparator +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.ComparisonExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.Expression + +internal data class ComparisonExprImpl( + override val comparator: Comparator, + override val left: Expression, + override val right: Expression, +) : ComparisonExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/FilterImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/FilterImpl.kt new file mode 100644 index 00000000000..f1ad451759a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/FilterImpl.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.* + +internal data object FilterImpl : Filter { + // ATTRIBUTES + + override fun attr(name: String) = AttributePath(name) + override fun AttributePath.get(index: Int) = AttributePath(index, parent = this) + override fun AttributePath.get(key: String) = AttributePath(key, parent = this) + + // BINARY OPERATORS + + override fun Expression.eq(expr: Expression) = ComparisonExpr(Comparator.EQUALS, this, expr) + override fun Expression.neq(expr: Expression) = ComparisonExpr(Comparator.NOT_EQUALS, this, expr) + override fun Expression.lt(expr: Expression) = ComparisonExpr(Comparator.LESS_THAN, this, expr) + override fun Expression.lte(expr: Expression) = ComparisonExpr(Comparator.LESS_THAN_OR_EQUAL, this, expr) + override fun Expression.gt(expr: Expression) = ComparisonExpr(Comparator.GREATER_THAN, this, expr) + override fun Expression.gte(expr: Expression) = ComparisonExpr(Comparator.GREATER_THAN_OR_EQUAL, this, expr) + + // RANGES & SETS + + override fun AttributePath.isBetween(min: Expression, max: Expression) = BetweenExpr(this, min, max) + override fun AttributePath.isIn(set: Collection) = InExpr(this, set) + + // FUNCTIONS + + override fun AttributePath.contains(expr: Expression) = BooleanFuncExpr(BooleanFunc.CONTAINS, this, expr) + override fun AttributePath.exists() = BooleanFuncExpr(BooleanFunc.ATTRIBUTE_EXISTS, this) + override fun AttributePath.notExists() = BooleanFuncExpr(BooleanFunc.ATTRIBUTE_NOT_EXISTS, this) + override fun AttributePath.isOfType(type: AttributeType) = BooleanFuncExpr(BooleanFunc.ATTRIBUTE_TYPE, this, LiteralExpr(type.abbreviation)) + override val AttributePath.size get() = ScalarFuncExpr(ScalarFunc.SIZE, this) + override fun AttributePath.startsWith(expr: Expression) = BooleanFuncExpr(BooleanFunc.BEGINS_WITH, this, expr) + + // BOOLEAN LOGIC + + override fun and(conditions: List) = AndExpr(conditions) + override fun not(condition: BooleanExpr) = NotExpr(condition) + override fun or(conditions: List) = OrExpr(conditions) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/InExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/InExprImpl.kt new file mode 100644 index 00000000000..e48dbe1a06c --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/InExprImpl.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.Expression +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.InExpr + +internal data class InExprImpl(override val value: Expression, override val set: Collection) : InExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/KeyFilterImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/KeyFilterImpl.kt new file mode 100644 index 00000000000..29fda70de05 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/KeyFilterImpl.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.* +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.util.dynamicAttr +import aws.sdk.kotlin.hll.dynamodbmapper.util.requireNull + +internal data class KeyFilterImpl(override val partitionKey: Any, override val sortKey: SortKeyExpr?) : KeyFilter { + init { + require( + partitionKey is ByteArray || + partitionKey is Number || + partitionKey is String || + partitionKey is UByte || + partitionKey is UInt || + partitionKey is ULong || + partitionKey is UShort, + ) { "Partition key values must be either a ByteArray, Number, String, or an unsigned number type" } + } +} + +internal fun KeyFilter.toExpression(schema: ItemSchema<*>) = when (schema) { + is ItemSchema.CompositeKey<*, *, *> -> { + val pkCondition = pkCondition(schema, partitionKey) + + sortKey?.let { sortKey -> + FilterImpl.run { + val skAttr = attr(schema.sortKey.name) + val skCondition = when (sortKey) { + is BetweenExpr -> BetweenExpr(skAttr, sortKey.min, sortKey.max) + is ComparisonExpr -> ComparisonExpr(sortKey.comparator, skAttr, sortKey.right) + is BooleanFuncExpr -> BooleanFuncExpr(sortKey.func, skAttr, sortKey.additionalOperands) + } + + and(pkCondition, skCondition) + } + } ?: pkCondition + } + + is ItemSchema.PartitionKey<*, *> -> { + requireNull(sortKey) { "Sort key condition not allowed on schema without a sort key" } + pkCondition(schema, partitionKey) + } + + else -> error("Unknown schema type ${schema::class} (expected ItemSchema.CompositeKey or ItemSchema.PartitionKey)") +} + +private fun pkCondition(schema: ItemSchema.PartitionKey<*, *>, partitionKey: Any) = + FilterImpl.run { attr(schema.partitionKey.name) eq LiteralExpr(dynamicAttr(partitionKey)) } diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/LiteralExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/LiteralExprImpl.kt new file mode 100644 index 00000000000..97236f665a9 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/LiteralExprImpl.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.LiteralExpr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue + +internal data class LiteralExprImpl(override val value: AttributeValue) : LiteralExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/NotExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/NotExprImpl.kt new file mode 100644 index 00000000000..815c4c03e5a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/NotExprImpl.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BooleanExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.NotExpr + +internal data class NotExprImpl(override val operand: BooleanExpr) : NotExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/OrExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/OrExprImpl.kt new file mode 100644 index 00000000000..e2c7d474648 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/OrExprImpl.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.BooleanExpr +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.OrExpr + +internal data class OrExprImpl(override val operands: List) : OrExpr { + init { + require(operands.size > 1) { "OR operations require two or more operands" } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ParameterizingExpressionVisitor.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ParameterizingExpressionVisitor.kt new file mode 100644 index 00000000000..f04af75a1c4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ParameterizingExpressionVisitor.kt @@ -0,0 +1,711 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.* +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue + +/** + * An [ExpressionVisitor] that traverses an [Expression] to yield an expression string, map of attribute names, and map + * of attribute values suitable for passing to DynamoDB. This visitor is designed for reuse across multiple expressions, + * yielding multiple expression strings but unified maps of attribute names and values. + * + * For example: + * + * ```kotlin + * val visitor = ParameterizingExpressionVisitor() + * + * ddb.query { + * filterExpression = visitor.visit() + * keyConditionExpression = visitor.visit() + * expressionAttributeNames = visitor.expressionAttributeNames() + * expressionAttributeValues = visitor.expressionAttributeValues() + * // … + * } + * ``` + */ +internal open class ParameterizingExpressionVisitor : ExpressionVisitor { + private val namePlaceholders = mutableMapOf() + private val valuePlaceholders = mutableMapOf() + + private fun Expression.accept(): String = accept(this@ParameterizingExpressionVisitor) + + fun expressionAttributeNames(): Map? = namePlaceholders + .entries + .associate { (name, placeholder) -> placeholder to name } + .takeUnless { it.isEmpty() } + + fun expressionAttributeValues(): Map? = valuePlaceholders + .entries + .associate { (value, placeholder) -> placeholder to value } + .takeUnless { it.isEmpty() } + + private fun funcString(funcName: String, path: AttributePath, additionalOperands: List) = buildString { + append(funcName) + append('(') + append(path.accept()) + additionalOperands.forEach { operand -> + append(", ") + append(operand.accept()) + } + append(')') + } + + override fun visit(expr: AndExpr) = expr.operands.joinToString(" AND ") { "(${it.accept()})" } + + override fun visit(expr: AttributePath) = buildString { + expr.parent?.let { parent -> append(parent.accept()) } + + when (val element = expr.element) { + is AttrPathElement.Index -> { + append('[') + append(element.index) + append(']') + } + + is AttrPathElement.Name -> { + if (expr.parent != null) append('.') + + val literal = if (element.name.attributeNameNeedsEscaping) { + namePlaceholders.getOrPut(element.name) { "#k${namePlaceholders.size}" } + } else { + element.name + } + append(literal) + } + } + } + + override fun visit(expr: BetweenExpr) = buildString { + append(expr.value.accept()) + append(" BETWEEN ") + append(expr.min.accept()) + append(" AND ") + append(expr.max.accept()) + } + + override fun visit(expr: BooleanFuncExpr) = funcString(expr.func.exprString, expr.path, expr.additionalOperands) + + override fun visit(expr: ComparisonExpr) = buildString { + append(expr.left.accept()) + append(' ') + append(expr.comparator.exprString) + append(' ') + append(expr.right.accept()) + } + + override fun visit(expr: LiteralExpr) = valuePlaceholders.getOrPut(expr.value) { ":v${valuePlaceholders.size}" } + + override fun visit(expr: InExpr): String = buildString { + append(expr.value.accept()) + append(" IN (") + expr.set.joinTo(this, ", ") { it.accept() } + append(')') + } + + override fun visit(expr: NotExpr) = "NOT (${expr.operand.accept()})" + + override fun visit(expr: OrExpr) = expr.operands.joinToString(" OR ") { "(${it.accept()})" } + + override fun visit(expr: ScalarFuncExpr) = funcString(expr.func.exprString, expr.path, expr.additionalOperands) +} + +/** + * The set of + * [DynamoDB reserved words](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html) + */ +private val reservedWords = setOf( + "abort", + "absolute", + "action", + "add", + "after", + "agent", + "aggregate", + "all", + "allocate", + "alter", + "analyze", + "and", + "any", + "archive", + "are", + "array", + "as", + "asc", + "ascii", + "asensitive", + "assertion", + "asymmetric", + "at", + "atomic", + "attach", + "attribute", + "auth", + "authorization", + "authorize", + "auto", + "avg", + "back", + "backup", + "base", + "batch", + "before", + "begin", + "between", + "bigint", + "binary", + "bit", + "blob", + "block", + "boolean", + "both", + "breadth", + "bucket", + "bulk", + "by", + "byte", + "call", + "called", + "calling", + "capacity", + "cascade", + "cascaded", + "case", + "cast", + "catalog", + "char", + "character", + "check", + "class", + "clob", + "close", + "cluster", + "clustered", + "clustering", + "clusters", + "coalesce", + "collate", + "collation", + "collection", + "column", + "columns", + "combine", + "comment", + "commit", + "compact", + "compile", + "compress", + "condition", + "conflict", + "connect", + "connection", + "consistency", + "consistent", + "constraint", + "constraints", + "constructor", + "consumed", + "continue", + "convert", + "copy", + "corresponding", + "count", + "counter", + "create", + "cross", + "cube", + "current", + "cursor", + "cycle", + "data", + "database", + "date", + "datetime", + "day", + "deallocate", + "dec", + "decimal", + "declare", + "default", + "deferrable", + "deferred", + "define", + "defined", + "definition", + "delete", + "delimited", + "depth", + "deref", + "desc", + "describe", + "descriptor", + "detach", + "deterministic", + "diagnostics", + "directories", + "disable", + "disconnect", + "distinct", + "distribute", + "do", + "domain", + "double", + "drop", + "dump", + "duration", + "dynamic", + "each", + "element", + "else", + "elseif", + "empty", + "enable", + "end", + "equal", + "equals", + "error", + "escape", + "escaped", + "eval", + "evaluate", + "exceeded", + "except", + "exception", + "exceptions", + "exclusive", + "exec", + "execute", + "exists", + "exit", + "explain", + "explode", + "export", + "expression", + "extended", + "external", + "extract", + "fail", + "false", + "family", + "fetch", + "fields", + "file", + "filter", + "filtering", + "final", + "finish", + "first", + "fixed", + "flattern", + "float", + "for", + "force", + "foreign", + "format", + "forward", + "found", + "free", + "from", + "full", + "function", + "functions", + "general", + "generate", + "get", + "glob", + "global", + "go", + "goto", + "grant", + "greater", + "group", + "grouping", + "handler", + "hash", + "have", + "having", + "heap", + "hidden", + "hold", + "hour", + "identified", + "identity", + "if", + "ignore", + "immediate", + "import", + "in", + "including", + "inclusive", + "increment", + "incremental", + "index", + "indexed", + "indexes", + "indicator", + "infinite", + "initially", + "inline", + "inner", + "innter", + "inout", + "input", + "insensitive", + "insert", + "instead", + "int", + "integer", + "intersect", + "interval", + "into", + "invalidate", + "is", + "isolation", + "item", + "items", + "iterate", + "join", + "key", + "keys", + "lag", + "language", + "large", + "last", + "lateral", + "lead", + "leading", + "leave", + "left", + "length", + "less", + "level", + "like", + "limit", + "limited", + "lines", + "list", + "load", + "local", + "localtime", + "localtimestamp", + "location", + "locator", + "lock", + "locks", + "log", + "loged", + "long", + "loop", + "lower", + "map", + "match", + "materialized", + "max", + "maxlen", + "member", + "merge", + "method", + "metrics", + "min", + "minus", + "minute", + "missing", + "mod", + "mode", + "modifies", + "modify", + "module", + "month", + "multi", + "multiset", + "name", + "names", + "national", + "natural", + "nchar", + "nclob", + "new", + "next", + "no", + "none", + "not", + "null", + "nullif", + "number", + "numeric", + "object", + "of", + "offline", + "offset", + "old", + "on", + "online", + "only", + "opaque", + "open", + "operator", + "option", + "or", + "order", + "ordinality", + "other", + "others", + "out", + "outer", + "output", + "over", + "overlaps", + "override", + "owner", + "pad", + "parallel", + "parameter", + "parameters", + "partial", + "partition", + "partitioned", + "partitions", + "path", + "percent", + "percentile", + "permission", + "permissions", + "pipe", + "pipelined", + "plan", + "pool", + "position", + "precision", + "prepare", + "preserve", + "primary", + "prior", + "private", + "privileges", + "procedure", + "processed", + "project", + "projection", + "property", + "provisioning", + "public", + "put", + "query", + "quit", + "quorum", + "raise", + "random", + "range", + "rank", + "raw", + "read", + "reads", + "real", + "rebuild", + "record", + "recursive", + "reduce", + "ref", + "reference", + "references", + "referencing", + "regexp", + "region", + "reindex", + "relative", + "release", + "remainder", + "rename", + "repeat", + "replace", + "request", + "reset", + "resignal", + "resource", + "response", + "restore", + "restrict", + "result", + "return", + "returning", + "returns", + "reverse", + "revoke", + "right", + "role", + "roles", + "rollback", + "rollup", + "routine", + "row", + "rows", + "rule", + "rules", + "sample", + "satisfies", + "save", + "savepoint", + "scan", + "schema", + "scope", + "scroll", + "search", + "second", + "section", + "segment", + "segments", + "select", + "self", + "semi", + "sensitive", + "separate", + "sequence", + "serializable", + "session", + "set", + "sets", + "shard", + "share", + "shared", + "short", + "show", + "signal", + "similar", + "size", + "skewed", + "smallint", + "snapshot", + "some", + "source", + "space", + "spaces", + "sparse", + "specific", + "specifictype", + "split", + "sql", + "sqlcode", + "sqlerror", + "sqlexception", + "sqlstate", + "sqlwarning", + "start", + "state", + "static", + "status", + "storage", + "store", + "stored", + "stream", + "string", + "struct", + "style", + "sub", + "submultiset", + "subpartition", + "substring", + "subtype", + "sum", + "super", + "symmetric", + "synonym", + "system", + "table", + "tablesample", + "temp", + "temporary", + "terminated", + "text", + "than", + "then", + "throughput", + "time", + "timestamp", + "timezone", + "tinyint", + "to", + "token", + "total", + "touch", + "trailing", + "transaction", + "transform", + "translate", + "translation", + "treat", + "trigger", + "trim", + "true", + "truncate", + "ttl", + "tuple", + "type", + "under", + "undo", + "union", + "unique", + "unit", + "unknown", + "unlogged", + "unnest", + "unprocessed", + "unsigned", + "until", + "update", + "upper", + "url", + "usage", + "use", + "user", + "users", + "using", + "uuid", + "vacuum", + "value", + "valued", + "values", + "varchar", + "variable", + "variance", + "varint", + "varying", + "view", + "views", + "virtual", + "void", + "wait", + "when", + "whenever", + "where", + "while", + "window", + "with", + "within", + "without", + "work", + "wrapped", + "write", + "year", + "zone", +) + +private val String.attributeNameNeedsEscaping: Boolean + get() = contains(attributeNameNeedsEscapingRegex) + +private val attributeNameNeedsEscapingRegex = buildString { + append('(') + + // Alternative 1: entire string is a reserved word + reservedWords.joinTo(this, separator = "|", prefix = "^(", postfix = ")$") + + // OR + append('|') + + // Alternative 2: string contains any non-alphanumeric character anywhere + append("[^A-Za-z0-9]") + + append(')') +}.toRegex(RegexOption.IGNORE_CASE) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ScalarFuncExprImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ScalarFuncExprImpl.kt new file mode 100644 index 00000000000..189d86806cd --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/ScalarFuncExprImpl.kt @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.* + +internal data class ScalarFuncExprImpl( + override val func: ScalarFunc, + override val path: AttributePath, + override val additionalOperands: List = listOf(), +) : ScalarFuncExpr diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/SortKeyFilterImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/SortKeyFilterImpl.kt new file mode 100644 index 00000000000..944bbfb5c6b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/expressions/internal/SortKeyFilterImpl.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.* + +private data object SortKeyImpl : SortKey + +internal data object SortKeyFilterImpl : SortKeyFilter { + override val sortKey: SortKey + get() = SortKeyImpl + + override infix fun SortKey.eq(expr: LiteralExpr) = + ComparisonExpr(Comparator.EQUALS, SkAttrPathImpl, expr) + + override infix fun SortKey.neq(expr: LiteralExpr) = + ComparisonExpr(Comparator.NOT_EQUALS, SkAttrPathImpl, expr) + + override infix fun SortKey.lt(expr: LiteralExpr) = + ComparisonExpr(Comparator.LESS_THAN, SkAttrPathImpl, expr) + + override infix fun SortKey.lte(expr: LiteralExpr) = + ComparisonExpr(Comparator.LESS_THAN_OR_EQUAL, SkAttrPathImpl, expr) + + override infix fun SortKey.gt(expr: LiteralExpr) = + ComparisonExpr(Comparator.GREATER_THAN, SkAttrPathImpl, expr) + + override infix fun SortKey.gte(expr: LiteralExpr) = + ComparisonExpr(Comparator.GREATER_THAN_OR_EQUAL, SkAttrPathImpl, expr) + + override fun SortKey.isBetween(min: LiteralExpr, max: LiteralExpr) = + BetweenExpr(SkAttrPathImpl, min, max) + + override infix fun SortKey.startsWith(expr: LiteralExpr) = + BooleanFuncExpr(BooleanFunc.BEGINS_WITH, SkAttrPathImpl, expr) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/internal/DynamoDbMapperImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/internal/DynamoDbMapperImpl.kt new file mode 100644 index 00000000000..8276aeb0f1b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/internal/DynamoDbMapperImpl.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.internal.tableImpl +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.InterceptorAny +import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import aws.sdk.kotlin.services.dynamodb.withConfig +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.RequestInterceptorContext +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.io.use + +internal data class DynamoDbMapperImpl( + override val client: DynamoDbClient, + override val config: DynamoDbMapper.Config, +) : DynamoDbMapper { + override fun getTable(name: String, schema: ItemSchema.PartitionKey) = + tableImpl(this, name, schema) + + override fun getTable(name: String, schema: ItemSchema.CompositeKey) = + tableImpl(this, name, schema) +} + +internal data class MapperConfigImpl( + override val interceptors: List, +) : DynamoDbMapper.Config { + override fun toBuilder() = DynamoDbMapper + .Config + .Builder() + .also { it.interceptors = interceptors.toMutableList() } +} + +internal class MapperConfigBuilderImpl : DynamoDbMapper.Config.Builder { + override var interceptors = mutableListOf() + + override fun build() = MapperConfigImpl(interceptors.toList()) +} + +/** + * An interceptor that emits the DynamoDB Mapper business metric + */ +private object BusinessMetricInterceptor : HttpInterceptor { + override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext): Any { + context.executionContext.emitBusinessMetric(AwsBusinessMetric.DDB_MAPPER) + return context.request + } +} + +internal inline fun DynamoDbClient.withWrappedClient(block: (DynamoDbClient) -> T): T = + withConfig { interceptors += BusinessMetricInterceptor }.use(block) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor.kt new file mode 100644 index 00000000000..4ffd08ec55c --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/AttributeDescriptor.kt @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.items.internal.AttributeDescriptorImpl +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Describes a single item attribute and how it is converted from an object of type [T] and to a build object of type + * [B]. + * @param A The type of value extracted by [getter], accepted by [setter], and used by [converter] + * @param T The type of object from which values are extracted + * @param B The type of builder object in which values are mutated + */ +@ExperimentalApi +public interface AttributeDescriptor { + /** + * The name of the attribute + */ + public val name: String + + /** + * A function which extracts a value of type [A] from an object of type [T] + */ + public val getter: (T) -> A + + /** + * A function which operates on a builder of type [B] and mutates a value of type [A] + */ + public val setter: B.(A) -> Unit + + /** + * A [ValueConverter] which defines how an object value is converted to an attribute value and vice versa + */ + public val converter: ValueConverter +} + +/** + * Instantiates a new [AttributeDescriptor] + * @param A The type of value extracted by [getter], accepted by [setter], and used by [converter] + * @param T The type of object from which values are extracted + * @param B The type of builder object in which values are mutated + * @param name The name of the attribute + * @param getter A function which extracts a value of type [A] from an object of type [T] + * @param setter A function which operates on a builder of type [B] and mutates a value of type [A] + * @param converter A [ValueConverter] which defines how an object value is converted to an attribute value and vice + * versa + */ +@ExperimentalApi +public fun AttributeDescriptor( + name: String, + getter: (T) -> A, + setter: B.(A) -> Unit, + converter: ValueConverter, +): AttributeDescriptor = AttributeDescriptorImpl(name, getter, setter, converter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/DocumentConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/DocumentConverter.kt new file mode 100644 index 00000000000..b5ae1f3699c --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/DocumentConverter.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.toItem +import aws.sdk.kotlin.hll.dynamodbmapper.util.NULL_ATTR +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.content.Document +import aws.smithy.kotlin.runtime.util.toNumber + +// FIXME Combine with DocumentValueConverter or refactor to commonize as much code as possible +@ExperimentalApi +public object DocumentConverter : ItemConverter { + override fun convertFrom(to: Item): Document = to + .mapValues { (_, attr) -> fromAttributeValue(attr) } + .let(Document::Map) + + override fun convertTo(from: Document, onlyAttributes: Set?): Item { + require(from is Document.Map) + + val map = if (onlyAttributes == null) { + from + } else { + from.filterKeys { it in onlyAttributes } + } + + return map.mapValues { (_, value) -> toAttributeValue(value) }.toItem() + } +} + +@OptIn(InternalApi::class) +private fun fromAttributeValue(attr: AttributeValue): Document? = when (attr) { + is AttributeValue.Null -> null + is AttributeValue.N -> Document.Number(attr.value.toNumber()!!) // FIXME need better toNumber logic + is AttributeValue.S -> Document.String(attr.value) + is AttributeValue.Bool -> Document.Boolean(attr.value) + is AttributeValue.L -> Document.List(attr.value.map(::fromAttributeValue)) + is AttributeValue.M -> Document.Map(attr.value.mapValues { (_, nestedValue) -> fromAttributeValue(nestedValue) }) + else -> error("Documents do not support ${attr::class.qualifiedName}") +} + +private fun toAttributeValue(value: Document?): AttributeValue = when (value) { + null -> NULL_ATTR + is Document.Number -> AttributeValue.N(value.value.toString()) + is Document.String -> AttributeValue.S(value.value) + is Document.Boolean -> AttributeValue.Bool(value.value) + is Document.List -> AttributeValue.L(value.value.map(::toAttributeValue)) + is Document.Map -> AttributeValue.M(value.mapValues { (_, nestedValue) -> toAttributeValue(nestedValue) }) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/HeterogeneousItemConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/HeterogeneousItemConverter.kt new file mode 100644 index 00000000000..39bfbbf9801 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/HeterogeneousItemConverter.kt @@ -0,0 +1,123 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.buildItem +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * An item converter which handles heterogeneous (i.e., incongruent) data types by way of a string discriminator + * attribute identified by [typeAttribute]. The given [typeMapper] function must return a string type name for an object + * which will be used for the [typeAttribute] attribute. Finally, the given [subConverters] map identifies the delegate + * converters for each type. + * + * This converter is particularly (although not _solely_) useful for mapping polymorphic structures. For example, given + * a class hierarchy: + * + * ```kotlin + * sealed interface Vehicle + * + * @DynamoDbItem + * data class Car( + * @DynamoDbPartitionKey val id: Int, + * val manufacturer: String, + * val model: String, + * val year: Int, + * ) : Vehicle + * + * @DynamoDbItem + * data class Bike( + * @DynamoDbPartitionKey val id: Int, + * val manufacturer: String + * val gears: Int, + * val isElectric: Boolean, + * ) : Vehicle + * ``` + * + * A heterogeneous item converter can be constructed: + * + * ```kotlin + * fun vehicleType(obj: Vehicle) = when (obj) { + * is Car -> "car" + * is Bike -> "bike" + * } + * + * val vehicleConverter = HeterogeneousItemConverter( + * typeMapper = ::vehicleType, + * typeAttribute = "type", + * subConverters = mapOf( + * "car" to CarConverter, + * "bike" to BikeConverter, + * ), + * ) + * ``` + * + * Objects mapped in this manner will use only the attributes relevant to their specific type, plus the [typeAttribute]. + * For example, given the following items and PutItem calls: + * + * ```kotlin + * val vehicles = listOf( + * Car(1, "Ford", "Model T", 1928), + * Bike(2, "Schwinn", 10, false), + * Car(3, "Edsel", "Corsair", 1958), + * Bike(4, "Kuwahara", 1, false), + * ) + * + * val table = ... // some table which uses the vehicleConverter from above in its schema + * + * vehicles.forEach { vehicle -> + * table.putItem { item = vehicle } + * } + * ``` + * + * Items would be persisted in the table as: + * + * | **id** | **type** | **manufacturer** | **model** | **year** | **gears** | **isElectric** | + * |-------:|----------|------------------|-----------|---------:|----------:|----------------| + * | 1 | car | Ford | Model T | 1928 | | | + * | 2 | bike | Schwinn | | | 10 | false | + * | 3 | car | Edsel | Corsair | 1958 | | | + * | 4 | bike | Kuwahara | | | 1 | false | + * + * @param T The common type ancestor for all subtypes handled by this converter. This may be a base class, interface, or + * even [Any]. + * @param typeMapper A function which accepts an instance of the common type [T] and returns the string identifier for + * the type. This identifier is written/read from the attribute identified by [typeAttribute] and used as a lookup key + * in [subConverters]. + * @param typeAttribute The name of the attribute in which to store/read type information. This attribute will be + * present for every item persisted via this converter. It should ideally be an attribute which doesn't conflict with + * other attributes used by subconverters. + * @param subConverters A map of type names (the same returned by [typeMapper]) to [ItemConverter] instances. If the + * [typeMapper] function returns a type name which does not exist in this map, or if an item is read containing a type + * attribute value which does not exist in this map, an exception will be thrown. + */ +@ExperimentalApi +public class HeterogeneousItemConverter( + public val typeMapper: (T) -> String, + public val typeAttribute: String, + public val subConverters: Map>, +) : ItemConverter { + override fun convertFrom(to: Item): T { + val attr = to[typeAttribute] ?: error("Missing $typeAttribute") + val typeValue = attr.asSOrNull() ?: error("No string value for $attr") + val converter = subConverters[typeValue] ?: error("No converter for $typeValue") + return converter.convertFrom(to) + } + + override fun convertTo(from: T, onlyAttributes: Set?): Item { + val typeValue = typeMapper(from) + val converter = subConverters[typeValue] ?: error("No converter for $typeValue") + + return buildItem { + if (onlyAttributes?.contains(typeAttribute) != false) { + put(typeAttribute, AttributeValue.S(typeValue)) + } + + putAll(converter.convertTo(from, onlyAttributes)) + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter.kt new file mode 100644 index 00000000000..be343fc4ae0 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemConverter.kt @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Defines the logic for converting between objects and DynamoDB items + * @param T The type of objects which will be converted + */ +@ExperimentalApi +public interface ItemConverter : Converter { + public fun convertTo(from: T, onlyAttributes: Set? = null): Item + public override fun convertTo(from: T): Item = convertTo(from, null) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema.kt new file mode 100644 index 00000000000..a5c42b46acc --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/ItemSchema.kt @@ -0,0 +1,113 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.items.internal.ItemSchemaCompositeKeyImpl +import aws.sdk.kotlin.hll.dynamodbmapper.items.internal.ItemSchemaPartitionKeyImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Defines a schema for handling objects of a certain type, including an [ItemConverter] for converting between objects + * items and a [KeySpec] for identifying primary keys. + * @param T The type of objects described by this schema + */ +@ExperimentalApi +public interface ItemSchema { + /** + * The [ItemConverter] used to convert between objects and items + */ + public val converter: ItemConverter + + /** + * The name(s) of the attributes which form the primary key of this table + */ + public val keyAttributeNames: Set + + /** + * Represents a schema with a primary key consisting of a single partition key + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface PartitionKey : ItemSchema { + /** + * The [KeySpec] for the partition key + */ + public val partitionKey: KeySpec + + override val keyAttributeNames: Set + get() = setOf(partitionKey.name) + } + + /** + * Represents a schema with a primary key that is a composite of a partition key and a sort key + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface CompositeKey : PartitionKey { + /** + * The [KeySpec] for the sort key + */ + public val sortKey: KeySpec + + override val keyAttributeNames: Set + get() = setOf(partitionKey.name, sortKey.name) + } +} + +/** + * Create a new item schema with a primary key consisting of a single partition key. + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param converter The [ItemConverter] used to convert between objects and items + * @param partitionKey The [KeySpec] for the partition key + */ +@ExperimentalApi +@Suppress("FunctionName") +public fun ItemSchema(converter: ItemConverter, partitionKey: KeySpec): ItemSchema.PartitionKey = + ItemSchemaPartitionKeyImpl(converter, partitionKey) + +/** + * Create a new item schema with a primary key consisting of a single partition key. + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + * @param converter The [ItemConverter] used to convert between objects and items + * @param partitionKey The [KeySpec] for the partition key + * @param sortKey The [KeySpec] for the sort key + */ +@ExperimentalApi +@Suppress("FunctionName") +public fun ItemSchema( + converter: ItemConverter, + partitionKey: KeySpec, + sortKey: KeySpec, +): ItemSchema.CompositeKey = ItemSchemaCompositeKeyImpl(converter, partitionKey, sortKey) + +/** + * Associate this [ItemConverter] with a [KeySpec] for a partition key to form a complete [ItemSchema] + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param partitionKey The [KeySpec] that describes the partition key + */ +@ExperimentalApi +public fun ItemConverter.withKeySpec(partitionKey: KeySpec): ItemSchema.PartitionKey = + ItemSchema(this, partitionKey) + +/** + * Associate this [ItemConverter] with [KeySpec] instances for a composite key to form a complete [ItemSchema] + * @param T The type of objects described by this schema + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + * @param partitionKey The [KeySpec] that describes the partition key + * @param sortKey The [KeySpec] that describes the sort key + */ +@ExperimentalApi +public fun ItemConverter.withKeySpec( + partitionKey: KeySpec, + sortKey: KeySpec, +): ItemSchema.CompositeKey = ItemSchema(this, partitionKey, sortKey) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec.kt new file mode 100644 index 00000000000..9cf119b4fc7 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/KeySpec.kt @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Defines a specification for a single key attribute + * @param K The type of the key property, either [kotlin.String], [kotlin.Number], or [kotlin.ByteArray] + */ +@ExperimentalApi +public sealed interface KeySpec { + /** + * A [KeySpec] for a [kotlin.ByteArray]-typed field + */ + @ExperimentalApi + public interface ByteArray : KeySpec + + /** + * A [KeySpec] for a [kotlin.Number]-typed field + */ + @ExperimentalApi + public interface Number : KeySpec + + /** + * A [KeySpec] for a [kotlin.String]-typed field + */ + @ExperimentalApi + public interface String : KeySpec + + @ExperimentalApi + public companion object { + /** + * Creates a new [ByteArray] key specification + * @param name The name of the key attribute + */ + public fun ByteArray(name: kotlin.String): KeySpec.ByteArray = ByteArrayImpl(name) + + /** + * Creates a new [Number] key specification + * @param name The name of the key attribute + */ + public fun Number(name: kotlin.String): KeySpec.Number = NumberImpl(name) + + /** + * Creates a new [String] key specification + * @param name The name of the key attribute + */ + public fun String(name: kotlin.String): KeySpec.String = StringImpl(name) + } + + /** + * The name of the key attribute + */ + public val name: kotlin.String + + /** + * Given a value for this key attribute, convert into a field + * @param value The value to use for the key attribute + */ + public fun toField(value: K): Pair +} + +private data class ByteArrayImpl(override val name: String) : KeySpec.ByteArray { + override fun toField(value: ByteArray) = name to AttributeValue.B(value) +} + +private data class NumberImpl(override val name: String) : KeySpec.Number { + override fun toField(value: Number) = name to AttributeValue.N(value.toString()) +} + +private data class StringImpl(override val name: String) : KeySpec.String { + override fun toField(value: String) = name to AttributeValue.S(value) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverter.kt new file mode 100644 index 00000000000..9e4d0fb457d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverter.kt @@ -0,0 +1,87 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.buildItem +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * An item converter which uses attribute descriptors to convert objects to items and vice versa. This converter + * distinguishes between the object type used for conversion into an item (treated as immutable) and a mutable builder + * type used for conversion from an item. Note that these do not have to be different types if the object type is + * already mutable. + * @param T The type of objects which will be converted to items. This converter treats type [T] as immutable. + * @param B The type of builders which will be used when converting from items. + * @param builderFactory A function which returns a new, default-valued builder object. This is invoked once for every + * item which must be converted to an object. + * @param build A method which builds a builder object of type [B] into its final representation of type [T]. If [B] and + * [T] are the same type, this may be an identity function. + * @param descriptors A collection of [AttributeDescriptor] which describe how to construct and parse attributes + */ +@ExperimentalApi +public class SimpleItemConverter( + private val builderFactory: () -> B, + private val build: B.() -> T, + vararg descriptors: AttributeDescriptor<*, T, B>, +) : ItemConverter { + public val descriptors: Map> = descriptors + .groupBy { it.name } + .mapValues { (name, descriptor) -> + requireNotNull(descriptor.singleOrNull()) { + """Multiple AttributeDescriptor instances for attribute "$name"""" + } + } + + override fun convertFrom(to: Item): T { + val builder = builderFactory() + + /** + * This is a convenience function to keep the compile-time safety for type param `A`. Without this, the compiler + * can't track generic types across multiple statements: + * + * ```kotlin + * val descriptor = descriptors[name] // AttributeDescriptor<*, T, B> + * val value = descriptor.converter.fromAttributeValue(av) // Any? + * descriptor.setter(builder, value) // Type mismatch for value. Required: Nothing, Found: Any? + * ``` + */ + fun AttributeDescriptor.fromAttributeValue(attr: AttributeValue) = + builder.setter(converter.convertFrom(attr)) + + to.forEach { (name, attr) -> + // TODO make behavior for unknown attributes configurable (ignore, exception, other?) + descriptors[name]?.fromAttributeValue(attr) + } + + return builder.build() + } + + override fun convertTo(from: T, onlyAttributes: Set?): Item { + /** + * This is a convenience function to keep the compile-time safety for type param `A`. Without this, the compiler + * can't track generic types across multiple statements: + * + * ```kotlin + * val descriptor = descriptors[name] // AttributeDescriptor<*, T, B> + * val value = descriptor.getter(obj) // Any? + * descriptor.converter.toAttributeValue(value) // Type mismatch for value. Required: Nothing, Found: Any? + * ``` + */ + fun AttributeDescriptor.toAttributeValue() = + converter.convertTo(getter(from)) + + val descriptors = if (onlyAttributes == null) { + this.descriptors.values + } else { + this.descriptors.filterKeys(onlyAttributes::contains).values + } + + return buildItem { + descriptors.forEach { desc -> put(desc.name, desc.toAttributeValue()) } + } + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/AttributeDescriptorImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/AttributeDescriptorImpl.kt new file mode 100644 index 00000000000..c5eaaf89100 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/AttributeDescriptorImpl.kt @@ -0,0 +1,15 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter + +internal data class AttributeDescriptorImpl( + override val name: String, + override val getter: (T) -> A, + override val setter: B.(A) -> Unit, + override val converter: ValueConverter, +) : AttributeDescriptor diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/ItemSchemaImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/ItemSchemaImpl.kt new file mode 100644 index 00000000000..789a917e2c0 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/items/internal/ItemSchemaImpl.kt @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec + +internal data class ItemSchemaPartitionKeyImpl( + override val converter: ItemConverter, + override val partitionKey: KeySpec, +) : ItemSchema.PartitionKey + +internal data class ItemSchemaCompositeKeyImpl( + override val converter: ItemConverter, + override val partitionKey: KeySpec, + override val sortKey: KeySpec, +) : ItemSchema.CompositeKey diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Index.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Index.kt new file mode 100644 index 00000000000..6d652224b69 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Index.kt @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.operations.IndexOperations +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a secondary index on a table in DynamoDB and an associated item schema. Operations on this index will + * invoke low-level operations and map items to objects. + * @param T The type of objects which will be read from this index + */ +@ExperimentalApi +public interface Index : + IndexSpec, + IndexOperations, + ItemSource { + + /** + * Represents a secondary index whose primary key is a single partition key + * @param T The type of objects which will be read from this index + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface PartitionKey : + Index, + PersistenceSpec.PartitionKey + + /** + * Represents a secondary index whose primary key is a composite of a partition key and a sort key + * @param T The type of objects which will be read from this index + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface CompositeKey : + Index, + PersistenceSpec.CompositeKey +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec.kt new file mode 100644 index 00000000000..b92b1bb3b36 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/IndexSpec.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Specifies how items can be read from a secondary index + * @param T The type of objects which will be read from this index + */ +@ExperimentalApi +public interface IndexSpec : PersistenceSpec { + /** + * The name of the table + */ + public val tableName: String + + /** + * The name of the secondary index + */ + public val indexName: String? + + /** + * Specifies how items can be read from a secondary index whose primary key consists of a single partition key + */ + @ExperimentalApi + public interface PartitionKey : IndexSpec { + override val schema: ItemSchema.PartitionKey + } + + /** + * Specifies how items can be read from a secondary index whose primary key consists of a composite of a partition + * key and a sort key + */ + @ExperimentalApi + public interface CompositeKey : IndexSpec { + override val schema: ItemSchema.CompositeKey + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Item.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Item.kt new file mode 100644 index 00000000000..3341b58ada1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Item.kt @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.model.internal.ItemImpl +import aws.sdk.kotlin.hll.dynamodbmapper.util.dynamicAttr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import kotlin.jvm.JvmName + +/** + * An immutable representation of a low-level item in a DynamoDB table. Items consist of attributes, each of which have + * a string name and a value. + */ +@ExperimentalApi +public interface Item : Map + +/** + * Builds a new immutable [Item] by populating a [MutableItem] using the provided [block] and returning a read-only copy + * of it. + * @param block The block to apply to the [MutableItem] builder + */ +@ExperimentalApi +public inline fun buildItem(block: MutableItem.() -> Unit): Item = + mutableMapOf().toMutableItem().apply(block).toItem() + +/** + * Convert this [Item] into a [MutableItem]. Changes to the returned instance do not affect this instance. + */ +@ExperimentalApi +public fun Item.toMutableItem(): MutableItem = toMutableMap().toMutableItem() + +/** + * Converts this map to an immutable [Item] + */ +@ExperimentalApi +public fun Map.toItem(): Item = ItemImpl(this) + +/** + * Dynamically converts this map to an immutable [Item] + */ +@JvmName("mapStringAnyToItem") +internal fun Map.toItem() = mapValues { (_, v) -> dynamicAttr(v) }.toItem() + +/** + * Returns a new immutable [Item] with the specified attributes, given as name-value pairs + * @param pairs A collection of [Pair]<[String], [AttributeValue]> where the first value is the attribute name and the + * second is the attribute value. + */ +@ExperimentalApi +public fun itemOf(vararg pairs: Pair): Item = mapOf(*pairs).toItem() + +/** + * Returns a new immutable [Item] with the specified attributes, given as name-value pairs + * @param pairs A collection of [Pair]<[String], [Any]`?`> where the first value is the attribute name and the + * second is the attribute value. + */ +@JvmName("itemOfPairStringAny") +internal fun itemOf(vararg pairs: Pair): Item = mapOf(*pairs).toItem() diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource.kt new file mode 100644 index 00000000000..a9141c17c04 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/ItemSource.kt @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.operations.ItemSourceOperations +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a source of DynamoDB items (such as a table or secondary index) + */ +@ExperimentalApi +public interface ItemSource : + PersistenceSpec, + ItemSourceOperations { + + /** + * Represents a source of DynamoDB items (such as a table or secondary index) whose primary key is a single + * partition key + * @param T The type of objects which will be read from and/or written to this item source + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface PartitionKey : + ItemSource, + PersistenceSpec.PartitionKey + + /** + * Represents a source of DynamoDB items (such as a table or secondary index) whose primary key is a composite of a + * partition key and a sort key + * @param T The type of objects which will be read from and/or written to this item source + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface CompositeKey : + ItemSource, + PersistenceSpec.CompositeKey +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem.kt new file mode 100644 index 00000000000..0b0bbd0e2c6 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/MutableItem.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.model.internal.MutableItemImpl +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * A mutable representation of a low-level item in a DynamoDB table. Items consist of attributes, each of which have a + * string name and a value. + */ +@ExperimentalApi +public interface MutableItem : MutableMap + +/** + * Convert this [MutableItem] to an immutable [Item]. Changes to this instance do not affect the returned instance. + */ +@ExperimentalApi +public fun MutableItem.toItem(): Item = toMap().toItem() + +/** + * Converts this map to a [MutableItem] + */ +@ExperimentalApi +public fun MutableMap.toMutableItem(): MutableItem = MutableItemImpl(this) + +/** + * Returns a new immutable [Item] with the specified attributes, given as name-value pairs + * @param pairs A collection of [Pair]<[String], [AttributeValue]> where the first value is the attribute name and the + * second is the attribute value. + */ +@ExperimentalApi +public fun mutableItemOf(vararg pairs: Pair): MutableItem = + MutableItemImpl(mutableMapOf(*pairs)) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec.kt new file mode 100644 index 00000000000..452b571040c --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/PersistenceSpec.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Specifies how items can be read from and written to a specific DynamoDB location (such as a table or a secondary + * index) + * @param T The type of objects which will be read from and/or written to this item source + */ +@ExperimentalApi +public interface PersistenceSpec { + /** + * The [DynamoDbMapper] which holds the underlying DynamoDB service client used to invoke operations + */ + public val mapper: DynamoDbMapper + + /** + * The [ItemSchema] which describes how to map objects to items and vice versa + */ + public val schema: ItemSchema + + /** + * Specifies how items can be read from and written to a specific DynamoDB location (such as a table or a secondary + * index) whose primary key consists of a single partition key + */ + @ExperimentalApi + public interface PartitionKey : PersistenceSpec { + override val schema: ItemSchema.PartitionKey + } + + /** + * Specifies how items can be read from and written to a specific DynamoDB location (such as a table or a secondary + * index) whose primary key consists of a composite of a partition key and a sort key + */ + @ExperimentalApi + public interface CompositeKey : PersistenceSpec { + override val schema: ItemSchema.CompositeKey + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Table.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Table.kt new file mode 100644 index 00000000000..fd96d1554cd --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/Table.kt @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.TableOperations +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a table in DynamoDB and an associated item schema. Operations on this table will invoke low-level + * operations after mapping objects to items and vice versa. + * @param T The type of objects which will be read from and/or written to this table + */ +@ExperimentalApi +public interface Table : + TableSpec, + TableOperations, + ItemSource { + + /** + * Represents a table whose primary key is a single partition key + * @param T The type of objects which will be read from and/or written to this table + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface PartitionKey : + Table, + ItemSource.PartitionKey { + // TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc. + public suspend fun getItem(partitionKey: PK): T? + } + + /** + * Represents a table whose primary key is a composite of a partition key and a sort key + * @param T The type of objects which will be read from and/or written to this table + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + */ + @ExperimentalApi + public interface CompositeKey : + Table, + ItemSource.CompositeKey { + // TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc. + public suspend fun getItem(partitionKey: PK, sortKey: SK): T? + } + + /** + * Get an [Index] reference for performing secondary index operations + * @param T The type of objects which will be read from to this index + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param name The name of the index + * @param schema The [ItemSchema] which describes the index, its keys, and how items are converted + */ + public fun getIndex( + name: String, + schema: ItemSchema.PartitionKey, + ): Index.PartitionKey + + /** + * Get an [Index] reference for performing secondary index operations + * @param T The type of objects which will be read from this index + * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] + * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] + * @param name The name of the index + * @param schema The [ItemSchema] which describes the index, its keys, and how items are converted + */ + public fun getIndex( + name: String, + schema: ItemSchema.CompositeKey, + ): Index.CompositeKey +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec.kt new file mode 100644 index 00000000000..a9d78371c70 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/TableSpec.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Specifies how items can be read from and written to a table + * @param T The type of objects which will be read from and/or written to this table + */ +@ExperimentalApi +public interface TableSpec : PersistenceSpec { + /** + * The name of the table + */ + public val tableName: String + + /** + * Specifies how items can be read from or written to a table whose primary key consists of a single partition key + */ + @ExperimentalApi + public interface PartitionKey : TableSpec { + override val schema: ItemSchema.PartitionKey + } + + /** + * Specifies how items can be read from or written to a table whose primary key consists of a composite of a + * partition key and a sort key + */ + @ExperimentalApi + public interface CompositeKey : TableSpec { + override val schema: ItemSchema.CompositeKey + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexImpl.kt new file mode 100644 index 00000000000..bd2acd02445 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexImpl.kt @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.Index +import aws.sdk.kotlin.hll.dynamodbmapper.model.IndexSpec +import aws.sdk.kotlin.hll.dynamodbmapper.operations.IndexOperations +import aws.sdk.kotlin.hll.dynamodbmapper.operations.IndexOperationsImpl + +internal fun indexImpl( + mapper: DynamoDbMapper, + tableName: String, + indexName: String, + schema: ItemSchema.PartitionKey, +): Index.PartitionKey { + val specImpl = IndexSpecPartitionKeyImpl(mapper, tableName, indexName, schema) + val opsImpl = IndexOperationsImpl(specImpl) + return object : + Index.PartitionKey, + IndexSpec.PartitionKey by specImpl, + IndexOperations by opsImpl { } +} + +internal fun indexImpl( + mapper: DynamoDbMapper, + tableName: String, + indexName: String, + schema: ItemSchema.CompositeKey, +): Index.CompositeKey { + val specImpl = IndexSpecCompositeKeyImpl(mapper, tableName, indexName, schema) + val opsImpl = IndexOperationsImpl(specImpl) + return object : + Index.CompositeKey, + IndexSpec.CompositeKey by specImpl, + IndexOperations by opsImpl { } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexSpecImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexSpecImpl.kt new file mode 100644 index 00000000000..00f58a55867 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/IndexSpecImpl.kt @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.IndexSpec + +internal data class IndexSpecPartitionKeyImpl( + override val mapper: DynamoDbMapper, + override val tableName: String, + override val indexName: String, + override val schema: ItemSchema.PartitionKey, +) : IndexSpec.PartitionKey + +internal data class IndexSpecCompositeKeyImpl( + override val mapper: DynamoDbMapper, + override val tableName: String, + override val indexName: String, + override val schema: ItemSchema.CompositeKey, +) : IndexSpec.CompositeKey diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/ItemImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/ItemImpl.kt new file mode 100644 index 00000000000..1f7e1a58fac --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/ItemImpl.kt @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue + +internal data class ItemImpl(private val delegate: Map) : + Item, + Map by delegate diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/MutableItemImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/MutableItemImpl.kt new file mode 100644 index 00000000000..5ca456c262b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/MutableItemImpl.kt @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.model.MutableItem +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue + +internal data class MutableItemImpl(private val delegate: MutableMap) : + MutableItem, + MutableMap by delegate diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableImpl.kt new file mode 100644 index 00000000000..b6dacc9b90f --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableImpl.kt @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.Index +import aws.sdk.kotlin.hll.dynamodbmapper.model.Table +import aws.sdk.kotlin.hll.dynamodbmapper.model.TableSpec +import aws.sdk.kotlin.hll.dynamodbmapper.operations.TableOperations +import aws.sdk.kotlin.hll.dynamodbmapper.operations.TableOperationsImpl + +internal fun tableImpl( + mapper: DynamoDbMapper, + name: String, + schema: ItemSchema.PartitionKey, +): Table.PartitionKey { + val tableName = name // shadowed below + val specImpl = TableSpecPartitionKeyImpl(mapper, tableName, schema) + val opsImpl = TableOperationsImpl(specImpl) + return object : + Table.PartitionKey, + TableSpec.PartitionKey by specImpl, + TableOperations by opsImpl { + + override fun getIndex( + name: String, + schema: ItemSchema.PartitionKey, + ): Index.PartitionKey = indexImpl(mapper, tableName, name, schema) + + override fun getIndex( + name: String, + schema: ItemSchema.CompositeKey, + ): Index.CompositeKey = indexImpl(mapper, tableName, name, schema) + + override suspend fun getItem(partitionKey: PK) = TODO("not yet implemented") + } +} + +internal fun tableImpl( + mapper: DynamoDbMapper, + name: String, + schema: ItemSchema.CompositeKey, +): Table.CompositeKey { + val specImpl = TableSpecCompositeKeyImpl(mapper, name, schema) + val opsImpl = TableOperationsImpl(specImpl) + return object : + Table.CompositeKey, + TableSpec.CompositeKey by specImpl, + TableOperations by opsImpl { + + override fun getIndex( + name: String, + schema: ItemSchema.PartitionKey, + ): Index.PartitionKey = indexImpl(mapper, tableName, name, schema) + + override fun getIndex( + name: String, + schema: ItemSchema.CompositeKey, + ): Index.CompositeKey = indexImpl(mapper, tableName, name, schema) + + override suspend fun getItem(partitionKey: PK, sortKey: SK) = TODO("Not yet implemented") + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableSpecImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableSpecImpl.kt new file mode 100644 index 00000000000..0e491e037cc --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/model/internal/TableSpecImpl.kt @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.model.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.TableSpec + +internal data class TableSpecPartitionKeyImpl( + override val mapper: DynamoDbMapper, + override val tableName: String, + override val schema: ItemSchema.PartitionKey, +) : TableSpec.PartitionKey + +internal data class TableSpecCompositeKeyImpl( + override val mapper: DynamoDbMapper, + override val tableName: String, + override val schema: ItemSchema.CompositeKey, +) : TableSpec.CompositeKey diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput.kt new file mode 100644 index 00000000000..f572e15e1ae --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/DeserializeInput.kt @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.DeserializeInputImpl +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.sdk.kotlin.services.dynamodb.model.GetItemResponse as LowLevelGetItemResponse + +/** + * Defines input to the deserialization step of the pipeline + * @param T The type of objects being converted to/from DynamoDB items + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + */ +@ExperimentalApi +public interface DeserializeInput { + /** + * The low-level response which is to be deserialized into a high-level response object + */ + public val lowLevelResponse: LRes + + /** + * The [ItemSchema] to use for deserializing items into objects + */ + public val deserializeSchema: ItemSchema +} + +/** + * Creates a new [DeserializeInput] + * @param T The type of objects being converted to/from DynamoDB items + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + * @param lowLevelResponse The low-level response which is to be deserialized into a high-level response object + * @param deserializeSchema The [ItemSchema] to use for deserializing items into objects + */ +@ExperimentalApi +public fun DeserializeInput( + lowLevelResponse: LRes, + deserializeSchema: ItemSchema, +): DeserializeInput = DeserializeInputImpl(lowLevelResponse, deserializeSchema) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext.kt new file mode 100644 index 00000000000..8c8abea9375 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HReqContext.kt @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.HReqContextImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Contextual data for stages in the pipeline dealing with high-level requests (i.e., before serialization) + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + */ +@ExperimentalApi +public interface HReqContext : SerializeInput { + + /** + * Additional, generalized context which may be useful to interceptors + */ + public val mapperContext: MapperContext + + /** + * The most recent error which occurred, if any. If another error occurs while this property is already set, it will + * be replaced with the new error and the old error will be added as a suppression + * (i.e., [Throwable.addSuppressed]). + */ + public val error: Throwable? +} + +/** + * Creates a new [HReqContext] + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param highLevelRequest The high-level request object which is to be serialized into a low-level request object + * @param serializeSchema The [ItemSchema] to use for serializing objects into items + * @param mapperContext Additional, generalized context which may be useful to interceptors + * @param error The most recent error which occurred, if any. Defaults to null. + */ +@ExperimentalApi +public fun HReqContext( + highLevelRequest: HReq, + serializeSchema: ItemSchema, + mapperContext: MapperContext, + error: Throwable? = null, +): HReqContext = HReqContextImpl(highLevelRequest, serializeSchema, mapperContext, error) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext.kt new file mode 100644 index 00000000000..0a6334b8ee2 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/HResContext.kt @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemResponse +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.HResContextImpl +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.sdk.kotlin.services.dynamodb.model.GetItemRequest as LowLevelGetItemRequest +import aws.sdk.kotlin.services.dynamodb.model.GetItemResponse as LowLevelGetItemResponse + +/** + * Contextual data for stages in the pipeline dealing with high-level responses (i.e., after deserialization) + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + * @param HRes The type of high-level response object (e.g., [GetItemResponse]) + */ +@ExperimentalApi +public interface HResContext : LResContext { + /** + * The high-level response to return to the caller + */ + public val highLevelResponse: HRes +} + +/** + * Creates a new [HResContext] + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + * @param highLevelRequest The high-level request object which is to be serialized into a low-level request object + * @param serializeSchema The [ItemSchema] to use for serializing objects into items + * @param mapperContext Additional, generalized context which may be useful to interceptors + * @param lowLevelRequest The low-level request object which is to be used in the low-level operation invocation + * @param lowLevelResponse The low-level response which is to be deserialized into a high-level response object + * @param deserializeSchema The [ItemSchema] to use for deserializing items into objects + * @param highLevelResponse The high-level response to return to the caller + * @param error The most recent error which occurred, if any. Defaults to null. + */ +@ExperimentalApi +public fun HResContext( + highLevelRequest: HReq, + serializeSchema: ItemSchema, + mapperContext: MapperContext, + lowLevelRequest: LReq, + lowLevelResponse: LRes, + deserializeSchema: ItemSchema, + highLevelResponse: HRes, + error: Throwable? = null, +): HResContext = HResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + lowLevelResponse, + deserializeSchema, + highLevelResponse, + error, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor.kt new file mode 100644 index 00000000000..c5de8f1f978 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/Interceptor.kt @@ -0,0 +1,186 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemResponse +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.sdk.kotlin.services.dynamodb.model.GetItemRequest as LowLevelGetItemRequest +import aws.sdk.kotlin.services.dynamodb.model.GetItemResponse as LowLevelGetItemResponse + +/** + * An object which defines hooks that can execute at critical stages of the mapper request pipeline. Callers can use + * these hooks to observe or modify the internal steps for executing a high-level operation. + * + * # Request pipeline + * + * The mapper request pipeline consists of 5 steps: + * * **Initialization**: Setting up the operation pipeline and the gathering initial context + * * **Serialization**: Converting high-level request objects (e.g., [GetItemRequest]) into low-level request objects + * (e.g., [LowLevelGetItemRequest]), which includes converting high-level objects to DynamoDB items consisting of + * attribute names and values + * * **Low-level invocation**: Executing a low-level operation on the underlying [DynamoDbClient], such as + * [DynamoDbClient.getItem] + * * **Deserialization**: Converting low-level response objects (e.g., [LowLevelGetItemResponse]) into high-level + * response objects (e.g., [GetItemResponse]), which includes converting DynamoDB items consisting of attributes names + * and values into high-level objects + * * **Completion**: Finalizing the high-level response to return to the caller or exception to throw + * + * # Hooks + * + * Hooks are interceptor methods which are invoked at stages before or after specific steps in the pipeline. They come + * in two flavors: **read-only** and **modify** (or read-write). For example, [readBeforeInvocation] is a **read-only** + * hook executed in the phase _before_ the **Low-level invocation** step of the pipeline. + * + * **Read-only hooks** are invoked before or after each step in the pipeline (except _before_ **Initialization** and + * _after_ **Completion**). They offer a read-only view of a high-level operation in progress. They may be useful for + * examining the state of an operation for logging, debugging, collecting metrics, etc. Each read-only hook receives a + * context argument and returns [Unit]. + * + * Any exception caught while executing a read-only hook will be added to the context and passed to subsequent + * interceptors hooks in the same phase. Only after _all_ interceptors' read-only hooks for a given phase have completed + * will any exception be thrown. For example, if a mapper has two interceptors **A** and **B** registered, and **A**'s + * [readAfterSerialization] hook throws an exception, it will be added to the context passed to **B**'s + * [readAfterSerialization] hook. After **B**'s [readAfterSerialization] hook has completed, the exception will be + * thrown back to the caller. + * + * **Modify hooks** are invoked before each step in the pipeline (except _before_ **Initialization**). They offer the + * ability to see and modify a high-level operation in progress. They can be used to customize behavior and data in ways + * that mapper configuration and item schemas do not. Each modify hook receives a context argument and returns some + * subset of that context as a result—either modified by the hook or passed-through from the input context. + * + * Any exception caught while executing a modify hook will halt the execution of other interceptors' modify hooks in the + * same phase. The exception will be added to the context and passed to the next read-only hook. Once all interceptors' + * read-only hooks for that phase have finished executing the exception will be thrown. For example, if a mapper has two + * interceptors **A** and **B** registered, and **A**'s [modifyBeforeSerialization] hook throws an exception, **B**'s + * [modifyBeforeSerialization] hook will not be invoked. Interceptors **A** and **B**'s [readAfterSerialization] hook + * will execute, after which the exception will be thrown back to the caller. + * + * # Registration and execution order + * + * Interceptors are registered on [DynamoDbMapper] as configuration. Multiple interceptors may be registered on a single + * mapper. The order in which interceptors are given in mapper config determines the order in which they will be + * executed: + * * For phases _before_ the **Low-level invocation** step, hooks will be executed _in given order_ + * * For phases _after_ the **Low-level invocation** step, hooks will be executed _in reverse order_ + * + * @param T The type of item being serialized + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + * @param HRes The type of high-level response object (e.g., [GetItemResponse]) + */ +@ExperimentalApi +public interface Interceptor { + // Hooks functions are defined in the same order as pipeline execution below: + + // ----------------------------------------------------------------------------------------------------------------- + // Initialization step + // ----------------------------------------------------------------------------------------------------------------- + + /** + * A **read-only** hook that runs _after_ the **Initialization** step + * @param ctx Context containing the high-level request + */ + public fun readAfterInitialization(ctx: HReqContext) { } + + /** + * A **modify** hook that runs _before_ the **Serialization** step + * @param ctx Context containing the high-level request + * @return A [SerializeInput] containing a potentially-modified high-level request and/or [ItemSchema] + */ + public fun modifyBeforeSerialization(ctx: HReqContext): SerializeInput = ctx + + /** + * A **read-only** hook that runs _before_ the **Serialization** step + * @param ctx Context containing the high-level request + */ + public fun readBeforeSerialization(ctx: HReqContext) { } + + // ----------------------------------------------------------------------------------------------------------------- + // Serialization step + // ----------------------------------------------------------------------------------------------------------------- + + /** + * A **read-only** hook that runs _after_ the **Serialization** step + * @param ctx Context containing the high-level request and low-level request, which may be `null` if an exception + * was caught during serialization + */ + public fun readAfterSerialization(ctx: LReqContext) { } + + /** + * A **modify** hook that runs _before_ the **Low-level invocation** step + * @param ctx Context containing the high-level and low-level requests + * @return A potentially-modified low-level request + */ + public fun modifyBeforeInvocation(ctx: LReqContext): LReq = ctx.lowLevelRequest + + /** + * A **read-only** hook that runs _before_ the **Low-level invocation** step + * @param ctx Context containing the high-level and low-level requests + */ + public fun readBeforeInvocation(ctx: LReqContext) { } + + // ----------------------------------------------------------------------------------------------------------------- + // Invocation step + // ----------------------------------------------------------------------------------------------------------------- + + /** + * A **read-only** hook that runs _after_ the **Low-level invocation** step + * @param ctx Context containing the high-level/low-level requests and the low-level response, which may be + * `null` if an exception was caught during low-level invocation + */ + public fun readAfterInvocation(ctx: LResContext) { } + + /** + * A **modify** hook that runs _before_ the **Deserialization** step + * @param ctx Context containing the high-level/low-level requests and the low-level response + * @return A [DeserializeInput] containing a potentially-modified low-level response and/or [ItemSchema] + */ + public fun modifyBeforeDeserialization(ctx: LResContext): DeserializeInput = ctx + + /** + * A **read-only** hook that runs _before_ the **Deserialization** step + * @param ctx Context containing the high-level/low-level requests and the low-level response + */ + public fun readBeforeDeserialization(ctx: LResContext) { } + + // ----------------------------------------------------------------------------------------------------------------- + // Deserialization step + // ----------------------------------------------------------------------------------------------------------------- + + /** + * A **read-only** hook that runs _after_ the **Deserialization** step + * @param ctx Context containing the high-level/low-level requests, the low-level response, and the high-level + * response, which may be `null` if an exception was caught during deserialization + */ + public fun readAfterDeserialization(ctx: HResContext) { } + + /** + * A **modify** hook that runs _before_ the **Completion** step + * @param ctx Context containing the high-level/low-level requests and responses + * @return A potentially-modified high-level response + */ + public fun modifyBeforeCompletion(ctx: HResContext): HRes = ctx.highLevelResponse + + /** + * A **read-only** hook that runs _before_ the **Completion** step + * @param ctx Context containing the high-level/low-level requests and responses + */ + public fun readBeforeCompletion(ctx: HResContext) { } + + // ----------------------------------------------------------------------------------------------------------------- + // Completion step + // ----------------------------------------------------------------------------------------------------------------- +} + +/** + * A universal interceptor which acts on any type of high-level objects, requests, and responses + */ +@ExperimentalApi +public typealias InterceptorAny = Interceptor<*, *, *, *, *> diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext.kt new file mode 100644 index 00000000000..39cf25cc0d1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LReqContext.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.LReqContextImpl +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.sdk.kotlin.services.dynamodb.model.GetItemRequest as LowLevelGetItemRequest + +/** + * Contextual data for stages in the pipeline dealing with low-level requests (i.e., between serialization and low-level + * invocation) + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + */ +@ExperimentalApi +public interface LReqContext : HReqContext { + /** + * The low-level request object which is to be used in the low-level operation invocation + */ + public val lowLevelRequest: LReq +} + +/** + * Creates a new [LReqContext] + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param highLevelRequest The high-level request object which is to be serialized into a low-level request object + * @param serializeSchema The [ItemSchema] to use for serializing objects into items + * @param mapperContext Additional, generalized context which may be useful to interceptors + * @param lowLevelRequest The low-level request object which is to be used in the low-level operation invocation + * @param error The most recent error which occurred, if any. Defaults to null. + */ +@ExperimentalApi +public fun LReqContext( + highLevelRequest: HReq, + serializeSchema: ItemSchema, + mapperContext: MapperContext, + lowLevelRequest: LReq, + error: Throwable? = null, +): LReqContext = LReqContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + error, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext.kt new file mode 100644 index 00000000000..f1e4f9bb065 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/LResContext.kt @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.LResContextImpl +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.sdk.kotlin.services.dynamodb.model.GetItemRequest as LowLevelGetItemRequest +import aws.sdk.kotlin.services.dynamodb.model.GetItemResponse as LowLevelGetItemResponse + +/** + * Contextual data for stages in the pipeline dealing with low-level responses (i.e., between low-level invocation and + * deserialization) + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + */ +@ExperimentalApi +public interface LResContext : + LReqContext, + DeserializeInput + +/** + * Creates a new [LResContext] + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param LReq The type of low-level request object (e.g., [LowLevelGetItemRequest]) + * @param LRes The type of low-level response object (e.g., [LowLevelGetItemResponse]) + * @param highLevelRequest The high-level request object which is to be serialized into a low-level request object + * @param serializeSchema The [ItemSchema] to use for serializing objects into items + * @param mapperContext Additional, generalized context which may be useful to interceptors + * @param lowLevelRequest The low-level request object which is to be used in the low-level operation invocation + * @param lowLevelResponse The low-level response which is to be deserialized into a high-level response object + * @param deserializeSchema The [ItemSchema] to use for deserializing items into objects + * @param error The most recent error which occurred, if any. Defaults to null. + */ +@ExperimentalApi +public fun LResContext( + highLevelRequest: HReq, + serializeSchema: ItemSchema, + mapperContext: MapperContext, + lowLevelRequest: LReq, + lowLevelResponse: LRes, + deserializeSchema: ItemSchema, + error: Throwable? = null, +): LResContext = LResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + lowLevelResponse, + deserializeSchema, + error, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext.kt new file mode 100644 index 00000000000..3115ff7d90b --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/MapperContext.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.model.PersistenceSpec +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.MapperContextImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Holds generalized context which may be useful to interceptors + * @param T The type of objects being converted to/from DynamoDB items + */ +@ExperimentalApi +public interface MapperContext { + // TODO what other fields would be useful in here? + + /** + * The metadata about an operation invocation + */ + public val persistenceSpec: PersistenceSpec + + /** + * The name of the high-level operation being invoked + */ + public val operation: String +} + +/** + * Create a new [MapperContext] + * @param T The type of objects being converted to/from DynamoDB items + * @param persistenceSpec The metadata about an operation invocation + * @param operation The name of the high-level operation being invoked + */ +@ExperimentalApi +public fun MapperContext(persistenceSpec: PersistenceSpec, operation: String): MapperContext = + MapperContextImpl(persistenceSpec, operation) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput.kt new file mode 100644 index 00000000000..b5f644cb098 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/SerializeInput.kt @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.operations.GetItemRequest +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal.SerializeInputImpl +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Defines input to the serialization step of the pipeline + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + */ +@ExperimentalApi +public interface SerializeInput { + /** + * The high-level request object which is to be serialized into a low-level request object + */ + public val highLevelRequest: HReq + + /** + * The [ItemSchema] to use for serializing objects into items + */ + public val serializeSchema: ItemSchema +} + +/** + * Creates a new [SerializeInput] + * @param T The type of objects being converted to/from DynamoDB items + * @param HReq The type of high-level request object (e.g., [GetItemRequest]) + * @param highLevelRequest The high-level request object which is to be serialized into a low-level request object + * @param serializeSchema The [ItemSchema] to use for serializing objects into items + */ +@ExperimentalApi +public fun SerializeInput(highLevelRequest: HReq, serializeSchema: ItemSchema): SerializeInput = + SerializeInputImpl(highLevelRequest, serializeSchema) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/ContextUtils.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/ContextUtils.kt new file mode 100644 index 00000000000..71287e2ad8c --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/ContextUtils.kt @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +internal interface Combinable { + operator fun plus(value: V): T +} + +internal interface ErrorCombinable { + val error: Throwable? + operator fun plus(e: Throwable?): T +} + +internal fun Throwable?.suppressing(e: Throwable?) = this?.apply { e?.let(::addSuppressed) } ?: e diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/DeserializeInputImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/DeserializeInputImpl.kt new file mode 100644 index 00000000000..9b8fae766ac --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/DeserializeInputImpl.kt @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.DeserializeInput + +internal data class DeserializeInputImpl( + override val lowLevelResponse: LRes, + override val deserializeSchema: ItemSchema, +) : DeserializeInput diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HReqContextImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HReqContextImpl.kt new file mode 100644 index 00000000000..ea3a33fe6e0 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HReqContextImpl.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.HReqContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.MapperContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.SerializeInput + +internal data class HReqContextImpl( + override val highLevelRequest: HReq, + override val serializeSchema: ItemSchema, + override val mapperContext: MapperContext, + override val error: Throwable? = null, +) : HReqContext, + ErrorCombinable>, + Combinable, SerializeInput> { + + override fun plus(e: Throwable?) = copy(error = e.suppressing(error)) + + override fun plus(value: SerializeInput) = copy( + highLevelRequest = value.highLevelRequest, + serializeSchema = value.serializeSchema, + ) +} + +internal operator fun HReqContext.plus(lowLevelRequest: LReq) = + LReqContextImpl(highLevelRequest, serializeSchema, mapperContext, lowLevelRequest, error) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HResContextImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HResContextImpl.kt new file mode 100644 index 00000000000..abc8aa14d55 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/HResContextImpl.kt @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.HResContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.MapperContext +import aws.sdk.kotlin.hll.dynamodbmapper.util.requireNull + +internal data class HResContextImpl( + override val highLevelRequest: HReq, + override val serializeSchema: ItemSchema, + override val mapperContext: MapperContext, + override val lowLevelRequest: LReq, + override val lowLevelResponse: LRes, + override val deserializeSchema: ItemSchema, + override val highLevelResponse: HRes, + override val error: Throwable?, +) : HResContext, + ErrorCombinable>, + Combinable, HRes> { + + override fun plus(e: Throwable?) = copy(error = e.suppressing(error)) + override fun plus(value: HRes) = copy(highLevelResponse = value) +} + +internal fun HResContextImpl.solidify() = + HResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + lowLevelResponse, + deserializeSchema, + requireNotNull(highLevelResponse) { "Cannot solidify context with a null high-level response" } as HRes, + requireNull(error) { "Cannot solidify context with a non-null error" }, + ) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LReqContextImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LReqContextImpl.kt new file mode 100644 index 00000000000..a22a76bd6de --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LReqContextImpl.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.LReqContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.MapperContext +import aws.sdk.kotlin.hll.dynamodbmapper.util.requireNull + +internal data class LReqContextImpl( + override val highLevelRequest: HReq, + override val serializeSchema: ItemSchema, + override val mapperContext: MapperContext, + override val lowLevelRequest: LReq, + override val error: Throwable?, +) : LReqContext, + ErrorCombinable>, + Combinable, LReq> { + + override fun plus(e: Throwable?) = copy(error = e.suppressing(error)) + override fun plus(value: LReq) = copy(lowLevelRequest = value) +} + +internal operator fun LReqContext.plus( + lowLevelResponse: LRes, +) = LResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + lowLevelResponse, + serializeSchema, // Use the already-resolved schema at first + error, +) + +internal fun LReqContext.solidify() = LReqContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + requireNotNull(lowLevelRequest) { "Cannot solidify context with a null low-level request" } as LReq, + requireNull(error) { "Cannot solidify context with a non-null error" }, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LResContextImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LResContextImpl.kt new file mode 100644 index 00000000000..7e299057d45 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/LResContextImpl.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.DeserializeInput +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.LResContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.MapperContext +import aws.sdk.kotlin.hll.dynamodbmapper.util.requireNull + +internal data class LResContextImpl( + override val highLevelRequest: HReq, + override val serializeSchema: ItemSchema, + override val mapperContext: MapperContext, + override val lowLevelRequest: LReq, + override val lowLevelResponse: LRes, + override val deserializeSchema: ItemSchema, + override val error: Throwable?, +) : LResContext, + ErrorCombinable>, + Combinable, DeserializeInput> { + + override fun plus(e: Throwable?) = copy(error = e.suppressing(error)) + + override fun plus(value: DeserializeInput) = copy( + lowLevelResponse = value.lowLevelResponse, + deserializeSchema = value.deserializeSchema, + ) +} + +internal operator fun LResContext.plus( + highLevelResponse: HRes, +) = HResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + lowLevelResponse, + deserializeSchema, + highLevelResponse, + error, +) + +internal fun LResContext.solidify() = LResContextImpl( + highLevelRequest, + serializeSchema, + mapperContext, + lowLevelRequest, + requireNotNull(lowLevelResponse) { "Cannot solidify context with a null low-level response" } as LRes, + deserializeSchema, + requireNull(error) { "Cannot solidify context with a non-null error" }, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/MapperContextImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/MapperContextImpl.kt new file mode 100644 index 00000000000..546a84d3fe5 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/MapperContextImpl.kt @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.model.PersistenceSpec +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.* + +internal data class MapperContextImpl( + override val persistenceSpec: PersistenceSpec, + override val operation: String, +) : MapperContext diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/Operation.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/Operation.kt new file mode 100644 index 00000000000..db1b2ea4f6e --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/Operation.kt @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.* + +internal class Operation( + private val initialize: (HReq) -> HReqContextImpl, + private val serialize: (HReq, ItemSchema) -> LReq, + private val lowLevelInvoke: suspend (LReq) -> LRes, + private val deserialize: (LRes, ItemSchema) -> HRes, + interceptors: Collection, +) { + private val interceptors = interceptors.map { + // Will cause runtime ClassCastExceptions during interceptor invocation if the types don't match. Is that ok? + @Suppress("UNCHECKED_CAST") + it as Interceptor + } + + suspend fun execute(hReq: HReq): HRes { + val hReqContext = doInitialize(hReq) + val lReqContext = doSerialize(hReqContext) + val lResContext = doLowLevelInvoke(lReqContext) + val hResContext = doDeserialize(lResContext) + return finalize(hResContext) + } + + private fun > readOnlyHook( + input: I, + reverse: Boolean = false, + hook: Interceptor.(I) -> Unit, + ) = interceptors.fold(input, reverse) { ctx, interceptor -> + runCatching { + interceptor.hook(ctx) + }.fold( + onSuccess = { ctx }, + onFailure = { e -> ctx + e }, + ) + }.apply { error?.let { throw it } } // Throw error if present after executing all read-only hooks + + private fun modifyHook( + input: I, + reverse: Boolean = false, + hook: Interceptor.(I) -> V, + ): I where I : Combinable, I : ErrorCombinable { + var latestCtx = input + return runCatching { + interceptors.fold(latestCtx, reverse) { ctx, interceptor -> + latestCtx = ctx + val value = interceptor.hook(ctx) + ctx + value + } + }.fold( + onSuccess = { it }, + onFailure = { e -> latestCtx + e }, + ) + } + + private fun doInitialize(input: HReq): HReqContextImpl { + val ctx = initialize(input) + return readOnlyHook(ctx) { readAfterInitialization(it) } + } + + private fun doSerialize(inputCtx: HReqContextImpl): LReqContextImpl { + val rbsCtx = modifyHook(inputCtx) { modifyBeforeSerialization(it) } + val serCtx = readOnlyHook(rbsCtx) { readBeforeSerialization(it) } + + val serRes = serCtx.runCatching { serialize(serCtx.highLevelRequest, serCtx.serializeSchema) } + val lReq = serRes.getOrNull() + val rasCtx = serCtx + serRes.exceptionOrNull() + lReq + + return readOnlyHook(rasCtx) { readAfterSerialization(it) }.solidify() + } + + private suspend fun doLowLevelInvoke( + inputCtx: LReqContextImpl, + ): LResContextImpl { + val rbiCtx = modifyHook(inputCtx) { modifyBeforeInvocation(it) } + val invCtx = readOnlyHook(rbiCtx) { readBeforeInvocation(it) } + + val invRes = runCatching { lowLevelInvoke(invCtx.lowLevelRequest) } + val lRes = invRes.getOrNull() + val raiCtx = invCtx + invRes.exceptionOrNull() + lRes + + return readOnlyHook(raiCtx, reverse = true) { readAfterInvocation(it) }.solidify() + } + + private fun doDeserialize( + inputCtx: LResContextImpl, + ): HResContextImpl { + val rbdCtx = modifyHook(inputCtx, reverse = true) { modifyBeforeDeserialization(it) } + val desCtx = readOnlyHook(rbdCtx, reverse = true) { readBeforeDeserialization(it) } + + val desRes = desCtx.runCatching { deserialize(desCtx.lowLevelResponse, desCtx.deserializeSchema) } + val hRes = desRes.getOrNull() + val radCtx = desCtx + desRes.exceptionOrNull() + hRes + + return readOnlyHook(radCtx, reverse = true) { readAfterDeserialization(it) }.solidify() + } + + private fun finalize(inputCtx: HResContextImpl): HRes { + val raeCtx = modifyHook(inputCtx, reverse = true) { modifyBeforeCompletion(it) } + val finalCtx = readOnlyHook(raeCtx, reverse = true) { readBeforeCompletion(it) } + return finalCtx.highLevelResponse!! + } +} + +private inline fun List.fold(initial: R, reverse: Boolean, operation: (R, T) -> R): R = + if (reverse) foldRight(initial) { curr, acc -> operation(acc, curr) } else fold(initial, operation) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/SerializeInputImpl.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/SerializeInputImpl.kt new file mode 100644 index 00000000000..7882b29a4cf --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/SerializeInputImpl.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.MapperContext +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.SerializeInput + +internal data class SerializeInputImpl( + override val highLevelRequest: HReq, + override val serializeSchema: ItemSchema, +) : SerializeInput + +internal operator fun SerializeInput.plus(mapperContext: MapperContext) = + HReqContextImpl(highLevelRequest, serializeSchema, mapperContext, null) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/AttributeValues.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/AttributeValues.kt new file mode 100644 index 00000000000..d46dd9158b4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/AttributeValues.kt @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.util + +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.jvm.JvmName + +internal val NULL_ATTR = AttributeValue.Null(true) + +internal fun attr(value: Boolean?) = value?.let(AttributeValue::Bool) ?: NULL_ATTR +internal fun attr(value: ByteArray?) = value?.let(AttributeValue::B) ?: NULL_ATTR + +@JvmName("attrListAny") +internal fun attr(value: List?) = attr(value?.map(::dynamicAttr)) + +internal fun attr(value: List?) = value?.let(AttributeValue::L) ?: NULL_ATTR + +@JvmName("attrMapStringAny") +internal fun attr(value: Map?) = attr(value?.mapValues { (_, v) -> dynamicAttr(v) }) + +internal fun attr(value: Map?) = value?.let(AttributeValue::M) ?: NULL_ATTR + +@Suppress("UNUSED_PARAMETER") +internal fun attr(value: Nothing?) = NULL_ATTR + +internal fun attr(value: Number?) = value?.let { AttributeValue.N(it.toString()) } ?: NULL_ATTR + +@JvmName("attrSetByteArray") +internal fun attr(value: Set?) = value?.let { AttributeValue.Bs(it.toList()) } ?: NULL_ATTR + +@JvmName("attrSetNumber") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ns(it.map(Number::toString)) } ?: NULL_ATTR + +@JvmName("attrSetString") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ss(it.toList()) } ?: NULL_ATTR + +internal fun attr(value: String?) = value?.let(AttributeValue::S) ?: NULL_ATTR + +@JvmName("attrSetUByte") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ns(it.map(UByte::toString)) } ?: NULL_ATTR + +@JvmName("attrSetUInt") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ns(it.map(UInt::toString)) } ?: NULL_ATTR + +@JvmName("attrSetULong") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ns(it.map(ULong::toString)) } ?: NULL_ATTR + +@JvmName("attrSetUShort") +internal fun attr(value: Set?) = value?.let { AttributeValue.Ns(it.map(UShort::toString)) } ?: NULL_ATTR + +internal fun attr(value: UByte?) = value?.let { AttributeValue.N(it.toString()) } ?: NULL_ATTR +internal fun attr(value: UInt?) = value?.let { AttributeValue.N(it.toString()) } ?: NULL_ATTR +internal fun attr(value: ULong?) = value?.let { AttributeValue.N(it.toString()) } ?: NULL_ATTR +internal fun attr(value: UShort?) = value?.let { AttributeValue.N(it.toString()) } ?: NULL_ATTR + +@Suppress("UNCHECKED_CAST") +internal fun dynamicAttr(value: Any?): AttributeValue = when (value) { + null -> NULL_ATTR + is AttributeValue -> value + is Boolean -> attr(value) + is ByteArray -> attr(value) + is List<*> -> attr(value) + is Map<*, *> -> attr(value as Map) + is Number -> attr(value) + is Set<*> -> when (val type = value.firstOrNull()) { // Attempt to determine set type by first element + null -> attr(value as Set) // FIXME Is this a bad idea for the empty set case? + is ByteArray -> attr(value as Set) + is Number -> attr(value as Set) + is String -> attr(value as Set) + is UByte -> attr(value as Set) + is UInt -> attr(value as Set) + is ULong -> attr(value as Set) + is UShort -> attr(value as Set) + else -> error("Unsupported set element type $type") + } + is String -> attr(value) + else -> error("Unsupported attribute value type ${value::class}") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/Conditions.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/Conditions.kt new file mode 100644 index 00000000000..92970c22a16 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/util/Conditions.kt @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +internal fun requireNull(value: T?, lazyMessage: () -> Any): T? { + contract { + returns() implies (value == null) + } + + if (value == null) { + return null + } else { + val message = lazyMessage() + throw IllegalArgumentException(message.toString()) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ItemToValueConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ItemToValueConverter.kt new file mode 100644 index 00000000000..c2adda57125 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ItemToValueConverter.kt @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values + +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.toItem +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [Item] and [AttributeValue]. + * This converter is typically chained following an [ItemConverter] using the [andThenTo] extension function. + */ +@ExperimentalApi +public object ItemToValueConverter : ValueConverter { + /** + * Convert from [AttributeValue] to [Item] + */ + override fun convertFrom(to: AttributeValue): Item = to.asM().toItem() + + /** + * Convert [from] [Item] to [AttributeValue] + */ + override fun convertTo(from: Item): AttributeValue = AttributeValue.M(from) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter.kt new file mode 100644 index 00000000000..90a22dfa670 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverter.kt @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values + +import aws.sdk.kotlin.hll.dynamodbmapper.util.NULL_ATTR +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.SplittingConverter +import aws.sdk.kotlin.hll.mapping.core.converters.mergeBy +import aws.sdk.kotlin.hll.mapping.core.util.Either +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import kotlin.reflect.KClass + +/** + * Converts between potentially `null` values and + * [DynamoDB `NULL` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Null). + * Note that this class is a [SplittingConverter] and the logic for handling non-null values is undefined in this class. + * Thus, it is typically used in conjunction with the [NullableConverter] factory function or via [mergeBy]. + * @param V The non-nullable type + */ +@ExperimentalApi +public class NullableConverter(klass: KClass) : SplittingConverter { + override fun convertTo(from: V?): Either = when (from) { + null -> Either.Left(NULL_ATTR) + else -> Either.Right(from) + } + + override fun convertFrom(to: AttributeValue): Either = when (to) { + is AttributeValue.Null -> Either.Left(null) + else -> Either.Right(to) + } +} + +/** + * Initializes a new [NullableConverter] for the given reified type [V] + */ +@ExperimentalApi +public inline fun NullableConverter(): NullableConverter = NullableConverter(V::class) + +@ExperimentalApi +@Suppress("ktlint:standard:function-naming") +public inline fun NullableConverter( + delegate: Converter, +): Converter = NullableConverter().mergeBy(delegate) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConverter.kt new file mode 100644 index 00000000000..3f403387563 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConverter.kt @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values + +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Defines the logic for converting individual values between a high-level type [V] (e.g., [String], [Boolean], [Map]) + * and + * [DynamoDB data types](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + * @param V The type of high-level values which will be converted to low-level DynamoDB attribute values + */ +@ExperimentalApi +public typealias ValueConverter = Converter diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverter.kt new file mode 100644 index 00000000000..d5003c9c011 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverter.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [List] and + * [DynamoDB `L` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.List). + * Note that the lists must contain already-converted [AttributeValue] elements. This converter is typically chained + * with another converter which handles converting elements to [AttributeValue] either by using the factory function + * [ListConverter] or using the [mapFrom] extension method. + * + * For example: + * + * ```kotlin + * val intListConv = ListConverter(IntConverter) // ValueConverter> + * val intListConv2 = ListConverter.mapFrom(IntConverter) // same as above + * ``` + */ +@ExperimentalApi +public val ListConverter: ValueConverter> = Converter(AttributeValue::L, AttributeValue::asL) + +/** + * Creates a new list converter using the given [elementConverter] as a delegate + * @param F The type of elements in the list + * @param elementConverter A converter for transforming between values of [F] and [AttributeValue] + */ +@ExperimentalApi +@Suppress("ktlint:standard:function-naming") +public fun ListConverter(elementConverter: Converter): ValueConverter> = + ListConverter.mapFrom(elementConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverter.kt new file mode 100644 index 00000000000..ef0b62d2b14 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverter.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapKeysFrom +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapValuesFrom +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [Map] and + * [DynamoDB `M` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.Map). + * Note that the maps must contain [String] keys and already-converted [AttributeValue] values. This converter is + * typically chained with another converter which handles converting values to [AttributeValue] either by using the + * factory function [MapConverter] or by using the [mapFrom]/[mapValuesFrom]/[mapKeysFrom] extension methods. + * + * ```kotlin + * val instantMapConv = MapConverter(InstantConverter.Default) // ValueConverter> + * val instantMapConv2 = MapConverter.mapValuesFrom(InstantConverter.Default) // same as above + * ``` + */ +@ExperimentalApi +public val MapConverter: ValueConverter> = Converter(AttributeValue::M, AttributeValue::asM) + +/** + * Creates a new map converter using the given [keyConverter] and [valueConverter] as delegates + * @param K The type of keys in the map + * @param V The type of values in the map + * @param keyConverter A converter for transforming between [K] keys and [String] keys + * @param valueConverter A converter for transforming between [V] values and [AttributeValue] + */ +@ExperimentalApi +@Suppress("ktlint:standard:function-naming") +public fun MapConverter( + keyConverter: Converter, + valueConverter: ValueConverter, +): ValueConverter> = MapConverter.mapFrom(keyConverter, valueConverter) + +/** + * Creates a new string-keyed map converter using the given [valueConverter] as a delegate + * @param V The type of values in the map + * @param valueConverter A converter for transforming between [V] values and [AttributeValue] + */ +@ExperimentalApi +@Suppress("ktlint:standard:function-naming") +public fun MapConverter(valueConverter: ValueConverter): ValueConverter> = + MapConverter.mapValuesFrom(valueConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConverters.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConverters.kt new file mode 100644 index 00000000000..f65098c46f1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/NumberSetConverters.kt @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.* +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenFrom +import aws.sdk.kotlin.hll.mapping.core.converters.collections.CollectionTypeConverters +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with numerical set conversion + */ +@ExperimentalApi +public object NumberSetConverters { + /** + * Converts between a [List] of [String] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ + public val StringListToAttributeValueNumberSetConverter: ValueConverter> = + Converter(AttributeValue::Ns, AttributeValue::asNs) + + /** + * Converts between a [Set] of [String] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ + public val StringSetToAttributeValueNumberSetConverter: ValueConverter> = + StringListToAttributeValueNumberSetConverter.andThenFrom(CollectionTypeConverters.SetToListConverter()) + + /** + * Creates a [ValueConverter] which converts between a [Set] of [N] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + * @param N The type of high-level values which will be converted + */ + public fun of(numberToStringConverter: Converter): ValueConverter> = + StringSetToAttributeValueNumberSetConverter.mapFrom(numberToStringConverter) +} + +/** + * Converts between a [Set] of [Byte] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val ByteSetConverter: ValueConverter> = NumberSetConverters.of(NumberConverters.ByteToStringConverter) + +/** + * Converts between a [Set] of [Double] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val DoubleSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.DoubleToStringConverter) + +/** + * Converts between a [Set] of [Float] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val FloatSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.FloatToStringConverter) + +/** + * Converts between a [Set] of [Int] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val IntSetConverter: ValueConverter> = NumberSetConverters.of(NumberConverters.IntToStringConverter) + +/** + * Converts between a [Set] of [Long] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val LongSetConverter: ValueConverter> = NumberSetConverters.of(NumberConverters.LongToStringConverter) + +/** + * Converts between a [Set] of [Short] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val ShortSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.ShortToStringConverter) + +/** + * Converts between a [Set] of [UByte] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val UByteSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.UByteToStringConverter) + +/** + * Converts between a [Set] of [UInt] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val UIntSetConverter: ValueConverter> = NumberSetConverters.of(NumberConverters.UIntToStringConverter) + +/** + * Converts between a [Set] of [ULong] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val ULongSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.ULongToStringConverter) + +/** + * Converts between a [Set] of [UShort] elements and + * [DynamoDB `NS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val UShortSetConverter: ValueConverter> = + NumberSetConverters.of(NumberConverters.UShortToStringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/PrimitiveSetConverters.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/PrimitiveSetConverters.kt new file mode 100644 index 00000000000..3ff20e92b81 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/PrimitiveSetConverters.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.TextConverters +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenFrom +import aws.sdk.kotlin.hll.mapping.core.converters.collections.CollectionTypeConverters +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between a [Set] of [ByteArray] elements and + * [DynamoDB `BS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val ByteArraySetConverter: ValueConverter> = Converter( + convertTo = { from: Set -> AttributeValue.Bs(from.toList()) }, + convertFrom = { to: AttributeValue -> to.asBs().toSet() }, +) + +/** + * Converts between a [List] of [String] elements and + * [DynamoDB `SS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val StringListToAttributeValueStringSetConverter: ValueConverter> = + Converter(AttributeValue::Ss, AttributeValue::asSs) + +/** + * Converts between a [Set] of [String] elements and + * [DynamoDB `SS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val StringSetConverter: ValueConverter> = + StringListToAttributeValueStringSetConverter.andThenFrom(CollectionTypeConverters.SetToListConverter()) + +/** + * Converts between a [Set] of [CharArray] elements and + * [DynamoDB `SS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val CharArraySetConverter: ValueConverter> = + StringSetConverter.mapFrom(TextConverters.CharArrayToStringConverter) + +/** + * Converts between a [Set] of [Char] elements and + * [DynamoDB `SS` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes) + */ +@ExperimentalApi +public val CharSetConverter: ValueConverter> = + StringSetConverter.mapFrom(TextConverters.CharToStringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/BooleanConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/BooleanConverter.kt new file mode 100644 index 00000000000..1b663b12344 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/BooleanConverter.kt @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [Boolean] and + * [DynamoDB `BOOL` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Boolean) + */ +@ExperimentalApi +public val BooleanConverter: ValueConverter = Converter(AttributeValue::Bool, AttributeValue::asBool) + +/** + * Converts between [Boolean] and [String] + */ +@ExperimentalApi +public val BooleanToStringConverter: Converter = Converter({ it.toString() }, { it.toBoolean() }) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ByteArrayConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ByteArrayConverter.kt new file mode 100644 index 00000000000..a0e49973d81 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ByteArrayConverter.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [ByteArray] and + * [DynamoDB `B` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Binary) + */ +@ExperimentalApi +public val ByteArrayConverter: ValueConverter = Converter(AttributeValue::B, AttributeValue::asB) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/EnumConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/EnumConverter.kt new file mode 100644 index 00000000000..2dc2897c11e --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/EnumConverter.kt @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenFrom +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Converts between [Enum] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + * @param E The [Enum] type to convert + */ +@ExperimentalApi +public class EnumConverter>( + private val enumToStringConverter: Converter, +) : ValueConverter by StringConverter.andThenFrom(enumToStringConverter) + +/** + * Instantiates a new [ValueConverter] for enums of type [E] + * @param E The [Enum] type for which to create a [ValueConverter] + */ +@ExperimentalApi +public inline fun > EnumConverter(): EnumConverter = + EnumConverter( + enumToStringConverter = Converter( + convertTo = { from: E -> from.name }, + convertFrom = { to: String -> enumValueOf(to) }, + ), + ) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConverters.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConverters.kt new file mode 100644 index 00000000000..865ffea0ea2 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/NumberConverters.kt @@ -0,0 +1,192 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenTo +import aws.sdk.kotlin.hll.mapping.core.converters.validatingFrom +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with number conversion + */ +@ExperimentalApi +public object NumberConverters { + /** + * Converts between [String] instances which contains numbers and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ + public val StringToAttributeValueNumberConverter: ValueConverter = + Converter(AttributeValue::N, AttributeValue::asN) + + /** + * Creates a [ValueConverter] which converts between number type [N] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ + public fun of(numberToStringConverter: Converter): ValueConverter = + numberToStringConverter.andThenTo(StringToAttributeValueNumberConverter) + + /** + * Converts between [Number] and [String] values + */ + public val AutoNumberToStringConverter: Converter = Converter( + convertTo = Number::toString, + convertFrom = { to: String -> + when { + '.' in to -> to.toDouble() + else -> when (val longNumber = to.toLong()) { + in Int.MIN_VALUE..Int.MAX_VALUE -> longNumber.toInt() + else -> longNumber + } + } + }, + ) + + /** + * Converts between [Byte] and [String] values + */ + public val ByteToStringConverter: Converter = Converter(Byte::toString, String::toByte) + + /** + * Converts between [Double] and [String] values. Because + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + * do not support them, this converter throws exceptions for non-finite numbers such as [Double.NEGATIVE_INFINITY], + * [Double.POSITIVE_INFINITY], and [Double.NaN]. + */ + public val DoubleToStringConverter: Converter = + Converter(Double::toString, String::toDouble).validatingFrom { from: Double -> + require(from.isFinite()) { "Cannot convert $from: only finite numbers are supported" } + } + + /** + * Converts between [Float] and [String] values. Because + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + * do not support them, this converter throws exceptions for non-finite numbers such as [Float.NEGATIVE_INFINITY], + * [Float.POSITIVE_INFINITY], and [Float.NaN]. + */ + public val FloatToStringConverter: Converter = + Converter(Float::toString, String::toFloat).validatingFrom { from: Float -> + require(from.isFinite()) { "Cannot convert $from: only finite numbers are supported" } + } + + /** + * Converts between [Int] and [String] values + */ + public val IntToStringConverter: Converter = Converter(Int::toString, String::toInt) + + /** + * Converts between [Long] and [String] values + */ + public val LongToStringConverter: Converter = Converter(Long::toString, String::toLong) + + /** + * Converts between [Short] and [String] values + */ + public val ShortToStringConverter: Converter = Converter(Short::toString, String::toShort) + + /** + * Converts between [UByte] and [String] values + */ + public val UByteToStringConverter: Converter = Converter(UByte::toString, String::toUByte) + + /** + * Converts between [UInt] and [String] values + */ + public val UIntToStringConverter: Converter = Converter(UInt::toString, String::toUInt) + + /** + * Converts between [ULong] and [String] values + */ + public val ULongToStringConverter: Converter = Converter(ULong::toString, String::toULong) + + /** + * Converts between [UShort] and [String] values + */ + public val UShortToStringConverter: Converter = Converter(UShort::toString, String::toUShort) +} + +/** + * Converts between [Number] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number). + * When converting attribute values into number values, the following concrete subclasses of [Number] will be returned: + * * [Double] — If the number contains any fractional component + * * [Int] — If the number is in the range of [Int.MIN_VALUE] and [Int.MAX_VALUE] (inclusive) + * * [Long] — Anything else + */ +@ExperimentalApi +public val AutoNumberConverter: ValueConverter = + NumberConverters.of(NumberConverters.AutoNumberToStringConverter) + +/** + * Converts between [Byte] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val ByteConverter: ValueConverter = NumberConverters.of(NumberConverters.ByteToStringConverter) + +/** + * Converts between [Double] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val DoubleConverter: ValueConverter = NumberConverters.of(NumberConverters.DoubleToStringConverter) + +/** + * Converts between [Float] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val FloatConverter: ValueConverter = NumberConverters.of(NumberConverters.FloatToStringConverter) + +/** + * Converts between [Int] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val IntConverter: ValueConverter = NumberConverters.of(NumberConverters.IntToStringConverter) + +/** + * Converts between [Long] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val LongConverter: ValueConverter = NumberConverters.of(NumberConverters.LongToStringConverter) + +/** + * Converts between [Short] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val ShortConverter: ValueConverter = NumberConverters.of(NumberConverters.ShortToStringConverter) + +/** + * Converts between [UByte] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val UByteConverter: ValueConverter = NumberConverters.of(NumberConverters.UByteToStringConverter) + +/** + * Converts between [UInt] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val UIntConverter: ValueConverter = NumberConverters.of(NumberConverters.UIntToStringConverter) + +/** + * Converts between [ULong] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val ULongConverter: ValueConverter = NumberConverters.of(NumberConverters.ULongToStringConverter) + +/** + * Converts between [UShort] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + */ +@ExperimentalApi +public val UShortConverter: ValueConverter = NumberConverters.of(NumberConverters.UShortToStringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConverters.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConverters.kt new file mode 100644 index 00000000000..df119cf0433 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/TextConverters.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenTo +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with text conversion + */ +@ExperimentalApi +public object TextConverters { + /** + * Converts between [CharArray] and [String] + */ + public val CharArrayToStringConverter: Converter = Converter(CharArray::concatToString, String::toCharArray) + + /** + * Converts between [Char] and [String] + */ + public val CharToStringConverter: Converter = Converter(Char::toString, String::single) + + /** + * Converts between [String] and [String] + */ + public val StringToStringConverter: Converter = Converter({ it }, { it }) +} + +/** + * Converts between [String] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + */ +@ExperimentalApi +public val StringConverter: ValueConverter = Converter(AttributeValue::S, AttributeValue::asS) + +/** + * Converts between [CharArray] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + */ +@ExperimentalApi +public val CharArrayConverter: ValueConverter = + TextConverters.CharArrayToStringConverter.andThenTo(StringConverter) + +/** + * Converts between [Char] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + */ +@ExperimentalApi +public val CharConverter: ValueConverter = TextConverters.CharToStringConverter.andThenTo(StringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter.kt new file mode 100644 index 00000000000..1d36c8383ae --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentConverter.kt @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.NullableConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.collections.ListConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.collections.MapConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.AutoNumberConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.BooleanConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom +import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapValuesFrom +import aws.sdk.kotlin.hll.mapping.core.converters.mergeBy +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.content.Document + +/** + * Converts between [Document] and various DynamoDB value types. The following conversions are performed: + * * `null` ↔ [DynamoDB `NULL` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Null) + * * [Document.Number] ↔ [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + * * [Document.String] ↔ [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + * * [Document.Boolean] ↔ [DynamoDB `BOOL` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Boolean) + * * [Document.List] ↔ [DynamoDB `L` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.List) + * * [Document.Map] ↔ [DynamoDB `M` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.Map) + */ +@ExperimentalApi +public class DocumentConverter( + private val numberConverter: ValueConverter = AutoNumberConverter, + private val stringConverter: ValueConverter = StringConverter, + private val booleanConverter: ValueConverter = BooleanConverter, + nullableConverter: NullableConverter = NullableConverter(), + listConverter: ValueConverter> = ListConverter, + mapConverter: ValueConverter> = MapConverter, +) : ValueConverter { + private val nullableConverter = nullableConverter.mergeBy(this) + private val listConverter = listConverter.mapFrom(this.nullableConverter) + private val mapConverter = mapConverter.mapValuesFrom(this.nullableConverter) + + @ExperimentalApi + public companion object { + /** + * The default instance of [DocumentConverter] + */ + public val Default: DocumentConverter = DocumentConverter() + } + + override fun convertFrom(to: AttributeValue): Document = when (to) { + is AttributeValue.N -> Document.Number(numberConverter.convertFrom(to)) + is AttributeValue.S -> Document.String(stringConverter.convertFrom(to)) + is AttributeValue.Bool -> Document.Boolean(booleanConverter.convertFrom(to)) + is AttributeValue.L -> Document.List(listConverter.convertFrom(to)) + is AttributeValue.M -> Document.Map(mapConverter.convertFrom(to)) + else -> throw IllegalArgumentException("Documents do not support ${to::class.qualifiedName} values") + } + + override fun convertTo(from: Document): AttributeValue = when (from) { + is Document.Number -> numberConverter.convertTo(from.value) + is Document.String -> stringConverter.convertTo(from.value) + is Document.Boolean -> booleanConverter.convertTo(from.value) + is Document.List -> listConverter.convertTo(from.value) + is Document.Map -> mapConverter.convertTo(from.value) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConverter.kt new file mode 100644 index 00000000000..9513032b386 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConverter.kt @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.LongConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenTo +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.TimestampFormat +import aws.smithy.kotlin.runtime.time.epochMilliseconds +import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds + +/** + * Provides access to [ValueConverter] types for various [Instant] representations + */ +@ExperimentalApi +public object InstantConverter { + /** + * Converts between [Instant] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + * containing the number of milliseconds since the Unix epoch + */ + public val EpochMs: ValueConverter = + Converter(Instant::epochMilliseconds, Instant::fromEpochMilliseconds).andThenTo(LongConverter) + + /** + * Converts between [Instant] and + * [DynamoDB `N` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number) + * containing the number of seconds since the Unix epoch + */ + public val EpochS: ValueConverter = + Converter(Instant::epochSeconds, Instant::fromEpochSeconds).andThenTo(LongConverter) + + /** + * Converts between [Instant] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + * containing a formatted ISO 8601 representation + */ + public val Iso8601: ValueConverter = + Converter( + convertTo = { from: Instant -> from.format(TimestampFormat.ISO_8601_FULL) }, + convertFrom = Instant::fromIso8601, + ).andThenTo(StringConverter) + + /** + * The default converter for [Instant] values, which is an instance of [EpochS] + */ + public val Default: ValueConverter = EpochS +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverter.kt new file mode 100644 index 00000000000..a727fda3283 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverter.kt @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenTo +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.net.url.Url + +/** + * Converts between [Url] and [String] types + */ +@ExperimentalApi +public val UrlToStringConverter: Converter = Converter(Url::toString, Url::parse) + +/** + * Converts between [Url] and + * [DynamoDB `S` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.String) + */ +@ExperimentalApi +public val UrlConverter: ValueConverter = UrlToStringConverter.andThenTo(StringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/FilterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/FilterTest.kt new file mode 100644 index 00000000000..a075057d3fc --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/FilterTest.kt @@ -0,0 +1,804 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.FilterImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.ParameterizingExpressionVisitor +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.UByteRange +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.UShortRange +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test +import kotlin.test.assertEquals + +class FilterTest { + @Test + fun testAnd() { + testFilters( + mapOf( + ":v0" to attr(5), + ":v1" to attr("banana"), + ":v2" to attr(null), + ), + "(foo = :v0) AND (bar < :v1) AND (baz <> :v2)" to { + and( + attr("foo") eq 5, + attr("bar") lt "banana", + attr("baz") neq null, + ) + }, + ) + } + + @Test + fun testBooleans() { + listOf(false, true, null).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + } + + @Test + fun testByteArrays() { + val b1 = byteArrayOf(1, 2, 3) + val b2 = byteArrayOf(4, 5, 6) + val b3 = byteArrayOf(7, 8, 9) + + listOf(b1, b2, b3).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(b1), + ":v1" to attr(b2), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo").isBetween(b1, b2) }, + ) + + (null as ByteArray?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + } + + @Test + fun testCollectionsOfByteArrays() { + testFilters( + mapOf( + ":v0" to attr(byteArrayOf(1, 2, 3)), + ":v1" to attr(byteArrayOf(4, 5, 6)), + ":v2" to attr(byteArrayOf(7, 8, 9)), + ), + "foo IN (:v0, :v1, :v2)" to { + attr("foo") isIn setOf( + byteArrayOf(1, 2, 3), + byteArrayOf(4, 5, 6), + byteArrayOf(7, 8, 9), + ) + }, + ) + } + + @Test + fun testCollectionsOfLists() { + testFilters( + mapOf( + ":v0" to attr(listOf("apple", false, 1, null)), + ":v1" to attr(listOf("banana", true, 2)), + ":v2" to attr(listOf("cherry", 3)), + ":v3" to attr(null), + ), + "foo IN (:v0, :v1, :v2, :v3)" to { + attr("foo") isIn setOf( + listOf("apple", false, 1, null), + listOf("banana", true, 2), + listOf("cherry", 3), + null, + ) + }, + ) + } + + @Test + fun testCollectionsOfMaps() { + testFilters( + mapOf( + ":v0" to attr(mapOf("a" to "apple", "b" to false, "c" to 1, "d" to null)), + ":v1" to attr(mapOf("e" to "banana", "f" to true, "g" to 2)), + ":v2" to attr(mapOf("h" to "cherry", "i" to 3)), + ":v3" to attr(null), + ), + "foo IN (:v0, :v1, :v2, :v3)" to { + attr("foo") isIn setOf( + mapOf("a" to "apple", "b" to false, "c" to 1, "d" to null), + mapOf("e" to "banana", "f" to true, "g" to 2), + mapOf("h" to "cherry", "i" to 3), + null, + ) + }, + ) + } + + @Test + fun testCollectionsOfNumbers() { + testFilters( + mapOf( + ":v0" to attr(42), + ":v1" to attr(-1_000_000_000_000_000L), + ":v2" to attr(null), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn setOf(42, -1_000_000_000_000_000L, null) }, + ) + } + + @Test + fun testCollectionsOfSetsOfByteArrays() { + // Collections of sets of arrays 😵‍💫 + + val sets = listOf( + setOf( + byteArrayOf(1, 2, 3), + byteArrayOf(4, 5, 6), + byteArrayOf(7, 8, 9), + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfNumbers() { + val sets = listOf( + setOf( + 13.toByte(), + (-42).toShort(), + -5, + 31_556_952_000L, + 2.71828f, + 3.14159, + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfStrings() { + val sets = listOf( + setOf( + "apple", + "banana", + "cherry", + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfUBytes() { + val sets = listOf( + setOf( + UByte.MIN_VALUE, + 42.toUByte(), + UByte.MAX_VALUE, + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfUInt() { + val sets = listOf( + setOf( + UInt.MIN_VALUE, + 42.toUInt(), + UInt.MAX_VALUE, + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfULong() { + val sets = listOf( + setOf( + ULong.MIN_VALUE, + 42.toULong(), + ULong.MAX_VALUE, + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testCollectionsOfSetsOfUShorts() { + val sets = listOf( + setOf( + UShort.MIN_VALUE, + 42.toUShort(), + UShort.MAX_VALUE, + ), + setOf(), + null, + ) + + testFilters( + mapOf( + ":v0" to attr(sets[0]), + ":v1" to attr(sets[1]), + ":v2" to attr(sets[2]), + ), + "foo IN (:v0, :v1, :v2)" to { attr("foo") isIn sets }, + ) + } + + @Test + fun testExists() = testFilters( + "attribute_exists(foo)" to { attr("foo").exists() }, + ) + + @Test + fun testExpressions() { + testFilters( + "foo = bar" to { attr("foo") eq attr("bar") }, + "foo <> bar" to { attr("foo") neq attr("bar") }, + "foo < bar" to { attr("foo") lt attr("bar") }, + "foo <= bar" to { attr("foo") lte attr("bar") }, + "foo > bar" to { attr("foo") gt attr("bar") }, + "foo >= bar" to { attr("foo") gte attr("bar") }, + "contains(foo, bar)" to { attr("foo") contains attr("bar") }, + "begins_with(foo, bar)" to { attr("foo") startsWith attr("bar") }, + "foo BETWEEN bar AND baz" to { attr("foo").isBetween(attr("bar"), attr("baz")) }, + "foo IN (bar, baz, qux)" to { attr("foo") isIn listOf(attr("bar"), attr("baz"), attr("qux")) }, + ) + + testFilters( + expectedAVs = null, + expectedANs = mapOf( + "#k0" to "Key.With.Dots", + "#k1" to "And", + "#k2" to "Or", + ), + tests = arrayOf( + "foo = Attr.#k0.#k1.#k2.Indices[5]" to { + attr("foo") eq attr("Attr")["Key.With.Dots"]["And"]["Or"]["Indices"][5] + }, + ), + ) + } + + @Test + fun testIsOfType() = + testFilters( + attr("S"), + "attribute_type(foo, :v0)" to { attr("foo") isOfType AttributeType.String }, + ) + + @Test + fun testLists() { + listOf( + listOf("apple", false, 1, null), + listOf("banana", true, 2), + listOf("cherry", 3), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testMaps() { + listOf( + mapOf("a" to "apple", "b" to false, "c" to 1, "d" to null), + mapOf("e" to "banana", "f" to true, "g" to 2), + mapOf("h" to "cherry", "i" to 3), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testNot() = testFilters( + "NOT (foo = bar)" to { not(attr("foo") eq attr("bar")) }, + ) + + @Test + fun testNotExists() = testFilters( + "attribute_not_exists(foo)" to { attr("foo").notExists() }, + ) + + @Test + fun testNumbers() { + listOf( + 13.toByte(), + (-42).toShort(), + -5, + 31_556_952_000L, + 2.71828f, + 3.14159, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100), + ":v1" to attr(200), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn 100..200 }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf(100, 200) }, + ) + + (null as Number?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + } + + @Test + fun testOr() { + testFilters( + mapOf( + ":v0" to attr(5), + ":v1" to attr("banana"), + ":v2" to attr(null), + ), + "(foo > :v0) OR (bar >= :v1) OR (baz = :v2)" to { + or( + attr("foo") gt 5, + attr("bar") gte "banana", + attr("baz") eq null, + ) + }, + ) + } + + @Test + fun testSetsOfByteArrays() { + listOf( + setOf( + byteArrayOf(1, 2, 3), + byteArrayOf(4, 5, 6), + byteArrayOf(7, 8, 9), + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfNumbers() { + listOf( + setOf( + 13.toByte(), + (-42).toShort(), + -5, + 31_556_952_000L, + 2.71828f, + 3.14159, + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfStrings() { + listOf( + setOf( + "apple", + "banana", + "cherry", + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfUBytes() { + listOf( + setOf( + UByte.MIN_VALUE, + 42.toUByte(), + UByte.MAX_VALUE, + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfUInts() { + listOf( + setOf( + UInt.MIN_VALUE, + 42.toUInt(), + UInt.MAX_VALUE, + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfULongs() { + listOf( + setOf( + ULong.MIN_VALUE, + 42.toULong(), + ULong.MAX_VALUE, + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSetsOfUShorts() { + listOf( + setOf( + UShort.MIN_VALUE, + 42.toUShort(), + UShort.MAX_VALUE, + ), + setOf(), + null, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testSize() = testFilters( + "size(foo)" to { attr("foo").size }, + ) + + @Test + fun testStrings() { + listOf( + "apple", + "banana", + "cherry", + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + "begins_with(foo, :v0)" to { attr("foo") startsWith value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr("apple"), + ":v1" to attr("banana"), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn "apple".."banana" }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf("apple", "banana") }, + ) + + (null as String?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "contains(foo, :v0)" to { attr("foo") contains value }, + ) + } + } + + @Test + fun testUBytes() { + listOf( + UByte.MIN_VALUE, + 42.toUByte(), + UByte.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUByte()), + ":v1" to attr(200.toUByte()), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn UByteRange(100.toUByte(), 200.toUByte()) }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf(100.toUByte(), 200.toUByte()) }, + ) + + (null as UByte?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testUInts() { + listOf( + UInt.MIN_VALUE, + 42.toUInt(), + UInt.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUInt()), + ":v1" to attr(200.toUInt()), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn 100.toUInt().rangeTo(200.toUInt()) }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf(100.toUInt(), 200.toUInt()) }, + ) + + (null as UInt?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testULongs() { + listOf( + ULong.MIN_VALUE, + 42.toULong(), + ULong.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toULong()), + ":v1" to attr(200.toULong()), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn 100.toULong().rangeTo(200.toULong()) }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf(100.toULong(), 200.toULong()) }, + ) + + (null as ULong?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + @Test + fun testUShorts() { + listOf( + UShort.MIN_VALUE, + 42.toUShort(), + UShort.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + "foo < :v0" to { attr("foo") lt value }, + "foo <= :v0" to { attr("foo") lte value }, + "foo > :v0" to { attr("foo") gt value }, + "foo >= :v0" to { attr("foo") gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUShort()), + ":v1" to attr(200.toUShort()), + ), + "foo BETWEEN :v0 AND :v1" to { attr("foo") isIn UShortRange(100.toUShort(), 200.toUShort()) }, + "foo IN (:v0, :v1)" to { attr("foo") isIn setOf(100.toUShort(), 200.toUShort()) }, + ) + + (null as UShort?).let { value -> + testFilters( + attr(value), + "foo = :v0" to { attr("foo") eq value }, + "foo <> :v0" to { attr("foo") neq value }, + ) + } + } + + private fun testFilters(vararg tests: Pair Expression>) { + testFilters(null, *tests) + } + + private fun testFilters(expectedAV: AttributeValue, vararg tests: Pair Expression>) = + testFilters(mapOf(":v0" to expectedAV), *tests) + + private fun testFilters( + expectedAVs: Map?, + vararg tests: Pair Expression>, + expectedANs: Map? = null, + ) = tests.forEach { (expectedExprString, block) -> + val parameterizer = ParameterizingExpressionVisitor() + val expr = FilterImpl.block() + val actualExprString = expr.accept(parameterizer) + + assertEquals(expectedExprString, actualExprString) + + val actualAVs = parameterizer.expressionAttributeValues() + assertEquals(expectedAVs, actualAVs) + + val actualANs = parameterizer.expressionAttributeNames() + assertEquals(expectedANs, actualANs) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilterTest.kt new file mode 100644 index 00000000000..a3ee5160946 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/KeyFilterTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.FilterImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.toExpression +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class KeyFilterTest { + private val singleKeySchema = ItemSchema(DummyConverter, KeySpec.String("primary")) + private val compositeSchema = ItemSchema(DummyConverter, KeySpec.String("primary"), KeySpec.Number("secondary")) + + @Test + fun testSingleKeySchema() { + val kf = KeyFilter("foo") + val actual = kf.toExpression(singleKeySchema) + val expected = FilterImpl.run { attr("primary") eq "foo" } + + assertEquals(expected, actual) + } + + @Test + fun testSingleKeySchemaWithErroneousSortKey() { + val kf = KeyFilter("foo") { sortKey eq 2 } + + assertFailsWith { + kf.toExpression(singleKeySchema) + } + } + + @Test + fun testCompositeSchema() { + val kf = KeyFilter("foo") { sortKey lte 10 } + val actual = kf.toExpression(compositeSchema) + val expected = FilterImpl.run { + and( + attr("primary") eq "foo", + attr("secondary") lte 10, + ) + } + + assertEquals(expected, actual) + } + + @Test + fun testCompositeSchemaWithoutSortKey() { + val kf = KeyFilter("foo") + val actual = kf.toExpression(compositeSchema) + val expected = FilterImpl.run { attr("primary") eq "foo" } + + assertEquals(expected, actual) + } +} + +object DummyConverter : ItemConverter { + override fun convertFrom(to: Item) = error("Not needed") + override fun convertTo(from: Any, onlyAttributes: Set?) = error("Not needed") +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilterTest.kt new file mode 100644 index 00000000000..1f29926a044 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/expressions/SortKeyFilterTest.kt @@ -0,0 +1,239 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.expressions + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.ParameterizingExpressionVisitor +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.SkAttrPathImpl +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.internal.SortKeyFilterImpl +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.UByteRange +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.UShortRange +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test +import kotlin.test.assertEquals + +class SortKeyFilterTest { + @Test + fun testByteArrays() { + val b1 = byteArrayOf(1, 2, 3) + val b2 = byteArrayOf(4, 5, 6) + val b3 = byteArrayOf(7, 8, 9) + + listOf(b1, b2, b3).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + "begins_with(foo, :v0)" to { sortKey startsWith value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(b1), + ":v1" to attr(b2), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey.isBetween(b1, b2) }, + ) + } + + @Test + fun testNumbers() { + listOf( + 13.toByte(), + (-42).toShort(), + -5, + 31_556_952_000L, + 2.71828f, + 3.14159, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100), + ":v1" to attr(200), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn 100..200 }, + ) + } + + @Test + fun testStrings() { + listOf( + "apple", + "banana", + "cherry", + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + "begins_with(foo, :v0)" to { sortKey startsWith value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr("apple"), + ":v1" to attr("banana"), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn "apple".."banana" }, + ) + } + + @Test + fun testUBytes() { + listOf( + UByte.MIN_VALUE, + 42.toUByte(), + UByte.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUByte()), + ":v1" to attr(200.toUByte()), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn UByteRange(100.toUByte(), 200.toUByte()) }, + ) + } + + @Test + fun testUInts() { + listOf( + UInt.MIN_VALUE, + 42.toUInt(), + UInt.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUInt()), + ":v1" to attr(200.toUInt()), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn 100.toUInt().rangeTo(200.toUInt()) }, + ) + } + + @Test + fun testULongs() { + listOf( + ULong.MIN_VALUE, + 42.toULong(), + ULong.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toULong()), + ":v1" to attr(200.toULong()), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn 100.toULong().rangeTo(200.toULong()) }, + ) + } + + @Test + fun testUShorts() { + listOf( + UShort.MIN_VALUE, + 42.toUShort(), + UShort.MAX_VALUE, + ).forEach { value -> + testFilters( + attr(value), + "foo = :v0" to { sortKey eq value }, + "foo <> :v0" to { sortKey neq value }, + "foo < :v0" to { sortKey lt value }, + "foo <= :v0" to { sortKey lte value }, + "foo > :v0" to { sortKey gt value }, + "foo >= :v0" to { sortKey gte value }, + ) + } + + testFilters( + mapOf( + ":v0" to attr(100.toUShort()), + ":v1" to attr(200.toUShort()), + ), + "foo BETWEEN :v0 AND :v1" to { sortKey isIn UShortRange(100.toUShort(), 200.toUShort()) }, + ) + } + + private fun testFilters(expectedAV: AttributeValue, vararg tests: Pair SortKeyExpr>) = + testFilters(mapOf(":v0" to expectedAV), *tests) + + private fun testFilters( + expectedAVs: Map?, + vararg tests: Pair SortKeyExpr>, + expectedANs: Map? = null, + ) = tests.forEach { (expectedExprString, block) -> + val parameterizer = SortKeyExpressionVisitor() + val expr = SortKeyFilterImpl.block() + val actualExprString = expr.accept(parameterizer) + + assertEquals(expectedExprString, actualExprString) + + val actualAVs = parameterizer.expressionAttributeValues() + assertEquals(expectedAVs, actualAVs) + + val actualANs = parameterizer.expressionAttributeNames() + assertEquals(expectedANs, actualANs) + } +} + +private class SortKeyExpressionVisitor : ParameterizingExpressionVisitor() { + override fun visit(expr: AttributePath) = when (expr) { + SkAttrPathImpl -> "foo" // Swap out dummy attr path for "foo" (normally KeyFilter.toExpression would do this) + else -> super.visit(expr) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverterTest.kt new file mode 100644 index 00000000000..dc9e33d22c4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/items/SimpleItemConverterTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.items + +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.BooleanConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SimpleItemConverterTest { + @Test + fun testSomething() { + val converter = SimpleItemConverter( + ::ProductBuilder, + ProductBuilder::build, + AttributeDescriptor("id", Product::id, ProductBuilder::id::set, IntConverter), + AttributeDescriptor("name", Product::name, ProductBuilder::name::set, StringConverter), + AttributeDescriptor("in-stock", Product::inStock, ProductBuilder::inStock::set, BooleanConverter), + ) + + val foo = Product(42, "Foo 2.0", inStock = true) + val item = converter.convertTo(foo) + + assertEquals(3, item.size) + assertEquals(42, item.getValue("id").asN().toInt()) + assertEquals("Foo 2.0", item.getValue("name").asS()) + assertTrue(item.getValue("in-stock").asBool()) + + val unconverted = converter.convertFrom(item) + assertEquals(foo, unconverted) + } +} + +private data class Product( + val id: Int, + val name: String, + val inStock: Boolean, +) + +private class ProductBuilder { + var id: Int? = null + var name: String? = null + var inStock: Boolean? = null + + fun build() = Product(id!!, name!!, inStock!!) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/Ranges.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/Ranges.kt new file mode 100644 index 00000000000..1b7107b4005 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/Ranges.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.testutils + +// Weirdly, Kotlin stdlib doesn't have range implementations for UByte and UShort (but it _does_ have UInt and ULong). +// So we're rolling our own here! +data class UByteRange(override val start: UByte, override val endInclusive: UByte) : ClosedRange +data class UShortRange(override val start: UShort, override val endInclusive: UShort) : ClosedRange diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverterTest.kt new file mode 100644 index 00000000000..1f2f01d470a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/NullableConverterTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values + +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.sdk.kotlin.hll.mapping.core.converters.andThenTo +import kotlin.test.Test + +class NullableConverterTest : ValueConvertersTest() { + @Test + fun testNullConverter() = given(NullableConverter(stringReverseConverter)) { + "foo" inDdbIs "oof" + "bar" inDdbIs "rab" + null inDdbIs theSame + "null" inDdbIs "llun" + } +} + +private val stringReverseConverter = Converter(String::reversed, String::reversed).andThenTo(StringConverter) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConvertersTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConvertersTest.kt new file mode 100644 index 00000000000..2077b365a7f --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/ValueConvertersTest.kt @@ -0,0 +1,218 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values + +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.hll.dynamodbmapper.util.dynamicAttr +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.jvm.JvmName +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * A base class for unit testing [ValueConverter] instances, including a DSL for streamlining test setup. Tests are + * configured using the [given] method and a DSL block that creates test steps using the + * [infix function](https://kotlinlang.org/docs/functions.html#infix-notation) [TestBuilder.inDdbIs]. + * + * Example usage: + * + * ```kotlin + * fun testFooConverter() = given(fooConverterInstance) { + * // Test conversion of high-level value `aFoo` to low-level attribute value { "S": "a foo" } and back again + * aFoo inDdbIs attr("a foo") + * + * // Test conversion of high-level value to an automatically-derived identical representation and back again + * bFoo inDdbIs theSame + * + * // Test conversion resulting in an error + * cFoo inDdbIs anError + * + * // Test conversion only going one way (useful when conversion back results in a different value) + * dFoo inDdbIs attr("d foo") whenGoing Direction.TO_DDB + * } + * ``` + * @see [given] + * @see [TestBuilder.inDdbIs] + * @see [TestBuilder.theSame] + * @see [TestBuilder.anError] + * @see [TestBuilder.whenGoing] + * @see [attr] + */ +abstract class ValueConvertersTest { + /** + * Executes a series of individual tests on the given [converter] + * @param converter The [ValueConverter] instance under test + * @param block A DSL block for constructing individual tests via [TestBuilder.inDdbIs] + */ + protected fun given(converter: ValueConverter, block: TestBuilder.() -> Unit) { + val tests = mutableListOf>() + TestBuilder(tests).apply(block) + + tests.forEachIndexed { index, (steps) -> + steps.forEach { (direction, highLevel, lowLevel) -> + when (direction) { + Direction.TO_ATTRIBUTE_VALUE -> { + val result = runCatching { converter.convertTo(highLevel.requireInput()) } + lowLevel.assert(result, "Test $index failed converting to attribute value") + } + + Direction.FROM_ATTRIBUTE_VALUE -> { + val result = runCatching { converter.convertFrom(lowLevel.requireInput()) } + highLevel.assert(result, "Test $index failed converting from attribute value") + } + } + } + } + } + + /** + * Identifies a direction of conversion + */ + enum class Direction { + /** + * Specifies conversion from a high-level value to a low-level DynamoDB attribute value + */ + TO_ATTRIBUTE_VALUE, + + /** + * Specifies conversion from a low-level DynamoDB attribute value to a high-level value + */ + FROM_ATTRIBUTE_VALUE, + } + + /** + * A test case consisting of multiple test steps + */ + data class TestCase(val steps: List>) { + companion object { + fun of(highLevel: TestExpectation, lowLevel: TestExpectation): TestCase { + val toAv = highLevel.asSuccessOrNull()?.let { TestStep(Direction.TO_ATTRIBUTE_VALUE, it, lowLevel) } + val fromAv = lowLevel.asSuccessOrNull()?.let { TestStep(Direction.FROM_ATTRIBUTE_VALUE, highLevel, it) } + return TestCase(listOfNotNull(toAv, fromAv)) + } + } + } + + /** + * A test step that describes the expectations for converting a value in a single direction + */ + data class TestStep( + val direction: Direction, + val highLevel: TestExpectation, + val lowLevel: TestExpectation, + ) + + /** + * An expected value or outcome for an input or output to a conversion + */ + sealed interface TestExpectation { + fun assert(result: Result<*>, message: String) + + fun asSuccessOrNull(): Success? = this as? Success + + fun requireInput(): T { + require(this is Success) { "Cannot use $this as an input to a converter test" } + return value + } + + data class Success(val value: T) : TestExpectation { + override fun assert(result: Result<*>, message: String) { + @Suppress("UNCHECKED_CAST") + val output = result.getOrThrow() as T + assertDeepEquals(value, output, message) + } + } + + data object Failure : TestExpectation { + override fun assert(result: Result<*>, message: String) { + assertTrue(result.isFailure, "$message (actual value: ${result.getOrNull()})") + } + } + } + + class TestBuilder(private val tests: MutableList>) { + object AnError + object TheSame + + /** + * Indicates that an error is expected + */ + val anError = AnError + + /** + * Indicates that an automatically-derived identical representation is expected + */ + val theSame = TheSame + + private fun T.addTest(lowLevel: AttributeValue): TestCase = + TestCase + .of(TestExpectation.Success(this), TestExpectation.Success(lowLevel)) + .also(tests::add) + + private fun T.addTest(anError: AnError): TestCase = + TestCase + .of(TestExpectation.Success(this), TestExpectation.Failure) + .also(tests::add) + + infix fun T.inDdbIs(anError: AnError) = addTest(anError) + infix fun T.inDdbIs(theSame: TheSame) = addTest(dynamicAttr(this)) + + infix fun T.inDdbIs(attr: AttributeValue) = addTest(attr) + infix fun T.inDdbIs(value: Boolean) = addTest(attr(value)) + infix fun T.inDdbIs(value: ByteArray) = addTest(attr(value)) + infix fun T.inDdbIs(value: List) = addTest(attr(value)) + infix fun T.inDdbIs(value: Map) = addTest(attr(value)) + infix fun T.inDdbIs(value: Nothing?) = addTest(attr(null)) + infix fun T.inDdbIs(value: Number) = addTest(attr(value)) + infix fun T.inDdbIs(value: String) = addTest(attr(value)) + + @JvmName("inDdbIsSetByteArray") + infix fun T.inDdbIs(value: Set) = addTest(attr(value)) + + @JvmName("inDdbIsSetNumber") + infix fun T.inDdbIs(value: Set) = addTest(attr(value)) + + @JvmName("inDdbIsSetString") + infix fun T.inDdbIs(value: Set) = addTest(attr(value)) + + /** + * Limits a test case to only a single direction (i.e., checking one of `fromAttributeValue`/`toAttributeValue` + * but not both). Most test cases are bidirectional. + */ + infix fun TestCase.whenGoing(direction: Direction) { + val removed = steps.filter { it.direction == direction } + if (removed != steps) { + tests.remove(this) + tests.add(TestCase(removed)) + } + } + } +} + +/** + * A convenience function that allows asserting value equality for non-array types and content equality for array types + * (including primitive arrays) + */ +@OptIn(ExperimentalUnsignedTypes::class) +@Suppress("UNCHECKED_CAST") +private fun assertDeepEquals(expected: T, actual: T, message: String) = when { + expected is Array<*> && actual is Array<*> -> + assertContentEquals(expected as Array, actual as Array, message) + + expected is BooleanArray && actual is BooleanArray -> assertContentEquals(expected, actual, message) + expected is ByteArray && actual is ByteArray -> assertContentEquals(expected, actual, message) + expected is CharArray && actual is CharArray -> assertContentEquals(expected, actual, message) + expected is DoubleArray && actual is DoubleArray -> assertContentEquals(expected, actual, message) + expected is FloatArray && actual is FloatArray -> assertContentEquals(expected, actual, message) + expected is IntArray && actual is IntArray -> assertContentEquals(expected, actual, message) + expected is LongArray && actual is LongArray -> assertContentEquals(expected, actual, message) + expected is ShortArray && actual is ShortArray -> assertContentEquals(expected, actual, message) + expected is UByteArray && actual is UByteArray -> assertContentEquals(expected, actual, message) + expected is UIntArray && actual is UIntArray -> assertContentEquals(expected, actual, message) + expected is ULongArray && actual is ULongArray -> assertContentEquals(expected, actual, message) + expected is UShortArray && actual is UShortArray -> assertContentEquals(expected, actual, message) + else -> assertEquals(expected, actual, message) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverterTest.kt new file mode 100644 index 00000000000..cc67cfde4d4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/ListConverterTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test + +class ListConverterTest : ValueConvertersTest() { + @Test + fun testListConverter() = given(ListConverter(FooConverter)) { + listOf(Foo("apple", 1), Foo("banana", 2), Foo("cherry", 3)) inDdbIs listOf( + mapOf("bar" to "apple", "baz" to 1), + mapOf("bar" to "banana", "baz" to 2), + mapOf("bar" to "cherry", "baz" to 3), + ) + + List(3) { Foo("date", 4) } inDdbIs List(3) { mapOf("bar" to "date", "baz" to 4) } + + listOf() inDdbIs theSame + } +} + +private data class Foo(val bar: String, val baz: Int) + +private object FooConverter : ValueConverter { + override fun convertFrom(to: AttributeValue): Foo { + val map = to.asM() + val bar = map.getValue("bar").asS() + val baz = map.getValue("baz").asN().toInt() + return Foo(bar, baz) + } + + override fun convertTo(from: Foo) = AttributeValue.M( + mapOf( + "bar" to attr(from.bar), + "baz" to attr(from.baz), + ), + ) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverterTest.kt new file mode 100644 index 00000000000..a9e65525349 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/MapConverterTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.util.attr +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test + +class MapConverterTest : ValueConvertersTest() { + @Test + fun testMapConverter() = given(MapConverter(BarConverter)) { + mapOf("short" to Bar(false, "meh"), "long" to Bar(true, "m", "e", "h")) inDdbIs mapOf( + "short" to listOf(false, "meh"), + "long" to listOf(true, "m", "e", "h"), + ) + + mapOf() inDdbIs theSame + } +} + +private data class Bar(val foo: Boolean, val baz: List) { + constructor(foo: Boolean, vararg baz: String) : this(foo, baz.toList()) +} + +private object BarConverter : ValueConverter { + override fun convertFrom(to: AttributeValue): Bar { + val list = to.asL() + val foo = list.first().asBool() + val baz = list.drop(1).map { it.asS() } + return Bar(foo, baz) + } + + override fun convertTo(from: Bar) = AttributeValue.L(listOf(attr(from.foo)) + from.baz.map(::attr)) +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/SetConvertersTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/SetConvertersTest.kt new file mode 100644 index 00000000000..8b2a32b19f1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/collections/SetConvertersTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.collections + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test + +private val emptyNumberSet = AttributeValue.Ns(listOf()) + +class SetConvertersTest : ValueConvertersTest() { + @Test + fun testByteArraySetConverter() = given(ByteArraySetConverter) { + setOf(byteArrayOf(1, 1, 2, 3), byteArrayOf(5, 8), byteArrayOf(13, 21, 34, 55, 89)) inDdbIs theSame + setOf() inDdbIs AttributeValue.Bs(listOf()) + } + + @Test + fun testByteSetConverter() = given(ByteSetConverter) { + setOf(1.toByte(), 5.toByte(), Byte.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testCharSetConverter() = given(CharSetConverter) { + setOf('a', 'b', 'A', 'B') inDdbIs setOf("a", "b", "A", "B") + setOf() inDdbIs AttributeValue.Ss(listOf()) + } + + @Test + fun testDoubleSetConverter() = given(DoubleSetConverter) { + setOf(1.0, -3.14159, Double.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testFloatSetConverter() = given(FloatSetConverter) { + setOf(1.0f, -3.14159f, Float.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testIntSetConverter() = given(IntSetConverter) { + setOf(392, -5_129_352, Int.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testLongSetConverter() = given(LongSetConverter) { + setOf(392L, -5_129_352_000_000_000L, Long.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testShortSetConverter() = given(ShortSetConverter) { + setOf(392.toShort(), (-1024).toShort(), Short.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun tesStringSetConverter() = given(StringSetConverter) { + setOf("The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dogs") inDdbIs theSame + setOf() inDdbIs AttributeValue.Ss(listOf()) + } + + @Test + fun testUByteSetConverter() = given(UByteSetConverter) { + setOf(1.toUByte(), (Byte.MAX_VALUE.toInt() + 1).toUByte(), UByte.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testUIntSetConverter() = given(UIntSetConverter) { + setOf(392u, Int.MAX_VALUE.toUInt() + 1u, UInt.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testULongSetConverter() = given(ULongSetConverter) { + setOf(392uL, Long.MAX_VALUE.toULong() + 1uL, ULong.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } + + @Test + fun testUShortSetConverter() = given(UShortSetConverter) { + setOf(392.toUShort(), (Short.MAX_VALUE.toInt() + 1).toUShort(), UShort.MAX_VALUE) inDdbIs theSame + setOf() inDdbIs emptyNumberSet + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ScalarConvertersTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ScalarConvertersTest.kt new file mode 100644 index 00000000000..18a2ecb9053 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/scalars/ScalarConvertersTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.scalars + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import kotlin.test.Test + +class ScalarConvertersTest : ValueConvertersTest() { + @Test + fun testBooleanConverter() = given(BooleanConverter) { + true inDdbIs theSame + false inDdbIs theSame + } + + @Test + fun testByteArrayConverter() = given(ByteArrayConverter) { + byteArrayOf() inDdbIs theSame + "Foo".encodeToByteArray() inDdbIs theSame + ByteArray(1024) { it.toByte() } inDdbIs theSame + } + + @Test + fun testByteConverter() = given(ByteConverter) { + 1.toByte() inDdbIs theSame + 47.toByte() inDdbIs theSame + Byte.MIN_VALUE inDdbIs theSame + } + + @Test + fun testCharArrayConverter() = given(CharArrayConverter) { + charArrayOf() inDdbIs "" + charArrayOf('G', 'u', 'i', 'n', 'e', 'a', ' ', 'p', 'i', 'g') inDdbIs "Guinea pig" + } + + @Test + fun testCharConverter() = given(CharConverter) { + '!' inDdbIs "!" + 'X' inDdbIs "X" + '7' inDdbIs "7" + } + + @Test + fun testDoubleConverter() = given(DoubleConverter) { + (-1.41421) inDdbIs theSame + 2.71828 inDdbIs theSame + 3.14159 inDdbIs theSame + 6.62607e-34 inDdbIs theSame + Double.NEGATIVE_INFINITY inDdbIs anError + Double.NaN inDdbIs anError + } + + @Test + fun testEnumConverter() = given(EnumConverter()) { + Suit.HEARTS inDdbIs "HEARTS" + Suit.DIAMONDS inDdbIs "DIAMONDS" + Suit.CLUBS inDdbIs "CLUBS" + Suit.SPADES inDdbIs "SPADES" + } + + @Test + fun testFloatConverter() = given(FloatConverter) { + (-1.73205f) inDdbIs theSame + 1.61803f inDdbIs theSame + 2.68545f inDdbIs theSame + Float.POSITIVE_INFINITY inDdbIs anError + Float.NaN inDdbIs anError + } + + @Test + fun testIntConverter() = given(IntConverter) { + 0 inDdbIs theSame + 1_000_000 inDdbIs theSame + Int.MIN_VALUE inDdbIs theSame + } + + @Test + fun testLongConverter() = given(LongConverter) { + 31_536_000L inDdbIs theSame + 9_460_730_472_580_800L inDdbIs theSame + Long.MIN_VALUE inDdbIs theSame + } + + @Test + fun testShortConverter() = given(ShortConverter) { + 1.toShort() inDdbIs theSame + 47.toShort() inDdbIs theSame + Short.MIN_VALUE inDdbIs theSame + } + + @Test + fun testStringConverter() = given(StringConverter) { + "The quick brown fox jumped over the lazy dogs" inDdbIs theSame + "Jackdaws love my big sphinx of quartz" inDdbIs theSame + """ + Benjamín pidió una bebida de kiwi y fresa. + Noé, sin vergüenza, la más exquisita champaña del menú. + """.trimIndent() inDdbIs theSame + } + + @Test + fun testUByteConverter() = given(UByteConverter) { + 1.toUByte() inDdbIs 1 + 47.toUByte() inDdbIs 47 + UByte.MAX_VALUE inDdbIs UByte.MAX_VALUE.toInt() + } + + @Test + fun testUIntConverter() = given(UIntConverter) { + 0u inDdbIs 0 + 1_000_000u inDdbIs 1_000_000 + UInt.MAX_VALUE inDdbIs UInt.MAX_VALUE.toLong() + } + + @Test + fun testULongConverter() = given(ULongConverter) { + 31_536_000uL inDdbIs 31_536_000L + 9_460_730_472_580_800uL inDdbIs 9_460_730_472_580_800L + ULong.MAX_VALUE inDdbIs AttributeValue.N(ULong.MAX_VALUE.toString()) + } + + @Test + fun testUShortConverter() = given(UShortConverter) { + 1.toUShort() inDdbIs 1 + 47.toUShort() inDdbIs 47 + UShort.MAX_VALUE inDdbIs UShort.MAX_VALUE.toInt() + } +} + +private enum class Suit { + HEARTS, + DIAMONDS, + CLUBS, + SPADES, +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentValueConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentValueConverterTest.kt new file mode 100644 index 00000000000..e0daa9843d1 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/DocumentValueConverterTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.smithy.kotlin.runtime.content.buildDocument +import kotlin.test.Test + +class DocumentValueConverterTest : ValueConvertersTest() { + @Test + fun testKitchenSink() = given(DocumentConverter.Default) { + val expectedDocument = buildDocument { + "name" to "Ian" + "pets" to buildList { + add("Arrayah") + add("Coffee") + add("Willow") + add("Bambi") + add("Ginny") + add("Tori") + add("Chewy") + add("Maisy") + add("Pineapple") + add("Gizmo") + add("Bug") + } + "inactive?" to false + "employer" to buildDocument { + "name" to "Amazon" + "founded" to 1994 + "offices" to buildList { + add("Seattle") + add("New York") + add("Boston") + } + "parentCompany" to null + } + "favoriteThings" to buildList { + add("chocolate chip cookies") + add(13) + add(true) + add(null) + } + } + + val expectedValue = mapOf( + "name" to "Ian", + "pets" to listOf( + "Arrayah", + "Coffee", + "Willow", + "Bambi", + "Ginny", + "Tori", + "Chewy", + "Maisy", + "Pineapple", + "Gizmo", + "Bug", + ), + "inactive?" to false, + "employer" to mapOf( + "name" to "Amazon", + "founded" to 1994, + "offices" to listOf( + "Seattle", + "New York", + "Boston", + ), + "parentCompany" to null, + ), + "favoriteThings" to listOf( + "chocolate chip cookies", + 13, + true, + null, + ), + ) + + expectedDocument inDdbIs expectedValue + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConvertersTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConvertersTest.kt new file mode 100644 index 00000000000..cd49217b39d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/InstantConvertersTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.smithy.kotlin.runtime.time.Instant +import kotlin.test.Test + +private val WHOLE_TIME = Instant.fromEpochSeconds(1234567890L) // 2009-02-13T23:31:30Z +private val MS_TIME = Instant.fromEpochSeconds(1234567890L, 123000000) // 2009-02-13T23:31:30.123Z +private val MICRO_TIME = Instant.fromEpochSeconds(1234567890L, 123456000) // 2009-02-13T23:31:30.123456Z +private val NS_TIME = Instant.fromEpochSeconds(1234567890L, 123456789) // 2009-02-13T23:31:30.123456789Z + +class InstantConvertersTest : ValueConvertersTest() { + @Test + fun testEpochS() = given(InstantConverter.EpochS) { + WHOLE_TIME inDdbIs 1234567890L + NS_TIME inDdbIs 1234567890L whenGoing Direction.TO_ATTRIBUTE_VALUE + } + + @Test + fun testEpochMs() = given(InstantConverter.EpochMs) { + WHOLE_TIME inDdbIs 1234567890000L + NS_TIME inDdbIs 1234567890123L whenGoing Direction.TO_ATTRIBUTE_VALUE + MS_TIME inDdbIs 1234567890123L whenGoing Direction.FROM_ATTRIBUTE_VALUE + } + + @Test + fun testIso8601() = given(InstantConverter.Iso8601) { + WHOLE_TIME inDdbIs "2009-02-13T23:31:30Z" + MS_TIME inDdbIs "2009-02-13T23:31:30.123Z" + MICRO_TIME inDdbIs "2009-02-13T23:31:30.123456Z" + NS_TIME inDdbIs "2009-02-13T23:31:30.123456789Z" + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverterTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverterTest.kt new file mode 100644 index 00000000000..6c57585379d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/values/smithytypes/UrlConverterTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes + +import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConvertersTest +import aws.smithy.kotlin.runtime.net.url.Url +import kotlin.test.Test + +private const val URL = "https://foo.bar.baz/qux?quux=corge#grault" + +class UrlConverterTest : ValueConvertersTest() { + @Test + fun testUrlConverter() = given(UrlConverter) { + Url.parse(URL) inDdbIs URL + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapperTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapperTest.kt new file mode 100644 index 00000000000..5b49c3ba0b2 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapperTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.operations.scanPaginated +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric +import aws.sdk.kotlin.services.dynamodb.scan +import aws.sdk.kotlin.services.dynamodb.withConfig +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.test.runTest +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DynamoDbMapperTest : DdbLocalTest() { + companion object { + private const val TABLE_NAME = "dummy" + + private data class DummyData(var foo: String = "", var bar: Int = 0) + + private val dummyConverter = SimpleItemConverter( + ::DummyData, + { this }, + AttributeDescriptor("foo", DummyData::foo, DummyData::foo::set, StringConverter), + AttributeDescriptor("bar", DummyData::bar, DummyData::bar::set, IntConverter), + ) + + private val dummySchema = ItemSchema(dummyConverter, KeySpec.String("foo"), KeySpec.Number("bar")) + } + + @BeforeAll + fun setUp() = runTest { + createTable(TABLE_NAME, dummySchema) + } + + @Test + fun testBusinessMetricEmission() = runTest { + val interceptor = MetricCapturingInterceptor() + + val ddb = lowLevelAccess { withConfig { interceptors += interceptor } } + interceptor.assertEmpty() + + // No metric for low-level client + lowLevelAccess { scan { tableName = TABLE_NAME } } + interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false) + interceptor.reset() + + // Metric for high-level client + val mapper = mapper(ddb) + val table = mapper.getTable(TABLE_NAME, dummySchema) + table.scanPaginated { }.collect() + interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER) + interceptor.reset() + + // Still no metric for low-level client (i.e., LL wasn't modified by HL) + lowLevelAccess { scan { tableName = TABLE_NAME } } + interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false) + interceptor.reset() + + // Original client can be closed, mapper is unaffected + lowLevelAccess { close() } + table.scanPaginated { }.collect() + interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER) + } +} + +private class MetricCapturingInterceptor : HttpInterceptor { + private val capturedMetrics = mutableSetOf() + + override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext) { + capturedMetrics += context.executionContext[BusinessMetrics] + } + + fun assertMetric(metric: BusinessMetric, exists: Boolean = true) { + if (exists) { + assertTrue( + metric.identifier in capturedMetrics, + "Expected metrics to contain $metric. Actual values: $capturedMetrics", + ) + } else { + assertFalse( + metric.identifier in capturedMetrics, + "Expected metrics *not* to contain $metric. Actual values: $capturedMetrics", + ) + } + } + + fun assertEmpty() { + assertTrue(capturedMetrics.isEmpty(), "Expected metrics to be empty. Actual values: $capturedMetrics") + } + + fun reset() { + capturedMetrics.clear() + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemTest.kt new file mode 100644 index 00000000000..8c56a0d6eb2 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/DeleteItemTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.operations + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.services.dynamodb.model.ReturnConsumedCapacity +import aws.sdk.kotlin.services.dynamodb.model.ReturnValue +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class DeleteItemTest : DdbLocalTest() { + companion object { + private const val TABLE_NAME = "delete-item-test" + + private data class Item(var id: String = "", var value: Int = 0) + + private val converter = SimpleItemConverter( + ::Item, + { this }, + AttributeDescriptor("id", Item::id, Item::id::set, StringConverter), + AttributeDescriptor("value", Item::value, Item::value::set, IntConverter), + ) + private val schema = ItemSchema(converter, KeySpec.String("id")) + } + + @BeforeAll + fun setUp() = runTest { + createTable( + TABLE_NAME, + schema, + itemOf("id" to "foo", "value" to 42), + ) + } + + @Test + fun testDeleteItem() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + val resp = table.deleteItem { + key = Item(id = "foo") + returnConsumedCapacity = ReturnConsumedCapacity.Indexes + returnValues = ReturnValue.AllOld + } + + val item = assertNotNull(resp.attributes) + assertEquals("foo", item.id) + assertEquals(42, item.value) + + val cc = assertNotNull(resp.consumedCapacity) + assertEquals(1.0, cc.capacityUnits) + assertEquals(TABLE_NAME, cc.tableName) + + val tableCapacity = assertNotNull(cc.table) + assertEquals(1.0, tableCapacity.capacityUnits) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemTest.kt new file mode 100644 index 00000000000..fff349cb2b9 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/GetItemTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.operations + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.model.Table +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.services.dynamodb.model.ReturnConsumedCapacity +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class GetItemTest : DdbLocalTest() { + companion object { + private const val PK_TABLE_NAME = "get-item-test-pk" + private const val CK_TABLE_NAME = "get-item-test-ck" + + private data class PkItem(var id: Int = 0, var value: String = "") + + private val pkConverter = SimpleItemConverter( + ::PkItem, + { this }, + AttributeDescriptor("id", PkItem::id, PkItem::id::set, IntConverter), + AttributeDescriptor("value", PkItem::value, PkItem::value::set, StringConverter), + ) + private val pkSchema = ItemSchema(pkConverter, KeySpec.Number("id")) + + private data class CkItem(var id: String = "", var version: Int = 0, var value: String = "") + + private val ckConverter = SimpleItemConverter( + ::CkItem, + { this }, + AttributeDescriptor("id", CkItem::id, CkItem::id::set, StringConverter), + AttributeDescriptor("version", CkItem::version, CkItem::version::set, IntConverter), + AttributeDescriptor("value", CkItem::value, CkItem::value::set, StringConverter), + ) + private val ckSchema = ItemSchema(ckConverter, KeySpec.String("id"), KeySpec.Number("version")) + } + + @BeforeAll + fun setUp() = runTest { + createTable(PK_TABLE_NAME, pkSchema, itemOf("id" to 1, "value" to "foo")) + createTable(CK_TABLE_NAME, ckSchema, itemOf("id" to "abcd", "version" to 42, "value" to "foo")) + } + + private fun testGetItem( + vararg keys: PkItem, + returnConsumedCapacity: ReturnConsumedCapacity? = null, + action: (GetItemResponse) -> Unit, + ) = testGetItem(mapper().getTable(PK_TABLE_NAME, pkSchema), returnConsumedCapacity, keys.toList(), action) + + private fun testGetItem( + vararg keys: CkItem, + returnConsumedCapacity: ReturnConsumedCapacity? = null, + action: (GetItemResponse) -> Unit, + ) = testGetItem(mapper().getTable(CK_TABLE_NAME, ckSchema), returnConsumedCapacity, keys.toList(), action) + + private fun testGetItem( + table: Table, + returnConsumedCapacity: ReturnConsumedCapacity? = null, + keys: List, + action: (GetItemResponse) -> Unit, + ) = runTest { + keys.forEach { key -> + val response = table.getItem { + this.key = key + this.returnConsumedCapacity = returnConsumedCapacity + } + + action(response) + } + } + + @Test + fun testPkGetItem() = testGetItem(PkItem(id = 1)) { + val item = assertNotNull(it.item) + assertEquals(1, item.id) + assertEquals("foo", item.value) + } + + @Test + fun testPkGetItemInvalidKey() = testGetItem( + PkItem(id = 2), + PkItem(), + ) { + assertNull(it.item) + } + + @Test + fun testCkGetItem() = testGetItem(CkItem(id = "abcd", version = 42)) { + val item = assertNotNull(it.item) + assertEquals("abcd", item.id) + assertEquals(42, item.version) + assertEquals("foo", item.value) + } + + @Test + fun testCkGetItemInvalidKey() = testGetItem( + CkItem(id = "bcde", version = 41), + CkItem(id = "abcd", version = 41), + CkItem(id = "bcde", version = 42), + CkItem(id = "abcd"), + ) { + assertNull(it.item) + } + + @Test + fun testGetItemAdditionalParams() = testGetItem( + PkItem(id = 42), + returnConsumedCapacity = ReturnConsumedCapacity.Indexes, + ) { + val cc = assertNotNull(it.consumedCapacity) + assertEquals(0.5, cc.capacityUnits) + assertEquals(PK_TABLE_NAME, cc.tableName) + + val tableCapacity = assertNotNull(cc.table) + assertEquals(0.5, tableCapacity.capacityUnits) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemTest.kt new file mode 100644 index 00000000000..389b9a6f755 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.operations + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.getItem +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class PutItemTest : DdbLocalTest() { + companion object { + private const val TABLE_NAME = "put-item-test" + + private data class Item(var id: String = "", var value: Int = 0) + + private val converter = SimpleItemConverter( + ::Item, + { this }, + AttributeDescriptor("id", Item::id, Item::id::set, StringConverter), + AttributeDescriptor("value", Item::value, Item::value::set, IntConverter), + ) + private val schema = ItemSchema(converter, KeySpec.String("id")) + } + + @BeforeAll + fun setUp() = runTest { + createTable(TABLE_NAME, schema) + } + + @Test + fun testPutItem() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + table.putItem { item = Item(id = "foo", value = 42) } + + val resp = lowLevelAccess { getItem(TABLE_NAME, "id" to "foo") } + + val item = assertNotNull(resp.item) + assertEquals("foo", item["id"]?.asSOrNull()) + assertEquals(42, item["value"]?.asNOrNull()?.toIntOrNull()) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryTest.kt new file mode 100644 index 00000000000..fce2dedfdfd --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/QueryTest.kt @@ -0,0 +1,265 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.operations + +import aws.sdk.kotlin.hll.dynamodbmapper.expressions.KeyFilter +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import kotlin.test.assertContentEquals + +class QueryTest : DdbLocalTest() { + companion object { + private const val TABLE_NAME = "query-test" + private const val TITLE_INDEX_NAME = "emps-by-title" + private const val NAME_INDEX_NAME = "emps-by-name" + + private data class NamedEmp( + var companyId: String = "", + var empId: String = "", + var name: String = "", + var title: String = "", + var tenureYears: Int = 0, + ) + + private val empConverter = SimpleItemConverter( + ::NamedEmp, + { this }, + AttributeDescriptor("companyId", NamedEmp::companyId, NamedEmp::companyId::set, StringConverter), + AttributeDescriptor("empId", NamedEmp::empId, NamedEmp::empId::set, StringConverter), + AttributeDescriptor("name", NamedEmp::name, NamedEmp::name::set, StringConverter), + AttributeDescriptor("title", NamedEmp::title, NamedEmp::title::set, StringConverter), + AttributeDescriptor("tenureYears", NamedEmp::tenureYears, NamedEmp::tenureYears::set, IntConverter), + ) + + private val namedEmpSchema = ItemSchema(empConverter, KeySpec.String("companyId"), KeySpec.String("empId")) + private val empsByNameSchema = ItemSchema(empConverter, KeySpec.String("companyId"), KeySpec.String("name")) + + private data class TitleEmp( + var title: String = "", + var name: String = "", + var companyId: String = "", + var empId: String = "", + ) + + private val titleConverter = SimpleItemConverter( + ::TitleEmp, + { this }, + AttributeDescriptor("title", TitleEmp::title, TitleEmp::title::set, StringConverter), + AttributeDescriptor("name", TitleEmp::name, TitleEmp::name::set, StringConverter), + AttributeDescriptor("empId", TitleEmp::empId, TitleEmp::empId::set, StringConverter), + AttributeDescriptor("companyId", TitleEmp::companyId, TitleEmp::companyId::set, StringConverter), + ) + + private val titleSchema = ItemSchema(titleConverter, KeySpec.String("title"), KeySpec.String("name")) + } + + @BeforeAll + fun setUp() = runTest { + createTable( + name = TABLE_NAME, + schema = namedEmpSchema, + gsis = mapOf(TITLE_INDEX_NAME to titleSchema), + lsis = mapOf(NAME_INDEX_NAME to empsByNameSchema), + items = listOf( + itemOf( + "companyId" to "foo-corp", + "empId" to "AB0123", + "name" to "Alice Birch", + "title" to "SDE", + "tenureYears" to 5, + ), + itemOf( + "companyId" to "foo-corp", + "empId" to "AB0126", + "name" to "Adriana Beech", + "title" to "Manager", + "tenureYears" to 7, + ), + itemOf( + "companyId" to "foo-corp", + "empId" to "EF0124", + "name" to "Eddie Fraser", + "title" to "SDE", + "tenureYears" to 3, + ), + itemOf( + "companyId" to "bar-corp", + "empId" to "157X", + "name" to "Charlie Douglas", + "title" to "Manager", + "tenureYears" to 4, + ), + ), + ) + } + + @Test + fun testQueryTable() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, namedEmpSchema) + + val items = table.queryPaginated { + keyCondition = KeyFilter("foo-corp") + }.items().toList() + + val expected = listOf( + NamedEmp( + companyId = "foo-corp", + empId = "AB0123", + name = "Alice Birch", + title = "SDE", + tenureYears = 5, + ), + NamedEmp( + companyId = "foo-corp", + empId = "AB0126", + name = "Adriana Beech", + title = "Manager", + tenureYears = 7, + ), + NamedEmp( + companyId = "foo-corp", + empId = "EF0124", + name = "Eddie Fraser", + title = "SDE", + tenureYears = 3, + ), + ) + + assertContentEquals(expected, items) + } + + @Test + fun testQueryTableWithSortKeyCondition() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, namedEmpSchema) + + val items = table.queryPaginated { + keyCondition = KeyFilter("foo-corp") { sortKey startsWith "AB0" } + }.items().toList() + + val expected = listOf( + NamedEmp( + companyId = "foo-corp", + empId = "AB0123", + name = "Alice Birch", + title = "SDE", + tenureYears = 5, + ), + NamedEmp( + companyId = "foo-corp", + empId = "AB0126", + name = "Adriana Beech", + title = "Manager", + tenureYears = 7, + ), + ) + + assertContentEquals(expected, items) + } + + @Test + fun testQueryTableWithFilter() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, namedEmpSchema) + + val items = table.queryPaginated { + keyCondition = KeyFilter("foo-corp") + filter { attr("title") eq "SDE" } + }.items().toList() + + val expected = listOf( + NamedEmp( + companyId = "foo-corp", + empId = "AB0123", + name = "Alice Birch", + title = "SDE", + tenureYears = 5, + ), + NamedEmp( + companyId = "foo-corp", + empId = "EF0124", + name = "Eddie Fraser", + title = "SDE", + tenureYears = 3, + ), + ) + + assertContentEquals(expected, items) + } + + @Test + fun testQueryGsi() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, namedEmpSchema) + val index = table.getIndex(TITLE_INDEX_NAME, titleSchema) + + val items = index.queryPaginated { + keyCondition = KeyFilter("Manager") + }.items().toList() + + val expected = listOf( + TitleEmp( + companyId = "foo-corp", + empId = "AB0126", + name = "Adriana Beech", + title = "Manager", + ), + TitleEmp( + companyId = "bar-corp", + empId = "157X", + name = "Charlie Douglas", + title = "Manager", + ), + ) + + assertContentEquals(expected, items) + } + + @Test + fun testQueryLsi() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, namedEmpSchema) + val index = table.getIndex(NAME_INDEX_NAME, empsByNameSchema) + + val items = index.queryPaginated { + keyCondition = KeyFilter("foo-corp") + }.items().toList() + + val expected = listOf( + NamedEmp( + companyId = "foo-corp", + empId = "AB0126", + name = "Adriana Beech", + title = "Manager", + tenureYears = 7, + ), + NamedEmp( + companyId = "foo-corp", + empId = "AB0123", + name = "Alice Birch", + title = "SDE", + tenureYears = 5, + ), + NamedEmp( + companyId = "foo-corp", + empId = "EF0124", + name = "Eddie Fraser", + title = "SDE", + tenureYears = 3, + ), + ) + + assertContentEquals(expected, items) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanTest.kt new file mode 100644 index 00000000000..8758d4f0b99 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/ScanTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.operations + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.smithytypes.InstantConverter +import aws.smithy.kotlin.runtime.time.Instant +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toSet +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals + +class ScanTest : DdbLocalTest() { + companion object { + private const val TABLE_NAME = "scan-test" + + private data class Product( + var id: Int = 0, + var name: String = "", + var modelNumber: String = "", + var released: Instant = Instant.now(), + ) + + private val converter = SimpleItemConverter( + ::Product, + { this }, + AttributeDescriptor("id", Product::id, Product::id::set, IntConverter), + AttributeDescriptor("name", Product::name, Product::name::set, StringConverter), + AttributeDescriptor("modelNumber", Product::modelNumber, Product::modelNumber::set, StringConverter), + AttributeDescriptor("released", Product::released, Product::released::set, InstantConverter.Iso8601), + ) + + private val schema = ItemSchema(converter, KeySpec.Number("id")) + } + + @BeforeAll + fun setUp() = runTest { + createTable( + name = TABLE_NAME, + schema = schema, + itemOf( + "id" to 1, + "name" to "Standard Widget", + "modelNumber" to "wid-001", + "released" to "2024-09-24T10:15:23Z", + ), + itemOf( + "id" to 2, + "name" to "Widget Plus+", + "modelNumber" to "wid-010", + "released" to "2024-09-25T10:15:23Z", + ), + itemOf( + "id" to 3, + "name" to "Gizmo", + "modelNumber" to "GIZ.m0", + "released" to "2024-09-20T10:15:23Z", + ), + itemOf( + "id" to 4, + "name" to "Thingy", + "modelNumber" to "T1", + "released" to "2024-09-19T10:15:23Z", + ), + itemOf( + "id" to 5, + "name" to "Doohickey", + "modelNumber" to "doohick/x", + "released" to "2024-09-21T10:15:23Z", + ), + ) + } + + @Test + fun testScanTable() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + table.scanPaginated { }.assertItems("Standard Widget", "Widget Plus+", "Gizmo", "Thingy", "Doohickey") + } + + @Test + fun testScanTableWithComparisonFilter() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + table.scanPaginated { + filter { attr("id") gt 2 } + }.assertItems("Gizmo", "Thingy", "Doohickey") + } + + @Test + fun testScanTableWithRangeFilter() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + table.scanPaginated { + filter { attr("released") isIn "2024-09-21T00:00:00Z".."2024-09-25T00:00:00Z" } + }.assertItems("Standard Widget", "Doohickey") + } + + @Test + fun testScanTableWithSizeFilter() = runTest { + val mapper = mapper() + val table = mapper.getTable(TABLE_NAME, schema) + + table.scanPaginated { + filter { attr("name").size gte 10 } + }.assertItems("Standard Widget", "Widget Plus+") + } + + private suspend fun Flow>.assertItems(vararg names: String) { + // Use sets for comparison because DDB partition keys aren't always sorted the way one might expect + val expected = names.toSet() + val actual = items().map { it.name }.toSet() + + assertEquals(expected, actual) + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/OperationTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/OperationTest.kt new file mode 100644 index 00000000000..d758cb581f4 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/pipeline/internal/OperationTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.pipeline.internal + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.withKeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.PersistenceSpec +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.* +import aws.sdk.kotlin.services.dynamodb.model.AttributeValue +import io.mockk.* +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs + +private const val TABLE_NAME = "foo-table" + +// FIXME Should be in commonTest but mockk is JVM-only and finding a good KMP mocking library is hard + +class OperationTest { + private val ddbMapper = mockk() + + private val fooTable = object : PersistenceSpec { + override val mapper = ddbMapper + override val schema = fooSchema + } + + private fun interceptor(name: String) = + spyk(object : Interceptor {}, name) + + private val interceptorA = interceptor("A") + private val interceptorB = interceptor("B") + private val interceptors = listOf(interceptorA, interceptorB) + + private fun initialize(hReq: HFooRequest) = HReqContextImpl(hReq, fooSchema, MapperContextImpl(fooTable, "FooOp")) + + private fun op( + initialize: (HFooRequest) -> HReqContextImpl = ::initialize, + serialize: (HFooRequest, ItemSchema) -> LFooRequest = { hReq, schema -> hReq.convert(TABLE_NAME, schema) }, + lowLevelInvoke: suspend (LFooRequest) -> LFooResponse = ::dummyInvoke, + deserialize: (LFooResponse, ItemSchema) -> HFooResponse = { lRes, schema -> lRes.convert(schema) }, + interceptors: Collection = this.interceptors, + ) = Operation(initialize, serialize, lowLevelInvoke, deserialize, interceptors) + + @Test + fun testFullInvocationOrder() = runTest { + val res = op().execute(HFooRequest(Foo("the foo"))) + assertEquals("the foo", res.foo.value) // Sanity check + + verifyOrder { + interceptorA.readAfterInitialization(any()) + interceptorB.readAfterInitialization(any()) + + interceptorA.modifyBeforeSerialization(any()) + interceptorB.modifyBeforeSerialization(any()) + + interceptorA.readBeforeSerialization(any()) + interceptorB.readBeforeSerialization(any()) + + interceptorA.readAfterSerialization(any()) + interceptorB.readAfterSerialization(any()) + + interceptorA.modifyBeforeInvocation(any()) + interceptorB.modifyBeforeInvocation(any()) + + interceptorA.readBeforeInvocation(any()) + interceptorB.readBeforeInvocation(any()) + + // Interceptor invocation order flips here + + interceptorB.readAfterInvocation(any()) + interceptorA.readAfterInvocation(any()) + + interceptorB.modifyBeforeDeserialization(any()) + interceptorA.modifyBeforeDeserialization(any()) + + interceptorB.readBeforeDeserialization(any()) + interceptorA.readBeforeDeserialization(any()) + + interceptorB.readAfterDeserialization(any()) + interceptorA.readAfterDeserialization(any()) + + interceptorB.modifyBeforeCompletion(any()) + interceptorA.modifyBeforeCompletion(any()) + + interceptorB.readBeforeCompletion(any()) + interceptorA.readBeforeCompletion(any()) + } + } + + @Test + fun testModifyHook() = runTest { + every { interceptorA.modifyBeforeSerialization(any()) } answers { + val ctx = assertIs>(it.invocation.args[0]) + SerializeInputImpl(HFooRequest(Foo(ctx.highLevelRequest.foo.value.reversed())), ctx.serializeSchema) + } + + val res = op().execute(HFooRequest(Foo("the foo"))) + assertEquals("oof eht", res.foo.value) // Should be reversed + } + + @Test + fun testReadOnlyHookErrorIsThrown() = runTest { + every { interceptorA.readAfterSerialization(any()) } throws RuntimeException("Cannot foo!") + + every { interceptorB.readAfterSerialization(any()) } answers { + val ctx = assertIs>(it.invocation.args[0]) + val ex = assertIs(ctx.error) + assertEquals("Cannot foo!", ex.message) + } + + assertFailsWith("Cannot foo!") { + op().execute(HFooRequest(Foo("the foo"))) + } + + verifyOrder { + interceptorA.readAfterSerialization(any()) + interceptorB.readAfterSerialization(any()) + } + + // Should not continue to later interceptors + verify(inverse = true) { + interceptorA.modifyBeforeInvocation(any()) + interceptorB.modifyBeforeInvocation(any()) + } + } + + @Test + fun testModifyHookErrorIsThrown() = runTest { + every { interceptorA.modifyBeforeSerialization(any()) } throws RuntimeException("Cannot foo!") + + interceptors.forEach { interceptor -> + every { interceptor.readBeforeSerialization(any()) } answers { + val ctx = assertIs>(it.invocation.args[0]) + val ex = assertIs(ctx.error) + assertEquals("Cannot foo!", ex.message) + } + } + + assertFailsWith("Cannot foo!") { + op().execute(HFooRequest(Foo("the foo"))) + } + + verifyOrder { + interceptorA.modifyBeforeSerialization(any()) + interceptorA.readBeforeSerialization(any()) + interceptorB.readBeforeSerialization(any()) + } + + // Should not continue to later interceptors + verify(inverse = true) { + interceptorB.modifyBeforeSerialization(any()) + interceptorA.readAfterSerialization(any()) + interceptorB.readAfterSerialization(any()) + } + } +} + +private data class Foo(val value: String) + +private val fooConverter = object : ItemConverter { + override fun convertFrom(to: Item): Foo = Foo(to["foo"]!!.asS()) + override fun convertTo(from: Foo, onlyAttributes: Set?): Item = itemOf("foo" to AttributeValue.S(from.value)) +} +private val fooSchema = fooConverter.withKeySpec(KeySpec.String("foo")) + +private data class HFooRequest(val foo: Foo) +private data class LFooRequest(val table: String, val foo: Item) +private data class LFooResponse(val foo: Item) +private data class HFooResponse(val foo: Foo) + +private fun HFooRequest.convert(table: String, schema: ItemSchema) = + LFooRequest(table, schema.converter.convertTo(foo)) + +private fun LFooResponse.convert(schema: ItemSchema) = + HFooResponse(schema.converter.convertFrom(foo)) + +private suspend fun dummyInvoke(req: LFooRequest) = LFooResponse(req.foo) diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbClientExtensions.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbClientExtensions.kt new file mode 100644 index 00000000000..76bb72717e0 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbClientExtensions.kt @@ -0,0 +1,179 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.testutils + +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import aws.sdk.kotlin.services.dynamodb.batchWriteItem +import aws.sdk.kotlin.services.dynamodb.createTable +import aws.sdk.kotlin.services.dynamodb.getItem +import aws.sdk.kotlin.services.dynamodb.model.* +import aws.sdk.kotlin.services.dynamodb.waiters.waitUntilTableExists + +/** + * Creates a table with the given name/keys. This method waits for the asynchronous completion of the operation before + * returning. + * @param name The name for the new table + * @param schema The schema for the table + * @param gsis A map of GSI names to schemas + * @param lsis A map of LSI names to schemas + */ +suspend fun DynamoDbClient.createTable( + name: String, + schema: ItemSchema<*>, + gsis: Map>, + lsis: Map>, +) { + val keys = schema.toKeys() + val throughput = ProvisionedThroughput { + // provisioned throughput is required but ignored by DDB Local so just use dummy values + readCapacityUnits = 1 + writeCapacityUnits = 1 + } + + createTable { + tableName = name + + attributeDefinitions = deriveAttributes(schema, gsis, lsis) + + keySchema = keys.mapIndexed { index, key -> + KeySchemaElement { + attributeName = key.name + keyType = if (index == 0) KeyType.Hash else KeyType.Range + } + } + + provisionedThroughput = throughput + + globalSecondaryIndexes = gsis.takeUnless { it.isEmpty() }?.map { (name, schema) -> + GlobalSecondaryIndex { + indexName = name + keySchema = schema.toKeySchema() + projection = schema.toProjection() + provisionedThroughput = throughput + } + } + + localSecondaryIndexes = lsis.takeUnless { it.isEmpty() }?.map { (name, schema) -> + LocalSecondaryIndex { + indexName = name + keySchema = schema.toKeySchema() + projection = schema.toProjection() + } + } + } + + waitUntilTableExists { tableName = name } +} + +/** + * Gets an item from the given table with the given keys + * @param tableName The name of the table to fetch from + * @param keys One or more tuples of string key to value + */ +suspend fun DynamoDbClient.getItem(tableName: String, vararg keys: Pair) = getItem { + this.tableName = tableName + key = itemOf(*keys) +} + +/** + * Puts the given items into the given table + * @param tableName The name of the table to insert to + * @param items A collection of maps of strings to values to be mapped and persisted to the table + */ +suspend fun DynamoDbClient.putItems(tableName: String, items: List) { + val writeRequests = items.map { item -> + WriteRequest { + putRequest { + this.item = item + } + } + } + + if (writeRequests.isNotEmpty()) { + batchWriteItem { + requestItems = mapOf(tableName to writeRequests) + } + } +} + +/** + * Derives the [AttributeDefinition] instances for a table, taking into account its schema and any secondary indices + * @param schema The schema of the table + * @param gsis A map of GSI names to schemas + * @param lsis A map of LSI names to schemas + */ +private fun deriveAttributes( + schema: ItemSchema<*>, + gsis: Map>, + lsis: Map>, +): List { + val allKeys = schema.toKeys() + gsis.values.flatMap { it.toKeys() } + lsis.values.flatMap { it.toKeys() } + return allKeys + .associateBy(KeySpec<*>::name) + .map { (name, key) -> + AttributeDefinition { + attributeName = name + attributeType = key.toScalarAttributeType() + } + } +} + +/** + * Gets the names of all attributes comprehended by this schema. **Note**: Only works for [SimpleItemConverter] since + * that's all our tests require so far. + */ +private val ItemSchema<*>.allAttributeNames: Set + get() = when (val c = converter) { + is SimpleItemConverter<*, *> -> c.descriptors.keys + else -> error("Unsupported converter type ${c::class}") + } + +/** + * Converts this [KeySpec] to a [ScalarAttributeType] + */ +private fun KeySpec<*>.toScalarAttributeType() = when (this) { + is KeySpec.ByteArray -> ScalarAttributeType.B + is KeySpec.Number -> ScalarAttributeType.N + is KeySpec.String -> ScalarAttributeType.S +} + +/** + * Extracts the [KeySpec] instances from this [ItemSchema] + */ +private fun ItemSchema<*>.toKeys() = when (this) { + is ItemSchema.CompositeKey<*, *, *> -> listOf(partitionKey, sortKey) + is ItemSchema.PartitionKey<*, *> -> listOf(partitionKey) + else -> error("Unknown schema type ${this::class}") +} + +/** + * Derives [KeySchemaElement] instances for this schema. The first [KeySpec] in the schema is used as the partition/hash + * key. The second (if present) is used as the sort/range key. + */ +private fun ItemSchema<*>.toKeySchema() = keyAttributeNames.mapIndexed { index, key -> + KeySchemaElement { + attributeName = key + keyType = if (index == 0) KeyType.Hash else KeyType.Range + } +} + +/** + * Derives a [Projection] for this schema + */ +private fun ItemSchema<*>.toProjection() = Projection { + val projectionAttributes = allAttributeNames - keyAttributeNames + + if (projectionAttributes.isEmpty()) { + projectionType = ProjectionType.KeysOnly + } else { + projectionType = ProjectionType.Include + nonKeyAttributes = (allAttributeNames - keyAttributeNames).toList() + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbLocalTest.kt b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbLocalTest.kt new file mode 100644 index 00000000000..28c8883bc63 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper/jvm/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbLocalTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.dynamodbmapper.testutils + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema +import aws.sdk.kotlin.hll.dynamodbmapper.model.Item +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric +import aws.sdk.kotlin.services.dynamodb.DynamoDbClient +import aws.sdk.kotlin.services.dynamodb.deleteTable +import aws.sdk.kotlin.services.dynamodb.waiters.waitUntilTableNotExists +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.net.Scheme +import aws.smithy.kotlin.runtime.net.url.Url +import io.kotest.core.spec.style.AnnotationSpec +import kotlinx.coroutines.runBlocking +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +private const val DDB_LOCAL_PORT = 44212 // Keep in sync with build.gradle.kts + +/** + * The base class for test classes which need DynamoDB Local running. This class provides a few convenience declarations + * for subclasses: + * * [ddb] — An instance of a low-level [DynamoDbClient] utilizing the DynamoDB Local instance which may be used for + * setting up or verifying various mapper tests + * * [createTable] — Creates a table for the given name/schema and optionally persists the given items into it. All + * tables created via this method will be automatically dropped when the test spec completes (whether successfully or + * unsuccessfully). + * * [mapper] — Returns a [DynamoDbMapper] instance utilizing the DynamoDB Local instance + */ +abstract class DdbLocalTest : AnnotationSpec() { + companion object { + private const val DOUBLE_TOLERANCE = 0.000001 + + fun assertEquals(expected: Double, actual: Double?) { + assertNotNull(actual) + assertEquals(expected, actual, DOUBLE_TOLERANCE) + } + } + + private val requests = mutableListOf() + private val requestInterceptor = RequestCapturingInterceptor(this@DdbLocalTest.requests) + + private val ddbHolder = lazy { + DynamoDbClient { + endpointUrl = Url { + scheme = Scheme.HTTP + host = Host.Domain("localhost") + port = DDB_LOCAL_PORT + } + + region = "DUMMY" + + credentialsProvider = StaticCredentialsProvider { + accessKeyId = "DUMMY" + secretAccessKey = "DUMMY" + } + + interceptors += requestInterceptor + } + } + + /** + * An instance of a low-level [DynamoDbClient] utilizing the DynamoDB Local instance which may be used for setting + * up or verifying various mapper tests. If this is the first time accessing the value, the client will be + * initialized. + * + * **Important**: This low-level client should only be accessed via [lowLevelAccess] to ensure that User-Agent + * header verification succeeds. + */ + private val ddb by ddbHolder + + private val tempTables = mutableListOf() + + /** + * Creates a table for the given name/schema and optionally persists the given items into it. All tables created via + * this method will be automatically dropped when the test spec completes (whether successfully or unsuccessfully). + * @param name The name of the table to create + * @param schema The schema for the table. This is used to derive the primary key and to map the [items] (if any). + * @param items A collection of maps of strings to values which will be mapped and persisted to the new table before + * returning. This can be used to pre-populate a table for a test. + */ + suspend fun createTable(name: String, schema: ItemSchema<*>, vararg items: Item) = + createTable(name, schema, mapOf(), mapOf(), items.toList()) + + /** + * Creates a table for the given name/schema and optionally persists the given items into it. All tables created via + * this method will be automatically dropped when the test spec completes (whether successfully or unsuccessfully). + * @param name The name of the table to create + * @param schema The schema for the table. This is used to derive the primary key and to map the [items] (if any). + * @param gsis A map of GSI names to schemas + * @param lsis A map of LSI names to schemas + * @param items A collection of maps of strings to values which will be mapped and persisted to the new table before + * returning. This can be used to pre-populate a table for a test. + */ + suspend fun createTable( + name: String, + schema: ItemSchema<*>, + gsis: Map>, + lsis: Map>, + items: List, + ) { + lowLevelAccess { + createTable(name, schema, gsis, lsis) + tempTables += name + putItems(name, items) + } + } + + /** + * Returns a [DynamoDbMapper] instance utilizing the DynamoDB Local instance + * @param config A function to set the configuration of the mapper before it's built + */ + fun mapper( + ddb: DynamoDbClient? = null, + config: DynamoDbMapper.Config.Builder.() -> Unit = { }, + ) = DynamoDbMapper(ddb ?: this.ddb, config) + + @BeforeEach + fun initializeTest() { + requestInterceptor.enabled = true + } + + /** + * Executes requests on a low-level [DynamoDbClient] and _does not_ log any requests executed in [block]. (This + * skips verifying that low-level requests contain the [AwsBusinessMetric.DDB_MAPPER] metric.) + */ + protected suspend fun lowLevelAccess(block: suspend DynamoDbClient.() -> T): T { + requestInterceptor.enabled = false + return block(ddb).also { requestInterceptor.enabled = true } + } + + @AfterEach + fun postVerify() { + requests.forEach { req -> + val uaString = requireNotNull(req.headers["User-Agent"]) { + "Missing User-Agent header for request $req" + } + + val components = uaString.split(" ") + + val metricsComponent = requireNotNull(components.find { it.startsWith("m/") }) { + """User-Agent header "$uaString" doesn't contain business metrics for request $req""" + } + + val metrics = metricsComponent.removePrefix("m/").split(",") + + assertContains( + metrics, + AwsBusinessMetric.DDB_MAPPER.identifier, + """Mapper business metric not present in User-Agent header "$uaString" for request $req""", + ) + } + } + + @AfterAll + fun cleanUp() { + if (ddbHolder.isInitialized()) { + runBlocking { + tempTables.forEach { name -> + ddb.deleteTable { tableName = name } + ddb.waitUntilTableNotExists { tableName = name } + } + } + + ddb.close() + } + } +} + +private class RequestCapturingInterceptor(val requests: MutableList) : HttpInterceptor { + var enabled = true + + override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext) { + if (enabled) { + requests += context.protocolRequest + } + } +} diff --git a/hll/hll-codegen/build.gradle.kts b/hll/hll-codegen/build.gradle.kts new file mode 100644 index 00000000000..853bef0cedc --- /dev/null +++ b/hll/hll-codegen/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "Common code-generation utilities used by AWS SDK for Kotlin's high level libraries" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: Codegen" +extra["moduleName"] = "aws.sdk.kotlin.hll.codegen" + +plugins { + alias(libs.plugins.kotlin.jvm) + `maven-publish` +} + +val optinAnnotations = listOf( + "aws.smithy.kotlin.runtime.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi", + "kotlin.RequiresOptIn", +) + +kotlin { + explicitApi() + + sourceSets.all { + optinAnnotations.forEach(languageSettings::optIn) + } +} + +dependencies { + api(project(":aws-runtime:aws-core")) + implementation(libs.ksp.api) + implementation(libs.smithy.kotlin.runtime.core) + + testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.kotest.assertions.core.jvm) + testImplementation(libs.kotlin.test.junit5) +} + +val sourcesJar by tasks.creating(Jar::class) { + group = "publishing" + description = "Assembles Kotlin sources jar" + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +publishing { + publications { + create("hll-codegen") { + from(components["java"]) + artifact(sourcesJar) + } + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGenerator.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGenerator.kt new file mode 100644 index 00000000000..b34295a00d4 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGenerator.kt @@ -0,0 +1,239 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.runtime.InternalSdkApi + +private const val INDENT = " " + +/** + * An object which generates code into some context (typically an in-memory buffer which will eventually be written to a + * file). It includes basic methods for writing code in Kotlin such as creating blocks and indentation, docs, and + * ordinary lines of code. + * + * # String templates + * + * All methods which accept text to be written (e.g., [write], [openBlock], etc.) allow for the use of a string template + * with zero or more arguments substituted in for placeholders. See [TemplateEngine] for more details on string + * templates and argument substitution. + * + * # Indentation tracking + * + * Code generators track a running indentation level, which is used to form an indentation prefix prepended to lines + * written by this generator. The default indentation string per level is 4 spaces and the indentation level starts at 0 + * for new generators. The indentation level may be increased or decreased manually via the methods [indent] and + * [dedent]. The indentation level is also adjusted by **block** methods (e.g., [openBlock], [withBlock], etc.), which + * automatically indent/dedent around logical blocks of code. + */ +@InternalSdkApi +public interface CodeGenerator { + /** + * The import directives for the current context + */ + public val imports: ImportDirectives + + /** + * Append a blank line if there isn't already one in the buffer (i.e., invoking this method multiple times + * sequentially will append _only one_ blank line). + */ + public fun blankLine() + + /** + * Close a manually-opened block of code by dedenting and appending some finalizing text + * @param template The string template or literal to append after dedenting (e.g., `}`) + * @param args The arguments to substitute into the [template] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + */ + public fun closeBlock(template: String, vararg args: Any) + + /** + * Close a manually-opened block and open a new one by dedenting, appending some intermediate text, and then + * indenting again + * @param template The string template or literal to append after dedenting and before re-indenting (e.g., + * `} else {`) + * @param args The arguments to substitute into the [template] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + */ + public fun closeAndOpenBlock(template: String, vararg args: Any) + + /** + * Decreases the active indentation level + * @param levels The number of levels to decrement. Defaults to `1` if unspecified. + */ + public fun dedent(levels: Int = 1) + + /** + * Increases the active indentation level + * @param levels The number of levels to increment. Defaults to `1` if unspecified. + */ + public fun indent(levels: Int = 1) + + /** + * Open a block of code manually by appending some starting text and then indenting + * @param template The string template or literal to append before indenting (e.g., `if (foo) {`) + * @param args The arguments to substitute into the [template] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + */ + public fun openBlock(template: String, vararg args: Any) + + /** + * Sends the accumulated text of this generator to the backing buffer (e.g., writes it to a file) + */ + public fun persist() + + /** + * Writes a logical block of text by appending some starting text, indenting, executing the [block] function which + * may add text inside the block, dedenting, and writing some finalizing text + * @param preTemplate The string template or literal to append before indenting (e.g., `if (foo) {`) + * @param postText The string literal to append after dedenting (e.g., `}`). No templating or argument substitution + * will be performed on this string. + * @param args The arguments to substitute into the [preTemplate] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + * @param block A function to execute in between appending the [preTemplate] and before appending the [postText]. + * This function typically writes more code, which will inherit the active indentation level which will have been + * incremented by `1` by this method. + */ + public fun withBlock(preTemplate: String, postText: String, vararg args: Any, block: () -> Unit) + + /** + * Writes a block of documentation by automatically prepending KDoc-style comment tokens as prefixes. This method + * will append an opening comment token (i.e., `/**`), add a special indentation prefix (i.e., ` * `) to the + * existing indentation, execute the given [block], dedent back to the non-comment indentation prefix, and append a + * closing comment token (i.e., `*/`). + * @param block The function to execute in between appending the opening comment token and the closing comment + * token. This function typically writes more code, which will inherit the active indentation prefix and be rendered + * as a KDoc-style comment. + */ + public fun withDocs(block: () -> Unit) + + /** + * Writes a single line of documentation, wrapping it with KDoc-style comment tokens. + * @param template The templated string of documentation to write + * @param args The arguments to the templated string, if any + */ + public fun docs(template: String, vararg args: Any) + + /** + * Writes a line of text, including a terminating newline (i.e., `\n`) + * @param template The string template or literal to append + * @param args The arguments to substitute into the [template] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + */ + public fun write(template: String, vararg args: Any) + + /** + * Writes a string of text, _not_ including a terminating newline. Invoking this method repeatedly in sequence + * allows gradually forming an entire line incrementally rather than using a single [write] call. An in-progress + * line may be terminated by invoking a method such as [write] (which will continue on the current line) or + * [blankLine] (which will append a blank line). + * @param template The string template or literal to append + * @param args The arguments to substitute into the [template] (if any). See [TemplateEngine] for more details on + * string templates and argument substitution. + */ + public fun writeInline(template: String, vararg args: Any) +} + +/** + * The standard implementation of [CodeGenerator]. This implementation automatically prepends a comment indicating the + * source is codegenned, a `package` directive specified by the constructor parameter [pkg], and a set of `import` + * directives (if any) from [imports]. + * + * Example of automatically prepended content: + * + * ```kotlin + * // Code generated by dynamodb-mapper-ops-codegen. DO NOT EDIT! + * + * package foo.bar + * + * import a.A + * import b.B + * import c.C + * ``` + * @param pkg The Kotlin package for the generated code (e.g., `aws.sdk.kotlin.hll.dynamodbmapper.operations`) + * @param engine A configured template engine to use while processing string templates + * @param persistCallback A callback method to invoke when the [persist] method is called on this class + * @param imports The import directives for the generator. These may be appended by more lines being written. + */ +internal class CodeGeneratorImpl( + private val pkg: String, + private val engine: TemplateEngine, + private val persistCallback: (String) -> Unit, + override val imports: ImportDirectives = ImportDirectives(), + private val codeGeneratorName: String, +) : CodeGenerator { + private val builder = StringBuilder() + private var indent = "" + + override fun blankLine() { + if (!builder.endsWith("\n\n")) builder.append('\n') + } + + override fun closeBlock(template: String, vararg args: Any) { + if (builder.endsWith("\n\n")) builder.deleteAt(builder.length - 1) + + dedent() + write(template, *args) + } + + override fun closeAndOpenBlock(template: String, vararg args: Any) { + dedent() + write(template, *args) + indent() + } + + override fun dedent(levels: Int) { + repeat(levels) { + indent = indent.removeSuffix(INDENT) + } + } + + override fun indent(levels: Int) { + indent += INDENT.repeat(levels) + } + + override fun openBlock(template: String, vararg args: Any) { + write(template, *args) + indent() + } + + override fun persist() { + val content = buildString { + appendLine("// Code generated by $codeGeneratorName. DO NOT EDIT!") + appendLine() + appendLine("package $pkg") + appendLine() + imports.formatted.takeIf { it.isNotBlank() }?.let { appendLine(it) } + append(builder) + } + persistCallback(content) + } + + override fun withBlock(preTemplate: String, postText: String, vararg args: Any, block: () -> Unit) { + openBlock(preTemplate, *args) + block() + closeBlock(postText) + } + + override fun withDocs(block: () -> Unit) { + write("/**") + indent += " * " + block() + indent = indent.removeSuffix(" * ") + write(" */") + } + + override fun docs(template: String, vararg args: Any) = withDocs { write(template, *args) } + + override fun write(template: String, vararg args: Any) { + writeInline(template, *args) + builder.append('\n') + } + + override fun writeInline(template: String, vararg args: Any) { + val text = engine.process(template, args.toList()) + if (builder.endsWith('\n')) builder.append(indent) + builder.append(text) + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGeneratorFactory.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGeneratorFactory.kt new file mode 100644 index 00000000000..3f16deea368 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/CodeGeneratorFactory.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.runtime.InternalSdkApi +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.CodeGenerator as KSCodeGenerator + +/** + * A factory for [CodeGenerator] instances which will be backed by a [KSCodeGenerator] instance + * @param ksCodeGenerator The underlying KSP [KSCodeGenerator] to use for low-level file access and dependency tracking + * @param logger A logger instance to use for message + */ +@InternalSdkApi +public class CodeGeneratorFactory( + private val ksCodeGenerator: KSCodeGenerator, + private val logger: KSPLogger, + private val dependencies: Dependencies = Dependencies.ALL_FILES, +) { + /** + * Creates a new [CodeGenerator] backed by a [KSCodeGenerator]. The returned generator starts with no imports and + * uses a configured [TemplateEngine] with the default set of processors. + * @param fileName The name of the file which should be created _without_ parent directory or extension (which is always + * **.kt**) + * @param pkg The Kotlin package for the generated code (e.g., `aws.sdk.kotlin.hll.dynamodbmapper.operations`) + * @param codeGeneratorName The name of this [CodeGenerator] + */ + public fun generator(fileName: String, pkg: String, codeGeneratorName: String): CodeGenerator { + val imports = ImportDirectives() + val processors = listOf( + TemplateProcessor.Literal, + TemplateProcessor.QuotedString, + TemplateProcessor.forType(pkg, imports), + ) + val engine = TemplateEngine(processors) + + val persistCallback: (String) -> Unit = { content -> + logger.info("Checking out code generator for class $pkg.$fileName") + + ksCodeGenerator + .createNewFile(dependencies, pkg, fileName) + .use { outputStream -> + outputStream.writer().use { writer -> writer.append(content) } + } + } + + return CodeGeneratorImpl(pkg, engine, persistCallback, imports, codeGeneratorName) + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/ImportDirectives.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/ImportDirectives.kt new file mode 100644 index 00000000000..645ac37d78e --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/ImportDirectives.kt @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.hll.codegen.model.TypeRef +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * A mutable collection of [ImportDirectives] for eventually writing to a code generator + */ +@InternalSdkApi +public class ImportDirectives : MutableSet by mutableSetOf() { + public operator fun get(shortName: String): ImportDirective? = firstOrNull { it.shortName == shortName } + + /** + * Returns a formatted code string with each import on a dedicated line. Imports will be sorted with the following + * precedence: + * 1. Unaliased imports before alised imports + * 2. The special package prefixes `java`, `javax`, `kotlin` after all other imports + * 3. Lexicographically sorted + */ + public val formatted: String + get() = buildString { + sortedWith(importComparator).forEach { appendLine(it.formatted) } + } +} + +private val specialPrefixes = setOf( + "java.", + "javax.", + "kotlin.", +) + +private val importComparator = compareBy { it.alias != null } // aliased imports at the very end + .thenBy { directive -> specialPrefixes.any { directive.fullName.startsWith(it) } } // special prefixes < aliases + .thenBy { it.fullName } // sort alphabetically + +/** + * Describes a Kotlin `import` directive + * @param fullName The full name of the import (e.g., `java.net.Socket`) + * @param alias An optional alias for the import (e.g., `JavaSocket`). If present, a formatted code string for this + * directive will include an `as` clause (e.g., `import java.net.Socket as JavaSocket`). + */ +@InternalSdkApi +public data class ImportDirective(val fullName: String, val alias: String? = null) { + /** + * The unaliased "short name" of an import directive—namely, everything after the last `.` separator. For example, + * for the full name `java.net.Socket` the short name is `Socket`. + */ + public val shortName: String = fullName.split(".").last() + + private val aliasFormatted = alias?.let { " as $it" } ?: "" + + /** + * The formatted `import` code string for this directive + */ + public val formatted: String = "import $fullName$aliasFormatted" +} + +@InternalSdkApi +public fun ImportDirective(type: TypeRef, alias: String? = null): ImportDirective = + ImportDirective(type.fullName, alias) diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngine.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngine.kt new file mode 100644 index 00000000000..a9f8873d8a2 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngine.kt @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * A string processing engine that replaces template parameters with passed arguments to form a final string. String + * templates take the form of string literals containing zero or more **parameters**, which are special sequences + * starting with `#`. When the engine encounters these parameters, it endeavors to replace the entire parameter with an + * argument value by using a matching [TemplateProcessor]. This may fail if no matching processor is found, there is a + * mismatch between the template and passed arguments, if data types of arguments are incorrect, or other reasons, in + * which case an exception will be thrown. + * + * # Parameters + * + * All parameters start with `#` and end with an uppercase letter (`[A-Z]`). They may optionally contain a number as + * well, which makes them **Positional Parameters**. Otherwise, they are **Sequential Parameters**. Templates cannot + * contain _both_ types of parameters, otherwise an exception will be thrown. + * + * ## Sequential Parameters + * + * Parameters in the form of `#[A-Z]` are sequential parameters. The first such sequential parameter will be substituted + * with the first passed argument, the second will be replaced with the second argument, and so forth. If the number of + * sequential parameters does not match the number of arguments, an exception will be thrown. + * + * Note that with this type of parameter, substituting the same argument value multiple times requires passing that + * argument multiple times. + * + * ## Positional Parameters + * + * Parameters in the form of `#n[A-Z]` (where `n > 0`) are positional parameters. The number `n` in the parameter + * indicates the 1-based index of the argument to use for substitution. If the number `n` references an index outside + * the range of arguments (e.g., less than `0` or greater than the number of passed arguments), an exception will be + * thrown. + * + * Note that with this type of parameter, substituting the same argument value multiple times does not require _passing_ + * that argument multiple times, merely using the same `n` value for multiple parameters. + * + * # Processors + * + * Template engines reference zero or more [TemplateProcessor] instances which perform mapping from raw argument values + * to strings to be used in parameter substitution. When the template engine encounters a parameter in a string + * template, it attempts to match the parameter **key** (an uppercase letter `[A-Z]`) to the key of a processor. If no + * referenced processor has a matching key, an exception will be thrown. + */ +@InternalSdkApi +public class TemplateEngine(processors: List) { + private val processors = processors.associate { it.key to it.handler } + private val parameterRegex = """(?): String { + var allIndexed: Boolean? = null + + return parameterRegex.replaceIndexed(template) { pos, result -> + val explicitIndex = result.groupValues[1].takeUnless { it.isEmpty() }?.toInt()?.minus(1) + val indexed = explicitIndex != null + + fun req(condition: Boolean, message: () -> String) { + require(condition) { + buildString { + append("Error in template '") + append(template) + append("' for parameter '") + append(result.value) + append("' at index ") + append(result.range.first) + append(": ") + + append(message()) + } + } + } + + fun reqNotNull(element: T?, message: () -> String): T { + req(element != null, message) + return element!! + } + + req(allIndexed == null || allIndexed == indexed) { + "Cannot mix indexed and non-indexed parameters in the same template" + } + allIndexed = indexed + + val key = result.groupValues[2].toCharArray().single() + val handler = reqNotNull(processors[key]) { "No processor found for key character '$key'" } + + val index = explicitIndex ?: pos + req(index in args.indices) { + buildString { + if (indexed) { + append("Index ") + append(result.groupValues[1]) + } else { + append("Parameter index ") + append(pos) + } + append(" is outside of args bounds ") + append(args.indices) + } + } + + handler(args[index]) + } + } +} + +private fun Regex.replaceIndexed(input: CharSequence, transform: (index: Int, MatchResult) -> CharSequence): String { + var index = 0 + return replace(input) { + transform(index, it).also { index++ } + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessor.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessor.kt new file mode 100644 index 00000000000..d91df6df871 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessor.kt @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.hll.codegen.model.Type +import aws.sdk.kotlin.hll.codegen.model.TypeRef +import aws.sdk.kotlin.hll.codegen.util.quote +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * Defines a template processor which maps an argument value of any type to a string value + * @param key The identifier for this processor, which will be used by the [TemplateEngine] to match a parameter with + * this processor + * @param handler A function that accepts an input argument (as an [Any]) and returns a formatted string + */ +@InternalSdkApi +public data class TemplateProcessor(val key: Char, val handler: (Any) -> String) { + @InternalSdkApi + public companion object { + /** + * Instantiate a new typed template processor which only receives arguments of a specific type [T] + * @param T The type of argument values this processor will accept + * @param key The identifier for this processor, which will be used by the [TemplateEngine] to match a parameter + * with this processor + * @param handler A function that accepts an input argument of type [T] and returns a formatted string + */ + public inline fun typed(key: Char, crossinline handler: (T) -> String): TemplateProcessor = + TemplateProcessor(key) { value -> + require(value is T) { "Expected argument of type ${T::class} but found $value" } + handler(value) + } + + /** + * A literal template processor. This processor substitutes parameters in the form of `#L` with the [toString] + * representation of the corresponding argument. + */ + public val Literal: TemplateProcessor = TemplateProcessor('L') { it.toString() } + + /** + * A quoted string template processor. This processor substitutes parameters in the form of `#S` with the + * quoted/escaped form of a string argument. See [quote] for more details. + */ + public val QuotedString: TemplateProcessor = typed('S') { it.quote() } + + /** + * Creates a template processor for [Type] values. This processor substitutes parameters in the form of `#T` + * with the name or an alias of the type. It also appends `import` directives if necessary. + * @param pkg The Kotlin package into which code is being generated. An `import` directive will not be added if + * a passed argument has the same package as this processor. + * @param imports An [ImportDirectives] collection to which new imports will be appended + */ + public fun forType(pkg: String, imports: ImportDirectives): TemplateProcessor { + val processor = ImportingTypeProcessor(pkg, imports) + return typed('T', processor::format) + } + } + + init { + require(key in 'A'..'Z') { "Key character must be a capital letter (A-Z)" } + } +} + +private open class TypeProcessor { + open fun format(type: Type): String = buildString { + append(type.shortName) + + if (type is TypeRef && type.genericArgs.isNotEmpty()) { + type.genericArgs.joinToString(", ", "<", ">", transform = ::format).let(::append) + } + + if (type.nullable) append('?') + } +} + +private class ImportingTypeProcessor(private val pkg: String, private val imports: ImportDirectives) : TypeProcessor() { + override fun format(type: Type): String = buildString { + if (type is TypeRef && type.pkg != pkg && type.pkg != "kotlin") { + val existingImport = imports[type.baseName] + + if (existingImport == null) { + imports += ImportDirective("${type.pkg}.${type.baseName}") + } else if (existingImport.fullName != type.fullName) { + append(type.pkg) + append('.') + } + } + + append(super.format(type)) + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/ksp/processors/HllKspProcessor.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/ksp/processors/HllKspProcessor.kt new file mode 100644 index 00000000000..a9ac3acc51c --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/ksp/processors/HllKspProcessor.kt @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.ksp.processors + +import aws.sdk.kotlin.hll.codegen.util.KspLoggerOutputStream +import aws.sdk.kotlin.hll.codegen.util.asPrintStream +import aws.sdk.kotlin.runtime.InternalSdkApi +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import java.io.PrintStream + +/** + * An abstract implementation for all KSP symbol processors used in high-level libraries + */ +@InternalSdkApi +public abstract class HllKspProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + private var invoked = false + private val logger = environment.logger + private var originalStdOut: PrintStream? = null + + final override fun process(resolver: Resolver): List { + if (invoked) { + logger.info("${this::class.simpleName} has already run once; skipping subsequent processing rounds") + return listOf() + } else { + invoked = true + } + + if (originalStdOut == null) { + val loggerOutStream = KspLoggerOutputStream(logger) + System.setOut(loggerOutStream.asPrintStream()) + } + + return processImpl(resolver) + } + + protected abstract fun processImpl(resolver: Resolver): List + + final override fun finish() { + originalStdOut?.let(System::setOut) + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/DslInfo.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/DslInfo.kt new file mode 100644 index 00000000000..6c9237c7497 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/DslInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * Contains information about types relevant to generating DSL methods + * @param interfaceType The interface type used as the receiver for a DSL block. This should generally be a `public` + * type. + * @param implType The implementation type used to actually invoke the DSL block. This should generally be an `internal` + * type. + * @param implSingleton A flag indicating whether [implType] is a "singleton type" that can be referenced without + * instantiation + */ +@InternalSdkApi +public data class DslInfo(val interfaceType: TypeRef, val implType: TypeRef, val implSingleton: Boolean = false) + +@InternalSdkApi +public val Member.dslInfo: DslInfo? + get() = attributes.getOrNull(ModelAttributes.DslInfo) diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Member.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Member.kt new file mode 100644 index 00000000000..54d62727b03 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Member.kt @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.collections.emptyAttributes +import aws.smithy.kotlin.runtime.collections.get +import com.google.devtools.ksp.symbol.KSPropertyDeclaration + +/** + * Describes a member (i.e., component field, attribute, property, etc.) of a [Structure] + * @param name The name of the member inside its parent [Structure] + * @param type The [Type] of the member + * @param mutable Whether the member is a mutable (`var`) property or an immutable (`val`) property + * @param attributes An [Attributes] collection for associating typed attributes with this member + */ +@InternalSdkApi +public data class Member( + val name: String, + val type: Type, + val mutable: Boolean = false, + val attributes: Attributes = emptyAttributes(), +) { + @InternalSdkApi + public companion object { + /** + * Derive a [Member] from a [KSPropertyDeclaration] + */ + public fun from(prop: KSPropertyDeclaration): Member { + val member = Member( + name = prop.simpleName.getShortName(), + type = Type.from(prop.type), + mutable = prop.isMutable, + ) + + return ModelParsingPlugin.transform(member, ModelParsingPlugin::postProcessMember) + } + } +} + +/** + * Gets the low-level [Member] equivalent for this high-level member + */ +@InternalSdkApi +public val Member.lowLevel: Member + get() = attributes[ModelAttributes.LowLevelMember] diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelAttributes.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelAttributes.kt new file mode 100644 index 00000000000..c99236819a7 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelAttributes.kt @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.AttributeKey + +/** + * Defines [AttributeKey] instances that relate to the data model of low-level to high-level codegen + */ +@InternalSdkApi +public object ModelAttributes { + /** + * The types involved for a DSL-style method for working with a complex member, if applicable + */ + public val DslInfo: AttributeKey = AttributeKey("aws.sdk.kotlin.hll#DslInfo") + + /** + * For a given high-level [Member], this attribute key identifies the associated low-level [Member] + */ + public val LowLevelMember: AttributeKey = AttributeKey("aws.sdk.kotlin.hll#LowLevelMember") + + /** + * For a given high-level [Operation], this attribute key identifies the associated low-level [Operation] + */ + public val LowLevelOperation: AttributeKey = AttributeKey("aws.sdk.kotlin.hll#LowLevelOperation") + + /** + * For a given high-level [Structure], this attribute key identifies the associated low-level [Structure] + */ + public val LowLevelStructure: AttributeKey = AttributeKey("aws.sdk.kotlin.hll#LowLevelStructure") +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelParsingPlugin.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelParsingPlugin.kt new file mode 100644 index 00000000000..2ccaf77633b --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/ModelParsingPlugin.kt @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi +import java.util.ServiceLoader + +/** + * Represents a plugin to the model parsing phase of code generation. Plugins have the opportunity to customize how + * models are parsed or customized. Implementations of this interface are loaded by + * [SPI](https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html). + */ +@InternalSdkApi +public interface ModelParsingPlugin { + @InternalSdkApi + public companion object { + /** + * Gets the list of plugins available on the classpath. Note that this list is loaded only once and doesn't + * refresh. + */ + public val instances: List by lazy { + val classLoader = ModelParsingPlugin::class.java.classLoader + ServiceLoader.load(ModelParsingPlugin::class.java, classLoader).toList() + } + + /** + * Executes a series of transformations from all loaded plugin instances by accepting an input [shape] and + * folding over the [processor] reference from each plugin. Plugin processors are executed in the order they + * were loaded, as visible in [instances]. + * @param S The type of shape this operation will transform + * @param shape The initial input shape + * @param processor A function which accepts a [ModelParsingPlugin] and an [S] shape which will be run for each + * plugin + */ + @InternalSdkApi + public fun transform(shape: S, processor: (ModelParsingPlugin, S) -> S): S = + instances.fold(shape) { prev, plugin -> processor(plugin, prev) } + } + + /** + * Perform some processing of an [Operation] after it's been parsed by the base codegen layer. Implementors should + * return a modified version of [operation] if changes are required or the exact same [operation] if no changes are + * necessary. + * @param operation The [Operation] to potentially modify + * @return A modified version of [operation] if changes are required or the exact same [operation] if no changes are + * necessary. + */ + public fun postProcessOperation(operation: Operation): Operation = operation + + /** + * Perform some processing of a [Structure] after it's been parsed by the base codegen layer. Implementors should + * return a modified version of [struct] if changes are required or the exact same [struct] if no changes are + * necessary. + * @param struct The [Structure] to potentially modify + * @return A modified version of [struct] if changes are required or the exact same [struct] if no changes are + * necessary. + */ + public fun postProcessStructure(struct: Structure): Structure = struct + + /** + * Perform some processing of a [Member] after it's been parsed by the base codegen layer. Implementors should + * return a modified version of [member] if changes are required or the exact same [member] if no changes are + * necessary. + * @param member The [Member] to potentially modify + * @return A modified version of [member] if changes are required or the exact same [member] if no changes are + * necessary. + */ + public fun postProcessMember(member: Member): Member = member +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Operation.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Operation.kt new file mode 100644 index 00000000000..95cb6ce1a73 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Operation.kt @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.hll.codegen.util.capitalizeFirstChar +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.collections.emptyAttributes +import aws.smithy.kotlin.runtime.collections.get +import com.google.devtools.ksp.symbol.KSFunctionDeclaration + +/** + * Describes a service operation (i.e., API method) + * @param methodName The name of the operation as a code-suitable method name. For example, `getItem` is a suitable + * method name in Kotlin, whereas `GetItem` is not (improperly cased) nor is `get item` (contains a space). + * @param request The [Structure] for requests/inputs to this operation + * @param response The [Structure] for responses/output from this operation + * @param attributes An [Attributes] collection for associating typed attributes with this operation + */ +@InternalSdkApi +public data class Operation( + val methodName: String, + val request: Structure, + val response: Structure, + val attributes: Attributes = emptyAttributes(), +) { + /** + * The capitalized name of this operation's [methodName]. For example, if [methodName] is `getItem` then [name] + * would be `GetItem`. + */ + public val name: String = methodName.capitalizeFirstChar // e.g., "GetItem" vs "getItem" + + @InternalSdkApi + public companion object { + /** + * Derive an [Operation] from a [KSFunctionDeclaration] + */ + public fun from(declaration: KSFunctionDeclaration): Operation { + val op = Operation( + methodName = declaration.simpleName.getShortName(), + request = Structure.from(declaration.parameters.single().type), + response = Structure.from(declaration.returnType!!), + ) + + return ModelParsingPlugin.transform(op, ModelParsingPlugin::postProcessOperation) + } + } +} + +/** + * Gets the low-level [Operation] equivalent for this high-level operation + */ +@InternalSdkApi +public val Operation.lowLevel: Operation + get() = attributes[ModelAttributes.LowLevelOperation] diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Pkg.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Pkg.kt new file mode 100644 index 00000000000..43895f81d00 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Pkg.kt @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * Named constants for various mapper runtime packages + */ +@InternalSdkApi +public object Pkg { + @InternalSdkApi + public object Kotlin { + public val Base: String = "kotlin" + public val Collections: String = "$Base.collections" + public val Jvm: String = "$Base.jvm" + } + + @InternalSdkApi + public object Kotlinx { + public val Base: String = "kotlinx" + + @InternalSdkApi + public object Coroutines { + public val Base: String = "${Kotlinx.Base}.coroutines" + public val Flow: String = "$Base.flow" + } + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Structure.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Structure.kt new file mode 100644 index 00000000000..a83f1308ebd --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Structure.kt @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.collections.emptyAttributes +import aws.smithy.kotlin.runtime.collections.get +import com.google.devtools.ksp.getDeclaredProperties +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSTypeReference + +/** + * Describes a structure (i.e., class, struct, etc.) which contains zero or more [Member] instances + * @param type The [TypeRef] for this structure, which includes its name and Kotlin package + * @param members The [Member] instances which are part of this structure + * @param attributes An [Attributes] collection for associating typed attributes with this structure + */ +@InternalSdkApi +public data class Structure( + val type: TypeRef, + val members: Set, + val attributes: Attributes = emptyAttributes(), +) { + @InternalSdkApi + public companion object { + /** + * Derives a [Structure] from the given [KSTypeReference] + */ + public fun from(ksTypeRef: KSTypeReference): Structure { + val struct = Structure( + type = Type.from(ksTypeRef), + members = (ksTypeRef.resolve().declaration as KSClassDeclaration) + .getDeclaredProperties() + .map(Member.Companion::from) + .toSet(), + ) + + return ModelParsingPlugin.transform(struct, ModelParsingPlugin::postProcessStructure) + } + } +} + +/** + * Gets a collection of all generic variables referenced by/in a [Structure], including in the structure's [TypeRef] and + * in the [TypeRef]s of every member + */ +@InternalSdkApi +public fun Structure.genericVars(): List = buildList { + addAll(type.genericVars()) + members.flatMap { it.type.genericVars() }.let(::addAll) +} + +/** + * Gets the low-level [Structure] equivalent for this high-level structure + */ +@InternalSdkApi +public val Structure.lowLevel: Structure + get() = attributes[ModelAttributes.LowLevelStructure] diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Type.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Type.kt new file mode 100644 index 00000000000..d3a543e8a83 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Type.kt @@ -0,0 +1,151 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.hll.codegen.util.requireAllDistinct +import aws.sdk.kotlin.runtime.InternalSdkApi +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeReference + +/** + * Describes a Kotlin data type + */ +@InternalSdkApi +public sealed interface Type { + @InternalSdkApi + public companion object { + /** + * Derives a [TypeRef] from a [KSClassDeclaration] + */ + public fun from(ksClassDecl: KSClassDeclaration): TypeRef = from(ksClassDecl.asStarProjectedType()) + + /** + * Derives a [TypeRef] from a [KSTypeReference] + */ + public fun from(ksTypeRef: KSTypeReference): TypeRef = from(ksTypeRef.resolve()) + + /** + * Derives a [TypeRef] from a [KSType] + */ + public fun from(ksType: KSType): TypeRef { + val name = ksType.declaration.qualifiedName!! + return TypeRef( + pkg = name.getQualifier(), + shortName = name.getShortName(), + genericArgs = ksType.arguments.map { from(it.type!!) }, + nullable = ksType.isMarkedNullable, + ) + } + } + + /** + * Gets the short name (i.e., not including the Kotlin package) for this type + */ + public val shortName: String + + /** + * Indicates whether instances of this type allow nullable references + */ + public val nullable: Boolean +} + +/** + * A reference to a specific, named type (e.g., [kotlin.String]). + * + * This type reference may have generic arguments, which are themselves instances of a [Type]. For instance, a [TypeRef] + * representing [kotlin.collections.List] would have a single generic argument, which may either be a concrete [TypeRef] + * itself (e.g., `List`) or a generic [TypeVar] (e.g., `List`). + * @param pkg The Kotlin package for this type + * @param shortName The short name (i.e., not including the Kotlin package) for this type + * @param genericArgs Zero or more [Type] generic arguments to this type + * @param nullable Indicates whether instances of this type allow nullable references + */ +@InternalSdkApi +public data class TypeRef( + val pkg: String, + override val shortName: String, + val genericArgs: List = listOf(), + override val nullable: Boolean = false, +) : Type { + /** + * The full name of this type, including the Kotlin package + */ + val fullName: String = "$pkg.$shortName" + + /** + * The base name of this type. In most cases, this will be the same as the short name, but for nested types, this + * will only include the top-level name. For example, the base name of a type Foo.Bar.Baz is Foo. + */ + val baseName: String = shortName.substringBefore(".") +} + +/** + * A generic type variable (e.g., `T`) + * @param shortName The name of this type variable + * @param nullable Indicates whether instances of this type allow nullable references + */ +@InternalSdkApi +public data class TypeVar(override val shortName: String, override val nullable: Boolean = false) : Type + +/** + * Derives a nullable [Type] equivalent for this type + */ +@InternalSdkApi +public fun Type.nullable(value: Boolean = true): Type = when { + nullable == value -> this + this is TypeRef -> copy(nullable = value) + this is TypeVar -> copy(nullable = value) + else -> error("Unknown Type ${this::class}") // Should be unreachable, only here to make compiler happy +} + +/** + * Derives a nullable [TypeRef] equivalent for this type reference + */ +@InternalSdkApi +public fun TypeRef.nullable(value: Boolean = true): TypeRef = when { + nullable == value -> this + else -> copy(nullable = value) +} + +/** + * Derives a nullable [TypeVar] equivalent for this type variable + */ +@InternalSdkApi +public fun TypeVar.nullable(value: Boolean = true): TypeVar = when { + nullable == value -> this + else -> copy(nullable = value) +} + +/** + * Gets a collection of all generic variables referenced by this [Type] + */ +@InternalSdkApi +public fun Type.genericVars(): List = buildList { + when (val type = this@genericVars) { + is TypeVar -> add(type) + is TypeRef -> type.genericArgs.flatMap { it.genericVars() } + } +} + +/** + * Formats a collection of [TypeVar] into a Kotlin generics list (e.g., ``) + * @param postfix An optional string to include at the end of the generated string. This can be useful when the intended + * destination for the string is codegen and additional spacing may be required. + */ +@InternalSdkApi +public fun List.asParamsList(postfix: String = ""): String = + takeUnless { isEmpty() } + ?.map { it.shortName } + ?.requireAllDistinct() + ?.joinToString(", ", "<", ">$postfix") + ?: "" + +/** + * Returns whether this [TypeRef] is generic for an [other] + * For example, List.isGenericFor(List) returns true. + */ +@InternalSdkApi +public fun TypeRef.isGenericFor(other: TypeRef): Boolean = fullName == other.fullName diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Types.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Types.kt new file mode 100644 index 00000000000..64400b17a79 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/model/Types.kt @@ -0,0 +1,95 @@ +package aws.sdk.kotlin.hll.codegen.model + +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * A container object for various [Type] instances + */ +@InternalSdkApi +public object Types { + @InternalSdkApi + public object Smithy { + public val ExperimentalApi: TypeRef = TypeRef("aws.smithy.kotlin.runtime", "ExperimentalApi") + public val Instant: TypeRef = TypeRef("aws.smithy.kotlin.runtime.time", "Instant") + public val Url: TypeRef = TypeRef("aws.smithy.kotlin.runtime.net.url", "Url") + public val Document: TypeRef = TypeRef("aws.smithy.kotlin.runtime.content", "Document") + } + + @InternalSdkApi + public object Kotlin { + public val Boolean: TypeRef = kotlin("Boolean") + public val Byte: TypeRef = kotlin("Byte") + public val ByteArray: TypeRef = kotlin("ByteArray") + public val Char: TypeRef = kotlin("Char") + public val CharArray: TypeRef = kotlin("CharArray") + public val Double: TypeRef = kotlin("Double") + public val Float: TypeRef = kotlin("Float") + public val Int: TypeRef = kotlin("Int") + public val Long: TypeRef = kotlin("Long") + public val Number: TypeRef = kotlin("Number") + public val OptIn: TypeRef = kotlin("OptIn") + public val Short: TypeRef = kotlin("Short") + public val String: TypeRef = kotlin("String") + public val StringNullable: TypeRef = String.nullable() + public val UByte: TypeRef = kotlin("UByte") + public val UInt: TypeRef = kotlin("UInt") + public val ULong: TypeRef = kotlin("ULong") + public val UShort: TypeRef = kotlin("UShort") + + @InternalSdkApi + public object Collections { + public val List: TypeRef = TypeRef(Pkg.Kotlin.Collections, "List") + public val Map: TypeRef = TypeRef(Pkg.Kotlin.Collections, "Map") + public val Set: TypeRef = TypeRef(Pkg.Kotlin.Collections, "Set") + } + + /** + * Creates a [TypeRef] for a generic [List] + * @param element The type of elements in the list + */ + public fun list(element: Type): TypeRef = TypeRef(Pkg.Kotlin.Collections, "List", listOf(element)) + + /** + * Creates a [TypeRef] for a named Kotlin type (e.g., `String`) + */ + public fun kotlin(name: String): TypeRef = TypeRef(Pkg.Kotlin.Base, name) + + /** + * Creates a [TypeRef] for a generic [Map] + * @param key The type of keys in the map + * @param value The type of values in the map + */ + public fun map(key: Type, value: Type): TypeRef = TypeRef(Pkg.Kotlin.Collections, "Map", listOf(key, value)) + + /** + * Creates a [TypeRef] for a generic [Map] with [String] keys + * @param value The type of values in the map + */ + public fun stringMap(value: Type): TypeRef = map(String, value) + + @InternalSdkApi + public object Jvm { + public val JvmName: TypeRef = TypeRef(Pkg.Kotlin.Jvm, "JvmName") + } + } + + @InternalSdkApi + public object Kotlinx { + @InternalSdkApi + public object Coroutines { + @InternalSdkApi + public object Flow { + public val flow: TypeRef = TypeRef(Pkg.Kotlinx.Coroutines.Flow, "flow") + + /** + * Creates a [TypeRef] for a generic `Flow` + * @param element The type of elements in the flow + */ + public fun flow(element: Type): TypeRef = + TypeRef(Pkg.Kotlinx.Coroutines.Flow, "Flow", listOf(element)) + + public val transform: TypeRef = TypeRef(Pkg.Kotlinx.Coroutines.Flow, "transform") + } + } + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/BuilderRenderer.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/BuilderRenderer.kt new file mode 100644 index 00000000000..d14733105f4 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/BuilderRenderer.kt @@ -0,0 +1,90 @@ +package aws.sdk.kotlin.hll.codegen.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGenerator +import aws.sdk.kotlin.hll.codegen.model.* +import aws.sdk.kotlin.hll.codegen.util.visibility +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * A DSL-style builder renderer. + * @param generator The generator in which the builder will be written + * @param builtType The [TypeRef] representing the type for which a builder will be generated. This type can be a class + * or an interface. + * @param implementationType The [TypeRef] representing the implementing type whose constructor will be called by the + * generated `build` method. This type must expose a constructor which accepts each element of [members] as parameters. + * Note that this type doesn't have to be public (merely accessible to the `build` method) and may be the same as + * [builtType] if it has an appropriate constructor. + * @param members The [Set] of members of [builtType] which will be included in the builder + * @param ctx The rendering context + */ +@InternalSdkApi +public class BuilderRenderer( + private val generator: CodeGenerator, + private val builtType: TypeRef, + private val implementationType: TypeRef, + private val members: Set, + private val ctx: RenderContext, +) : CodeGenerator by generator { + @InternalSdkApi + public companion object { + public fun builderName(builtType: TypeRef): String = "${builtType.shortName}Builder" + public fun builderType(builtType: TypeRef): TypeRef = builtType.copy(shortName = builderName(builtType)) + } + + private val builderName = builderName(builtType) + + public fun render() { + docs("A DSL-style builder for instances of [#T]", builtType) + + val genericParams = members.flatMap { it.type.genericVars() }.asParamsList() + + write("@#T", Types.Smithy.ExperimentalApi) + withBlock("#Lclass #L#L {", "}", ctx.attributes.visibility, builderName, genericParams) { + members.forEach(::renderProperty) + blankLine() + + withBlock("#Lfun build(): #T {", "}", ctx.attributes.visibility, builtType, genericParams) { + members.forEach { + if (it.type.nullable) { + write("val #1L = #1L", it.name) + } else { + write("val #1L = requireNotNull(#1L) { #2S }", it.name, "Missing value for ${it.name}") + } + } + blankLine() + withBlock("return #T(", ")", implementationType) { + members.forEach { + write("#L,", it.name) + } + } + } + } + blankLine() + } + + private fun renderProperty(member: Member) { + val dslInfo = member.dslInfo + + if (dslInfo != null) blankLine() + + write("#Lvar #L: #T = null", ctx.attributes.visibility, member.name, member.type.nullable()) + + if (dslInfo != null) { + blankLine() + withBlock( + "#Lfun #L(block: #T.() -> #T) {", + "}", + ctx.attributes.visibility, + member.name, + dslInfo.interfaceType, + member.type, + ) { + val constructorIfNecessary = if (dslInfo.implSingleton) "" else "()" + write("#L = #T#L.run(block)", member.name, dslInfo.implType, constructorIfNecessary) + } + blankLine() + } + + // TODO add DSL methods for low-level structure members + } +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderContext.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderContext.kt new file mode 100644 index 00000000000..eaae066a5e1 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderContext.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGeneratorFactory +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.collections.emptyAttributes +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.KSNode + +/** + * Holds useful context data about code generation. Like many context objects, this is sort of a grab-bag of things + * which may be useful during various stages. + * @param logger A logger object for sending events to the console + * @param codegenFactory A factory that creates code generator instances for specific files + * @param pkg The Kotlin package for the generated code (e.g., `aws.sdk.kotlin.hll.dynamodbmapper.operations`) + */ +@InternalSdkApi +public data class RenderContext( + val logger: KSPLogger, + val codegenFactory: CodeGeneratorFactory, + val pkg: String, + val rendererName: String = "aws-sdk-kotlin-hll-codegen", + val attributes: Attributes = emptyAttributes(), +) + +public fun RenderContext.logging(message: String, symbol: KSNode? = null): Unit = logger.logging(message, symbol) +public fun RenderContext.info(message: String, symbol: KSNode? = null): Unit = logger.info(message, symbol) +public fun RenderContext.warn(message: String, symbol: KSNode? = null): Unit = logger.warn(message, symbol) +public fun RenderContext.error(message: String, symbol: KSNode? = null): Unit = logger.error(message, symbol) + +public fun RenderContext.exception(e: Throwable): Unit = logger.exception(e) diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderOptions.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderOptions.kt new file mode 100644 index 00000000000..514988feee5 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RenderOptions.kt @@ -0,0 +1,26 @@ +package aws.sdk.kotlin.hll.codegen.rendering + +import aws.smithy.kotlin.runtime.collections.AttributeKey + +public object RenderOptions { + + /** + * Determines the visibility of code-generated classes / objects. Defaults to [Visibility.PUBLIC]. + */ + public val VisibilityAttribute: AttributeKey = AttributeKey("Visibility") +} + +/** + * Determines the visibility of code-generated classes / objects. Defaults to [PUBLIC]. + */ +public enum class Visibility { + /** + * All code-generated constructs will be `public` + */ + PUBLIC, + + /** + * All code-generated constructs will be `internal` + */ + INTERNAL, +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RendererBase.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RendererBase.kt new file mode 100644 index 00000000000..a33cb38fb08 --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/rendering/RendererBase.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.rendering + +import aws.sdk.kotlin.hll.codegen.core.CodeGenerator +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * The parent class for renderers backed by a [CodeGenerator] + * @param ctx The active [RenderContext] + * @param fileName The name of the file which should be created _without_ parent directory or extension (which is always + * **.kt**) + */ +@InternalSdkApi +public abstract class RendererBase( + ctx: RenderContext, + fileName: String, +) : CodeGenerator by ctx.codegenFactory.generator(fileName, ctx.pkg, ctx.rendererName) { + /** + * Run this renderer by calling the `abstract` [generate] method and then [persist] + */ + public fun render() { + generate() + persist() + } + + protected abstract fun generate() +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/AttributesExt.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/AttributesExt.kt new file mode 100644 index 00000000000..d639110110e --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/AttributesExt.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.util + +import aws.sdk.kotlin.hll.codegen.rendering.RenderOptions.VisibilityAttribute +import aws.sdk.kotlin.hll.codegen.rendering.Visibility +import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.smithy.kotlin.runtime.collections.* + +/** + * Combines this [Attributes] collection with another collection and returns the new result + * @param other The other attributes to merge + */ +@InternalSdkApi +public operator fun Attributes.plus(other: Attributes): Attributes = toMutableAttributes().apply { merge(other) } + +/** + * Adds another attribute to this collection and returns the new result + * @param other A tuple of [AttributeKey] to a value (which may be `null`) + */ +@InternalSdkApi +public operator fun Attributes.plus(other: Pair, T?>): Attributes = + toMutableAttributes().apply { + other.second?.let { set(other.first, it) } ?: remove(other.first) + } + +/** + * Convert this [Attributes]' [VisibilityAttribute] to a string + */ +@InternalSdkApi +public val Attributes.visibility: String + get() = when (this[VisibilityAttribute]) { + Visibility.PUBLIC -> "public " + Visibility.INTERNAL -> "internal " + } diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/IterableExt.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/IterableExt.kt new file mode 100644 index 00000000000..f4213b69ceb --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/IterableExt.kt @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.util + +import aws.sdk.kotlin.runtime.InternalSdkApi + +/** + * Verifies that all elements in this collection are distinct. If duplicates exist, an [IllegalArgumentException] is + * thrown that details precisely which elements are duplicates. + */ +@InternalSdkApi +public fun > C.requireAllDistinct(): C { + val collection = this + val itemCounts = buildMap { + collection.forEach { element -> + compute(element) { _, existingCount -> (existingCount ?: 0) + 1 } + } + } + + val duplicates = itemCounts.filter { (_, count) -> count > 1 }.keys + require(duplicates.isEmpty()) { "Found duplicated items: $duplicates" } + + return collection +} diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/KspLoggerOutputStream.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/KspLoggerOutputStream.kt new file mode 100644 index 00000000000..6d27a200b7c --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/KspLoggerOutputStream.kt @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.util + +import aws.sdk.kotlin.runtime.InternalSdkApi +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.KSNode +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import java.io.PrintStream + +private const val NEWLINE = 0xa // \n + +/** + * Wraps a [KSPLogger] as an [OutputStream], enabling JVM-standard stream operations on the logger. All data written to + * this stream will be buffered until a newline is encountered. Once a newline is encountered, the buffered data is + * emitted to the logger (_without_ the newline) and the buffer is cleared. + * @param logger The logger instance to which data should be written + * @param logMethod The specific logger method to use on the logger. The default value is [KSPLogger.info]. + */ +@InternalSdkApi +public class KspLoggerOutputStream( + private val logger: KSPLogger, + private val logMethod: KSPLogger.(message: String, symbol: KSNode?) -> Unit = KSPLogger::info, +) : OutputStream() { + private val currentLine = ByteArrayOutputStream() + + override fun write(b: Int) { + if (b == NEWLINE) flush() else currentLine.write(b) + } + + override fun flush() { + logger.info(currentLine.toByteArray().decodeToString()) + currentLine.reset() + } +} + +/** + * Wraps a [KspLoggerOutputStream] in a [PrintStream] + */ +@InternalSdkApi +public fun KspLoggerOutputStream.asPrintStream(): PrintStream = PrintStream(this) diff --git a/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/Strings.kt b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/Strings.kt new file mode 100644 index 00000000000..4c1ee94e10c --- /dev/null +++ b/hll/hll-codegen/src/main/kotlin/aws/sdk/kotlin/hll/codegen/util/Strings.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.util + +import aws.sdk.kotlin.runtime.InternalSdkApi + +private val codepointMap = buildMap { + (0..255).filter(Character::isISOControl).forEach { code -> put(code, unicodeEscape(code)) } + put(8, "\\b") + put(9, "\\t") + put(10, "\\n") + put(13, "\\r") + put(34, "\\\"") + put(36, "\\\$") + put(92, "\\\\") +}.withDefault(Character::toString) + +private fun unicodeEscape(code: Int): String { + require(code < 65536) { "This function only works for codepoints < 65536. If you've run into this error, fix me!" } + return "\\u" + String.format("%04X", code) +} + +/** + * Escape a string to one that is safe for use as a string literal by replacing various reserved characters with escape + * sequences + */ +@InternalSdkApi +public fun String.escape(): String = buildString { + this@escape.codePoints().forEach { append(codepointMap.getValue(it)) } +} + +/** + * Returns a string with the first letter capitalized (if applicable) + */ +@InternalSdkApi +public val String.capitalizeFirstChar: String + get() = replaceFirstChar { it.uppercaseChar() } + +/** + * Returns a string with the first letter lowercased (if applicable) + */ +@InternalSdkApi +public val String.lowercaseFirstChar: String + get() = replaceFirstChar { it.lowercaseChar() } + +/** + * Escapes and quotes a string such that it could be used in codegen + */ +@InternalSdkApi +public fun String.quote(): String = "\"${escape()}\"" diff --git a/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngineTest.kt b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngineTest.kt new file mode 100644 index 00000000000..c853b989f7f --- /dev/null +++ b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateEngineTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertFailsWith + +class TemplateEngineTest { + private val reverser = TemplateProcessor.typed('R') { it.reversed() } + private val numbersToWords = TemplateProcessor.typed('N') { + it + .toString() + .replace("-", "Negative") + .replace("0", "Zero") + .replace("1", "One") + .replace("2", "Two") + .replace("3", "Three") + .replace("4", "Four") + .replace("5", "Five") + .replace("6", "Six") + .replace("7", "Seven") + .replace("8", "Eight") + .replace("9", "Nine") + } + + private val engine = TemplateEngine(listOf(TemplateProcessor.Literal, reverser, numbersToWords)) + + private fun test(expected: String, template: String, vararg args: Any) { + val actual = engine.process(template, args.toList()) + assertEquals(expected, actual) + } + + @Test + fun testSimple() { + test("Parameter-less test", "Parameter-less test") + test("Reversed 'foo' is 'oof'", "Reversed 'foo' is '#R'", "foo") + test("Favorite number = OneTwoThree", "Favorite number = #N", 123) + test("Mixed: yemilb, FourTwo", "Mixed: #R, #N", "blimey", 42) + } + + @Test + fun testIndexed() { + test("Repeated: abc, abc, abc, def, abc, def", "Repeated: #1L, #1L, #1L, #2L, #1L, #2L", "abc", "def") + + assertFailsWith { + test("n/a", "This should fail: #1L #2L #L #3L", "a", "b", "c") + } + + assertFailsWith { + test("n/a", "This should fail: #L #L #1L #L", "a", "b", "c") + } + } +} diff --git a/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessorTest.kt b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessorTest.kt new file mode 100644 index 00000000000..ffc793866f7 --- /dev/null +++ b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/core/TemplateProcessorTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.core + +import aws.sdk.kotlin.hll.codegen.model.TypeRef +import aws.sdk.kotlin.hll.codegen.model.TypeVar +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertContains + +class TemplateProcessorTest { + @Test + fun testLiteral() { + val processor = TemplateProcessor.Literal + assertEquals("foo", processor.handler("foo")) + } + + @Test + fun testQuotedString() { + val processor = TemplateProcessor.QuotedString + assertEquals(""""This is a test!"""", processor.handler("This is a test!")) + assertEquals(""""This is a \"test\"!"""", processor.handler("""This is a "test"!""")) + } + + @Test + fun testType() { + val pkg = "foo.bar" + val imports = ImportDirectives() + val processor = TemplateProcessor.forType(pkg, imports) + + val typeVar = TypeVar("T") + assertEquals("T", processor.handler(typeVar)) + assertEquals(0, imports.size) + + val samePkgClass = TypeRef(pkg, "Apple") + assertEquals("Apple", processor.handler(samePkgClass)) + assertEquals(0, imports.size) + + val otherPkg = "bar.foo" + val otherPkgClass = TypeRef(otherPkg, "Banana") + assertEquals("Banana", processor.handler(otherPkgClass)) + assertEquals(1, imports.size) + assertContains(imports, ImportDirective(otherPkgClass)) + + // Try again + assertEquals("Banana", processor.handler(otherPkgClass)) + assertEquals(1, imports.size) // Size shouldn't have changed since class is already imported + + val fig = TypeRef(otherPkg, "Fig") + val elderberry = TypeRef(otherPkg, "Elderberry", genericArgs = listOf(TypeVar("E"), fig)) + val date = TypeRef(pkg, "Date") + val cherry = TypeRef(otherPkg, "Cherry", genericArgs = listOf(date, elderberry)) + assertEquals("Cherry>", processor.handler(cherry)) + assertEquals(4, imports.size) + assertContains(imports, ImportDirective(cherry)) + assertContains(imports, ImportDirective(elderberry)) + assertContains(imports, ImportDirective(fig)) + } +} diff --git a/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/util/StringsTest.kt b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/util/StringsTest.kt new file mode 100644 index 00000000000..c2f85036c6d --- /dev/null +++ b/hll/hll-codegen/src/test/kotlin/aws/sdk/kotlin/hll/codegen/util/StringsTest.kt @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.codegen.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class StringsTest { + @Test + fun testEscape() { + assertEquals("Control char \\t escaped?", "Control char \t escaped?".escape()) + assertEquals("Control char \\\\ escaped?", "Control char \\ escaped?".escape()) + + assertEquals("Control char \\u0000 escaped?", "Control char ${Character.toString(0)} escaped?".escape()) + assertEquals("Control char \\u0080 escaped?", "Control char ${Character.toString(128)} escaped?".escape()) + + assertEquals("Unescaped ñ ™ € ⭐ 🏆", "Unescaped ñ ™ € ⭐ 🏆".escape()) + } + + @Test + fun testQuote() { + assertEquals("\"This \\t is a \\\"test\\\"!\"", "This \t is a \"test\"!".quote()) + } +} diff --git a/hll/hll-mapping-core/api/hll-mapping-core.api b/hll/hll-mapping-core/api/hll-mapping-core.api new file mode 100644 index 00000000000..c5289eab9dd --- /dev/null +++ b/hll/hll-mapping-core/api/hll-mapping-core.api @@ -0,0 +1,121 @@ +public abstract interface class aws/sdk/kotlin/hll/mapping/core/converters/Converter : aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom, aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo { +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/ConverterKt { + public static final fun Converter (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun andThenFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun andThenTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun validatingFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun validatingTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom { + public abstract fun convertFrom (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFromKt { + public static final fun andThenConvertsFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public static final fun firstValidatingTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo { + public abstract fun convertTo (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/ConvertsToKt { + public static final fun andThenConvertsTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public static final fun firstValidatingFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter : aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom, aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo { +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverterKt { + public static final fun mergeBy (Laws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/CollectionTypeConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/mapping/core/converters/collections/CollectionTypeConverters; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConverters; + public final fun mapConvertsFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun mapConvertsTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConvertersKt { + public static final fun mapFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConverters; + public final fun mapConvertsFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun mapConvertsKeysFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun mapConvertsKeysTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun mapConvertsTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun mapConvertsValuesFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun mapConvertsValuesTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun ofKeys (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun ofKeys (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun ofKeys (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun ofValues (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun ofValues (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun ofValues (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConvertersKt { + public static final fun mapFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapKeysFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapKeysTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapValuesFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapValuesTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConverters { + public static final field INSTANCE Laws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConverters; + public final fun mapConvertsFrom (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun mapConvertsTo (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom; + public final fun of (Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo;)Laws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo; +} + +public final class aws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConvertersKt { + public static final fun mapFrom (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; + public static final fun mapTo (Laws/sdk/kotlin/hll/mapping/core/converters/Converter;Laws/sdk/kotlin/hll/mapping/core/converters/Converter;)Laws/sdk/kotlin/hll/mapping/core/converters/Converter; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/util/Either { + public static final field Companion Laws/sdk/kotlin/hll/mapping/core/util/Either$Companion; +} + +public final class aws/sdk/kotlin/hll/mapping/core/util/Either$Companion { + public final fun Left (Ljava/lang/Object;)Laws/sdk/kotlin/hll/mapping/core/util/Either$Left; + public final fun Right (Ljava/lang/Object;)Laws/sdk/kotlin/hll/mapping/core/util/Either$Right; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/util/Either$Left : aws/sdk/kotlin/hll/mapping/core/util/Either { + public abstract fun getValue ()Ljava/lang/Object; +} + +public abstract interface class aws/sdk/kotlin/hll/mapping/core/util/Either$Right : aws/sdk/kotlin/hll/mapping/core/util/Either { + public abstract fun getValue ()Ljava/lang/Object; +} + +public final class aws/sdk/kotlin/hll/mapping/core/util/EitherKt { + public static final fun fold (Laws/sdk/kotlin/hll/mapping/core/util/Either;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun map (Laws/sdk/kotlin/hll/mapping/core/util/Either;Lkotlin/jvm/functions/Function1;)Laws/sdk/kotlin/hll/mapping/core/util/Either; + public static final fun merge (Laws/sdk/kotlin/hll/mapping/core/util/Either;)Ljava/lang/Object; +} + diff --git a/hll/hll-mapping-core/build.gradle.kts b/hll/hll-mapping-core/build.gradle.kts new file mode 100644 index 00000000000..b7c2dc41027 --- /dev/null +++ b/hll/hll-mapping-core/build.gradle.kts @@ -0,0 +1,20 @@ +import aws.sdk.kotlin.gradle.kmp.kotlin + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "Common data mapping utilities used by AWS SDK for Kotlin's high level libraries" +extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: Mapping" +extra["moduleName"] = "aws.sdk.kotlin.hll.mapping.core" + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.smithy.kotlin.runtime.core) + } + } + } +} diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/Converter.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/Converter.kt new file mode 100644 index 00000000000..cbb024960e6 --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/Converter.kt @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Models two-way conversion between a type [T] and a type [F] + * @param F The type being converted from + * @param T The type being converted to + */ +@ExperimentalApi +public interface Converter : + ConvertsTo, + ConvertsFrom + +/** + * Creates a new two-way converter from symmetrical one-way converters + * @param F The type being converted from + * @param T The type being converted to + * @param convertTo A converter instance for converting one-way from [F] to [T] + * @param convertFrom A converter instance for converting one-way from [T] to [F] + */ +@ExperimentalApi +public fun Converter(convertTo: ConvertsTo, convertFrom: ConvertsFrom): Converter = + object : Converter, ConvertsTo by convertTo, ConvertsFrom by convertFrom { } + +/** + * Chains this converter with another converter, yielding a new converter which performs a two-stage conversion. (Note + * that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps in their actual + * implementation.) + * @param F The source type of this converter + * @param T The target type of this converter and the source type of the given [converter] + * @param T2 The target type of the given [converter] + * @param converter The converter to chain together with this converter. Note that the source type of the given + * [converter] must be the same as the target type of this converter. + */ +@ExperimentalApi +public fun Converter.andThenTo(converter: Converter): Converter = + Converter(this.andThenConvertsTo(converter), converter.andThenConvertsFrom(this)) + +/** + * Chains this converter with another converter, yielding a new converter which performs a two-stage conversion. (Note + * that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps in their actual + * implementation.) + * @param F The source type of this converter and the target type of the given [converter] + * @param F2 The source type of the given [converter] + * @param T The target type of this converter + * @param converter The converter to chain together with this converter. Note that the target type of the given + * [converter] must be the same as the source type of this converter. + */ +@ExperimentalApi +public fun Converter.andThenFrom(converter: Converter): Converter = + Converter(converter.andThenConvertsTo(this), this.andThenConvertsFrom(converter)) + +/** + * Adds validation before conversions by running [validate] on [F] values before converting them to type [T]. Validators + * are expected to throw an exception if the expected condition is not met. + * @param F The type being converted from + * @param T The type being converted to + * @param validate A function which accepts an [F] value and throws an exception if the expected condition is not + * met + */ +@ExperimentalApi +public fun Converter.validatingFrom(validate: (F) -> Unit): Converter = + Converter(this.firstValidatingFrom(validate), this) + +/** + * Adds validation before conversions by running [validate] on [T] values before converting them to type [F]. Validators + * are expected to throw an exception if the expected condition is not met. + * @param F The type being converted to + * @param T The type being converted from + * @param validate A function which accepts a [T] value and throws an exception if the expected condition is not + * met + */ +@ExperimentalApi +public fun Converter.validatingTo(validate: (T) -> Unit): Converter = + Converter(this, this.firstValidatingTo(validate)) diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom.kt new file mode 100644 index 00000000000..7f81d9aee9f --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsFrom.kt @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Models one-way conversion from a type [T] to a type [F]. This type is similar to [ConvertsTo] but models conversion + * in the opposite direction. + * @param F The type being converted to + * @param T The type being converted from + */ +@ExperimentalApi +public fun interface ConvertsFrom { + /** + * Converts a single value from type [T] to type [F] + * @param to The value to convert + */ + public fun convertFrom(to: T): F +} + +/** + * Chains this converter with another converter, yielding a new converter which performs a two-stage conversion. (Note + * that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps in their actual + * implementation.) + * @param F The source type of this converter and the target type of the given [converter] + * @param F2 The source type of the given [converter] + * @param T The target type of this converter + * @param converter The converter to chain together with this converter. Note that the target type of the given + * [converter] must be the same as the source type of this converter. + */ +@ExperimentalApi +public fun ConvertsFrom.andThenConvertsFrom(converter: ConvertsFrom): ConvertsFrom = + ConvertsFrom { to: T -> converter.convertFrom(this.convertFrom(to)) } + +/** + * Adds validation before a conversion by running [validate] on [T] values before converting them to type [F]. + * Validators are expected to throw an exception if the expected condition is not met. + * @param F The type being converted to + * @param T The type being converted from + * @param validate A function which accepts a [T] value and throws an exception if the expected condition is not met + */ +@ExperimentalApi +public fun ConvertsFrom.firstValidatingTo(validate: (T) -> Unit): ConvertsFrom = + ConvertsFrom { to: T -> + validate(to) + this.convertFrom(to) + } diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo.kt new file mode 100644 index 00000000000..b7c4e02eb85 --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/ConvertsTo.kt @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Models one-way conversion from a type [F] to a type [T] + * @param F The type being converted from + * @param T The type being converted to + */ +@ExperimentalApi +public fun interface ConvertsTo { + /** + * Converts a single value from type [F] to type [T] + * @param from The value to convert + */ + public fun convertTo(from: F): T +} + +/** + * Chains this converter with another converter, yielding a new converter which performs a two-stage conversion. (Note + * that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps in their actual + * implementation.) + * @param F The source type of this converter + * @param T The target type of this converter and the source type of the given [converter] + * @param T2 The target type of the given [converter] + * @param converter The converter to chain together with this converter. Note that the source type of the given + * [converter] must be the same as the target type of this converter. + */ +@ExperimentalApi +public fun ConvertsTo.andThenConvertsTo(converter: ConvertsTo): ConvertsTo = + ConvertsTo { from: F -> converter.convertTo(this.convertTo(from)) } + +/** + * Adds validation before a conversion by running [validate] on [F] values before converting them to type [T]. + * Validators are expected to throw an exception if the expected condition is not met. + * @param F The type being converted from + * @param T The type being converted to + * @param validate A function which accepts an [F] value and throws an exception if the expected condition is not met + */ +@ExperimentalApi +public fun ConvertsTo.firstValidatingFrom(validate: (F) -> Unit): ConvertsTo = + ConvertsTo { from: F -> + validate(from) + this.convertTo(from) + } diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter.kt new file mode 100644 index 00000000000..6bdce1186e6 --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/SplittingConverter.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters + +import aws.sdk.kotlin.hll.mapping.core.util.Either +import aws.sdk.kotlin.hll.mapping.core.util.map +import aws.sdk.kotlin.hll.mapping.core.util.merge +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Models partial, asymmetrical conversion between a type [F] and a type [T], where some condition internal to the + * converter splits the possible pathways data make take through conversion logic. One of these branches will be more + * complex and involve converting types [F] and [T] to types [F2] and [T2] (respectively). The remaining conversion + * between [F2] and [T2] will typically be delegated to another converter. + * + * Because they are partial and asymmetrical, [SplittingConverter] instances are typically not very useful on their own. + * Most often they are combined with another [Converter] via the [mergeBy] extension method, forming a complete, + * symmetrical converter between [F] and [T]. + * + * Splitting converters use the [Either] type to denote values which may follow the simple branch ([Either.Left]) or the + * complex branch ([Either.Right]). + * + * @param F The overall type being converted from + * @param F2 The intermediate type being converted from on the complex branch + * @param T2 The intermediate type being converted to on the complex branch + * @param T The overall type being converted to + */ +@ExperimentalApi +public interface SplittingConverter : + ConvertsTo>, + ConvertsFrom, T> + +/** + * Merges this [SplittingConverter] by delegating to a [Converter] instance that converts between types [F2] and [T2]. + * After the merge, a new [Converter] will be returned which fully converts between types [F] and [T]. + * @param F The overall type being converted from + * @param F2 The intermediate type being converted from on the complex branch, which is also the source type of + * [converter] + * @param T2 The intermediate type being converted to on the complex branch, which is also the target type of + * [converter] + * @param T The overall type being converted to + * @param converter A [Converter] between types [F2] and [T2] + */ +@ExperimentalApi +public fun SplittingConverter.mergeBy( + converter: Converter, +): Converter = + Converter( + convertTo = { from: F -> this@mergeBy.convertTo(from).map(converter::convertTo).merge() }, + convertFrom = { to: T -> this@mergeBy.convertFrom(to).map(converter::convertFrom).merge() }, + ) diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/CollectionTypeConverters.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/CollectionTypeConverters.kt new file mode 100644 index 00000000000..5c9cb9b288e --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/CollectionTypeConverters.kt @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters.collections + +import aws.sdk.kotlin.hll.mapping.core.converters.Converter +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with mapping between collection types (e.g., [Set] to + * [List]) + */ +@ExperimentalApi +public object CollectionTypeConverters { + /** + * Creates a [Converter] which transforms between [Set] and [List] instances (both of some type [T]) + * @param T The type of elements in the [Set]/[List] + */ + @Suppress("ktlint:standard:function-naming") + public inline fun SetToListConverter(): Converter, List> = + Converter(Set::toList, List::toSet) +} diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConverters.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConverters.kt new file mode 100644 index 00000000000..addfd5aa23e --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/ListMappingConverters.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters.collections + +import aws.sdk.kotlin.hll.mapping.core.converters.* +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with [List] mapping + */ +@ExperimentalApi +public object ListMappingConverters { + /** + * Creates a one-way converter for transforming [List] with elements of type [T] to [List] with elements of type [F] + * @param F The type being converted to + * @param T The type being converted from + * @param elementConverter A one-way converter of [T] elements to [F] elements + */ + public fun of(elementConverter: ConvertsFrom): ConvertsFrom, List> = + ConvertsFrom { to: List -> to.map(elementConverter::convertFrom) } + + /** + * Chains this list converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param F The source element type of this converter and the target type of the given [elementConverter] + * @param F2 The source type of the given [elementConverter] + * @param T The target type of this converter + * @param elementConverter The element converter to chain together with this list converter. Note that the target + * type of the given [elementConverter] must be the same as the source element type of this converter. + */ + public fun ConvertsFrom, T>.mapConvertsFrom( + elementConverter: ConvertsFrom, + ): ConvertsFrom, T> = this.andThenConvertsFrom(of(elementConverter)) + + /** + * Creates a one-way converter for transforming [List] with elements of type [F] to [List] with elements of type [T] + * @param F The type being converted from + * @param T The type being converted to + * @param elementConverter A one-way converter of [F] elements to [T] elements + */ + public fun of(elementConverter: ConvertsTo): ConvertsTo, List> = + ConvertsTo { from: List -> from.map(elementConverter::convertTo) } + + /** + * Chains this list converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param F The source type of this converter + * @param T The target element type of this converter and the source type of the given [elementConverter] + * @param T2 The target type of the given [elementConverter] + * @param elementConverter The element converter to chain together with this list converter. Note that the source + * type of the given [elementConverter] must be the same as the target element type of this converter. + */ + public fun ConvertsTo>.mapConvertsTo( + elementConverter: ConvertsTo, + ): ConvertsTo> = this.andThenConvertsTo(of(elementConverter)) + + /** + * Creates a two-way converter for transforming between a [List] with elements of type [F] and a [List] with + * elements of type [T] + * @param F The type being converted from + * @param T The type being converted to + * @param elementConverter A [Converter] for transforming between elements of type [F] and [T] + */ + public fun of(elementConverter: Converter): Converter, List> = + Converter(of(elementConverter as ConvertsTo), of(elementConverter as ConvertsFrom)) +} + +/** + * Chains this list converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source element type of this converter and the target type of the given [elementConverter] + * @param F2 The source type of the given [elementConverter] + * @param T The target type of this converter + * @param elementConverter The element converter to chain together with this list converter. Note that the target type + * of the given [elementConverter] must be the same as the source element type of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapFrom(elementConverter: Converter): Converter, T> = + this.andThenFrom(ListMappingConverters.of(elementConverter)) + +/** + * Chains this list converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param T The target element type of this converter and the source type of the given [elementConverter] + * @param T2 The target type of the given [elementConverter] + * @param elementConverter The element converter to chain together with this list converter. Note that the source type + * of the given [elementConverter] must be the same as the target element type of this converter. + */ +@ExperimentalApi +public fun Converter>.mapTo(elementConverter: Converter): Converter> = + this.andThenTo(ListMappingConverters.of(elementConverter)) diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConverters.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConverters.kt new file mode 100644 index 00000000000..9e65fb5bba5 --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/MapMappingConverters.kt @@ -0,0 +1,390 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters.collections + +import aws.sdk.kotlin.hll.mapping.core.converters.* +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with [Map] mapping + */ +@ExperimentalApi +public object MapMappingConverters { + /** + * Creates a one-way converter for transforming [Map] with keys of type [TK] to [Map] with keys of type [FK]. The + * values of the map are unchanged. + * @param FK The type of keys being converted to + * @param TK The type of keys being converted from + * @param V The type of values + * @param keyConverter A one-way converter of [TK] keys to [FK] keys + */ + public fun ofKeys(keyConverter: ConvertsFrom): ConvertsFrom, Map> = + ConvertsFrom { to: Map -> + to.mapKeys { e: Map.Entry -> keyConverter.convertFrom(e.key) } + } + + /** + * Chains this map converter with a key converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param FK The type of keys being converted from + * @param FK2 The type of keys being converted to + * @param V The type of values + * @param T The target type of this converter + * @param keyConverter The key converter to chain together with this map converter. Note that the target type of the + * given [keyConverter] must be the same as the source key type of this converter. + */ + public fun ConvertsFrom, T>.mapConvertsKeysFrom( + keyConverter: ConvertsFrom, + ): ConvertsFrom, T> = this.andThenConvertsFrom(ofKeys(keyConverter)) + + /** + * Creates a one-way converter for transforming [Map] with values of type [TV] to [Map] with values of type [FV]. + * The keys of the map are unchanged. + * @param K The type of keys + * @param FV The type of values being converted to + * @param TV The type of values being converted from + * @param valueConverter A one-way converter of [TV] values to [FV] values + */ + public fun ofValues( + valueConverter: ConvertsFrom, + ): ConvertsFrom, Map> = + ConvertsFrom { to: Map -> + to.mapValues { e: Map.Entry -> valueConverter.convertFrom(e.value) } + } + + /** + * Chains this map converter with a value converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param K The type of keys + * @param FV The type of values being converted to + * @param FV2 The type of values being converted from + * @param T The target type of this converter + * @param valueConverter The value converter to chain together with this map converter. Note that the target type of + * the given [valueConverter] must be the same as the source value type of this converter. + */ + public fun ConvertsFrom, T>.mapConvertsValuesFrom( + valueConverter: ConvertsFrom, + ): ConvertsFrom, T> = this.andThenConvertsFrom(ofValues(valueConverter)) + + /** + * Creates a one-way converter for transforming [Map] with keys of type [TK] and values of type [TV] to [Map] with + * keys of type [FK] and values of type [FV] + * @param FK The type of keys being converted to + * @param FV The type of values being converted to + * @param TK The type of keys being converted from + * @param TV The type of values being converted from + * @param entryConverter A one-way converter of [TK]/[TV] pairs to [FK]/[FV] pairs + */ + public fun of( + entryConverter: ConvertsFrom, Pair>, + ): ConvertsFrom, Map> = + ConvertsFrom { to: Map -> + to.entries.associate { e: Map.Entry -> entryConverter.convertFrom(e.toPair()) } + } + + /** + * Chains this map converter with an entry converter, yielding a new converter which performs a two stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param FK The type of keys being converted to + * @param FV The type of values being converted to + * @param FK2 The type of keys being converted from + * @param FV2 The type of values being converted from + * @param entryConverter The entry converter to chain together with this map converter. Note that the target types + * of the given [entryConverter] must be the same as the source types of this converter. + */ + public fun ConvertsFrom, T>.mapConvertsFrom( + entryConverter: ConvertsFrom, Pair>, + ): ConvertsFrom, T> = this.andThenConvertsFrom(of(entryConverter)) + + /** + * Creates a one-way converter for transforming [Map] with keys of type [FK] to [Map] with keys of type [TK]. The + * values of the map are unchanged. + * @param FK The type of keys being converted from + * @param TK The type of keys being converted to + * @param V The type of values + * @param keyConverter A one-way converter of [FK] keys to [TK] keys + */ + public fun ofKeys( + keyConverter: ConvertsTo, + ): ConvertsTo, Map> = + ConvertsTo { from: Map -> + from.mapKeys { e: Map.Entry -> keyConverter.convertTo(e.key) } + } + + /** + * Chains this map converter with a key converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param F The source type of this converter + * @param TK The type of keys being converted from + * @param TK2 The type of keys being converted to + * @param V The type of values + * @param keyConverter The key converter to chain together with this map converter. Note that the source type of the + * given [keyConverter] must be the same as the target key type of this converter. + */ + public fun ConvertsTo>.mapConvertsKeysTo( + keyConverter: ConvertsTo, + ): ConvertsTo> = this.andThenConvertsTo(ofKeys(keyConverter)) + + /** + * Creates a one-way converter for transforming [Map] with values of type [FV] to [Map] with values of type [TV]. + * The keys of the map are unchanged. + * @param K The type of keys + * @param FV The type of values being converted from + * @param TV The type of values being converted to + * @param valueConverter A one-way converter of [FV] values to [TV] values + */ + public fun ofValues( + valueConverter: ConvertsTo, + ): ConvertsTo, Map> = + ConvertsTo { from: Map -> + from.mapValues { e: Map.Entry -> valueConverter.convertTo(e.value) } + } + + /** + * Chains this map converter with a value converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param F The source type of this converter + * @param K The type of keys + * @param TV The type of values being converted from + * @param TV2 The type of values being converted to + * @param valueConverter The value converter to chain together with this map converter. Note that the source type of + * the given [valueConverter] must be the same as the target value type of this converter. + */ + public fun ConvertsTo>.mapConvertsValuesTo( + valueConverter: ConvertsTo, + ): ConvertsTo> = this.andThenConvertsTo(ofValues(valueConverter)) + + /** + * Creates a one-way converter for transforming [Map] with keys of type [FK] and values of type [FV] to [Map] with + * keys of type [TK] and values of type [TV] + * @param FK The type of keys being converted from + * @param FV The type of values being converted from + * @param TK The type of keys being converted to + * @param TV The type of values being converted to + * @param entryConverter A one-way converter of [FK]/[FV] pairs to [TK]/[TV] pairs + */ + public fun of( + entryConverter: ConvertsTo, Pair>, + ): ConvertsTo, Map> = + ConvertsTo { from: Map -> + from.entries.associate { e -> entryConverter.convertTo(e.toPair()) } + } + + /** + * Chains this map converter with an entry converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical + * steps in their actual implementation.) + * @param F The source type of this converter + * @param TK The type of keys being converted from + * @param TV The type of values being converted from + * @param TK2 The type of keys being converted to + * @param TV2 The type of values being converted to + * @param entryConverter The entry converter to chain together with this map converter. Note that the source types + * of the given [entryConverter] must be the same as the target types of this converter. + */ + public fun ConvertsTo>.mapConvertsTo( + entryConverter: ConvertsTo, Pair>, + ): ConvertsTo> = this.andThenConvertsTo(of(entryConverter)) + + /** + * Creates a two-way converter for transforming between [Map] with keys of type [FK] and [Map] with keys of type + * [TK]. The values of maps are unchanged. + * @param FK The type of keys being converted from + * @param TK The type of keys being converted to + * @param V The type of values + * @param keyConverter A converter for transforming between keys of type [FK] and [TK] + */ + public fun ofKeys( + keyConverter: Converter, + ): Converter, Map> = + Converter(ofKeys(keyConverter as ConvertsTo), ofKeys(keyConverter as ConvertsFrom)) + + /** + * Creates a two-way converter for transforming between [Map] with values of type [FV] and [Map] with values of type + * [TV]. The keys of the map are unchanged. + * @param K The type of keys + * @param FV The type of values being converted from + * @param TV The type of values being converted to + * @param valueConverter A converter for transforming between values of type [FV] and [TV] + */ + public fun ofValues( + valueConverter: Converter, + ): Converter, Map> = + Converter(ofValues(valueConverter as ConvertsTo), ofValues(valueConverter as ConvertsFrom)) + + /** + * Creates a two-way converter for transforming between [Map] with keys of type [FK] and values of type [FV] to + * [Map] with keys of type [TK] and values of type [TV] + * @param FK The type of keys being converted from + * @param FV The type of values being converted from + * @param TK The type of keys being converted to + * @param TV The type of values being converted to + * @param entryConverter A converter for transforming between [FK]/[FV] pairs and [TK]/[TV] pairs + */ + public fun of( + entryConverter: Converter, Pair>, + ): Converter, Map> = + Converter( + of(entryConverter as ConvertsTo, Pair>), + of(entryConverter as ConvertsFrom, Pair>), + ) +} + +/** + * Chains this map converter with a key converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param FK The type of keys being converted to + * @param FK2 The type of keys being converted from + * @param V The type of values + * @param T The target type of this converter + * @param keyConverter The key converter to chain together with this map converter. Note that the target key type of the + * given [keyConverter] must be the same as the source key type of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapKeysFrom( + keyConverter: Converter, +): Converter, T> = this.andThenFrom(MapMappingConverters.ofKeys(keyConverter)) + +/** + * Chains this map converter with a key converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param TK The type of keys being converted from + * @param TK2 The type of keys being converted to + * @param V The type of values + * @param keyConverter The key converter to chain together with this map converter. Note that the source key type of the + * given [keyConverter] must be the same as the target key type of this converter. + */ +@ExperimentalApi +public fun Converter>.mapKeysTo( + keyConverter: Converter, +): Converter> = this.andThenTo(MapMappingConverters.ofKeys(keyConverter)) + +/** + * Chains this map converter with a value converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param K The type of keys + * @param FV The type of values being converted to + * @param FV2 The type of values being converted from + * @param T The target type of this converter + * @param valueConverter The value converter to chain together with this map converter. Note that the target value type + * of the given [valueConverter] must be the same as the source value type of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapValuesFrom( + valueConverter: Converter, +): Converter, T> = this.andThenFrom(MapMappingConverters.ofValues(valueConverter)) + +/** + * Chains this map converter with a value converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param K The type of keys + * @param TV The type of values being converted from + * @param TV2 The type of values being converted to + * @param valueConverter The value converter to chain together with this map converter. Note that the source value type + * of the given [valueConverter] must be the same as the target value type of this converter. + */ +@ExperimentalApi +public fun Converter>.mapValuesTo( + valueConverter: Converter, +): Converter> = this.andThenTo(MapMappingConverters.ofValues(valueConverter)) + +/** + * Convenience function for combining independent converters into `Converter, Pair>`, suitable for + * use as a map entry converter + * @param F1 The first type of value being converted from + * @param T1 The first type of value being converted to + * @param F2 The second type of value being converted from + * @param T2 The second type of value being converted to + * @param other The converter to zip with this one + */ +private fun Converter.zip(other: Converter) = Converter( + convertTo = { from: Pair -> this.convertTo(from.first) to other.convertTo(from.second) }, + convertFrom = { to: Pair -> this.convertFrom(to.first) to other.convertFrom(to.second) }, +) + +/** + * Chains this map converter with an entry converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param FK The type of keys being converted to + * @param FV The type of values being converted to + * @param FK2 The type of keys being converted from + * @param FV2 The type of values being converted from + * @param T The target type of this converter + * @param entryConverter The entry converter to chain together with this map converter. Note that the target types of + * the given [entryConverter] must be the same as the source types of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapFrom( + entryConverter: Converter, Pair>, +): Converter, T> = this.andThenFrom(MapMappingConverters.of(entryConverter)) + +/** + * Chains this map converter with a key converter and a value converter, yielding a new converter which performs a + * two-stage mapping conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of + * multiple logical steps in their actual implementation.) + * @param FK The type of keys being converted to + * @param FV The type of values being converted to + * @param FK2 The type of keys being converted from + * @param FV2 The type of values being converted from + * @param T The target type of this converter + * @param keyConverter The key converter to chain together with this map converter. Note that the target key type of the + * given [keyConverter] must be the same as the source key type of this converter. + * @param valueConverter The value converter to chain together with this map converter. Note that the target value type + * of the given [valueConverter] must be the same as the source value type of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapFrom( + keyConverter: Converter, + valueConverter: Converter, +): Converter, T> = mapFrom(keyConverter.zip(valueConverter)) + +/** + * Chains this map converter with an entry converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param TK The type of keys being converted from + * @param TV The type of values being converted from + * @param TK2 The type of keys being converted to + * @param TV2 The type of values being converted to + * @param entryConverter The entry converter to chain together with this map converter. Note that the source types of + * the given [entryConverter] must be the same as the target types of this converter. + */ +@ExperimentalApi +public fun Converter>.mapTo( + entryConverter: Converter, Pair>, +): Converter> = this.andThenTo(MapMappingConverters.of(entryConverter)) + +/** + * Chains this map converter with a key converter and a value converter, yielding a new converter which performs a + * two-stage mapping conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of + * multiple logical steps in their actual implementation.) + * @param F The source type of this converter + * @param TK The type of keys being converted from + * @param TV The type of values being converted from + * @param TK2 The type of keys being converted to + * @param TV2 The type of values being converted to + * @param keyConverter The key converter to chain together with this map converter. Note that the source key type of the + * given [keyConverter] must be the same as the target key type of this converter. + * @param valueConverter The value converter to chain together with this map converter. Note that the source value type + * of the given [valueConverter] must be the same as the target value type of this converter. + */ +@ExperimentalApi +public fun Converter>.mapTo( + keyConverter: Converter, + valueConverter: Converter, +): Converter> = mapTo(keyConverter.zip(valueConverter)) diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConverters.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConverters.kt new file mode 100644 index 00000000000..b97585779bd --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/converters/collections/SetMappingConverters.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.converters.collections + +import aws.sdk.kotlin.hll.mapping.core.converters.* +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Namespace for containing various conversion utilities dealing with [Set] mapping + */ +@ExperimentalApi +public object SetMappingConverters { + /** + * Creates a one-way converter for transforming [Set] with elements of type [T] to [Set] with elements of type [F] + * @param F The type being converted to + * @param T The type being converted from + * @param elementConverter A one-way converter of [T] values to [F] values + */ + public fun of(elementConverter: ConvertsFrom): ConvertsFrom, Set> = + ConvertsFrom { to: Set -> to.map(elementConverter::convertFrom).toSet() } + + /** + * Chains this set converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source element type of this converter and the target type of the given [elementConverter] + * @param F2 The source type of the given [elementConverter] + * @param T The target type of this converter + * @param elementConverter The element converter to chain together with this set converter. Note that the target type + * of the given [elementConverter] must be the same as the source element type of this converter. + */ + public fun ConvertsFrom, T>.mapConvertsFrom( + elementConverter: ConvertsFrom, + ): ConvertsFrom, T> = this.andThenConvertsFrom(of(elementConverter)) + + /** + * Creates a one-way converter for transforming [Set] with elements of type [F] to [Set] with elements of type [T] + * @param F The type being converted from + * @param T The type being converted to + * @param elementConverter A one-way converter of [F] values to [T] values + */ + public fun of(elementConverter: ConvertsTo): ConvertsTo, Set> = + ConvertsTo { from: Set -> from.map(elementConverter::convertTo).toSet() } + + /** + * Chains this set converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param T The target element type of this converter and the source type of the given [elementConverter] + * @param T2 The target type of the given [elementConverter] + * @param elementConverter The element converter to chain together with this set converter. Note that the source type + * of the given [elementConverter] must be the same as the target element type of this converter. + */ + public fun ConvertsTo>.mapConvertsTo( + elementConverter: ConvertsTo, + ): ConvertsTo> = this.andThenConvertsTo(of(elementConverter)) + + /** + * Creates a two-way converter for transforming between a [Set] with elements of type [F] and a [Set] with elements + * of type [T] + * @param F The type being converted from + * @param T The type being converted to + * @param elementConverter A [Converter] for transforming between values of type [F] and [T] + */ + public fun of(elementConverter: Converter): Converter, Set> = + Converter(of(elementConverter as ConvertsTo), of(elementConverter as ConvertsFrom)) +} + +/** + * Chains this set converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source element type of this converter and the target type of the given [elementConverter] + * @param F2 The source type of the given [elementConverter] + * @param T The target type of this converter + * @param elementConverter The element converter to chain together with this set converter. Note that the target type + * of the given [elementConverter] must be the same as the source element type of this converter. + */ +@ExperimentalApi +public fun Converter, T>.mapFrom(elementConverter: Converter): Converter, T> = + this.andThenFrom(SetMappingConverters.of(elementConverter)) + +/** + * Chains this set converter with an element converter, yielding a new converter which performs a two-stage mapping + * conversion. (Note that these two "stages" are conceptual. Each of these stages may consist of multiple logical steps + * in their actual implementation.) + * @param F The source type of this converter + * @param T The target element type of this converter and the source type of the given [elementConverter] + * @param T2 The target type of the given [elementConverter] + * @param elementConverter The element converter to chain together with this set converter. Note that the source type + * of the given [elementConverter] must be the same as the target element type of this converter. + */ +@ExperimentalApi +public fun Converter>.mapTo(elementConverter: Converter): Converter> = + this.andThenTo(SetMappingConverters.of(elementConverter)) diff --git a/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/util/Either.kt b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/util/Either.kt new file mode 100644 index 00000000000..b6b79d2ec81 --- /dev/null +++ b/hll/hll-mapping-core/common/src/aws/sdk/kotlin/hll/mapping/core/util/Either.kt @@ -0,0 +1,100 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.hll.mapping.core.util + +import aws.smithy.kotlin.runtime.ExperimentalApi + +/** + * Represents a value which may be one of two possible types: [L] or [R]. An instance of this type will be either [Left] + * or [Right]. + * + * By convention [Either] is **right-biased**, meaning that [Right] values are the default values to operate on (e.g., + * via [map]) and [Left] value are typically unmodified. This lends itself to using [R]/[Right] for values which may + * require more processing and using [L]/[Left] for values which are relatively "final". + * + * @param L The type of [Left] values + * @param R The type of [Right] values + */ +@ExperimentalApi +public sealed interface Either { + /** + * The left side of an [Either] + * @param L The type of values held in this class + */ + @ExperimentalApi + public interface Left : Either { + /** + * An [L] value + */ + public val value: L + } + + /** + * The right side of an [Either] + * @param R The type of values held in this class + */ + @ExperimentalApi + public interface Right : Either { + /** + * An [R] value + */ + public val value: R + } + + @ExperimentalApi + public companion object { + /** + * Creates a new [Left] with the given [value] + * @param L The type of values held in this class + * @param value An [L] value + */ + public fun Left(value: L): Left = LeftImpl(value) + + /** + * Creates a new [Right] with the given [value] + * @param R The type of values held in this class + * @param value An [R] value + */ + public fun Right(value: R): Right = RightImpl(value) + } +} + +private data class LeftImpl(override val value: L) : Either.Left + +private data class RightImpl(override val value: R) : Either.Right + +/** + * Map the right value of this [Either] to a new value. Left values are unmodified. + * @param L The type of left value + * @param R The current type of right value + * @param R2 The new type of right value + * @param func A mapping function which turns an [R] into an [R2] + */ +@ExperimentalApi +public inline fun Either.map(func: (right: R) -> R2): Either = when (this) { + is Either.Left -> this + is Either.Right -> Either.Right(func(value)) +} + +/** + * Transform this [Either] into a value of type [T] via specialized mapping functions for both left and right values + * @param L The current type of left value + * @param R The current type of right value + * @param T The type of output value + * @param ifLeft A function for converting [L] values to [T] + * @param ifRight A function for converting [R] values to [T] + */ +@ExperimentalApi +public inline fun Either.fold(ifLeft: (left: L) -> T, ifRight: (right: R) -> T): T = when (this) { + is Either.Left -> ifLeft(value) + is Either.Right -> ifRight(value) +} + +/** + * Returns the left value or right value + * @param T The type of values in left/right + */ +@ExperimentalApi +public fun Either.merge(): T = fold({ it }, { it }) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7a29b53784f..203fcef7b8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,14 @@ pluginManagement { } } } + resolutionStrategy { + val sdkVersion: String by settings + eachPlugin { + if (requested.id.id == "aws.sdk.kotlin.hll.dynamodbmapper.schema.generator") { + useModule("aws.sdk.kotlin:dynamodb-mapper-schema-generator-plugin:$sdkVersion") + } + } + } } dependencyResolutionManagement { @@ -39,6 +47,9 @@ include(":aws-runtime:aws-core") include(":aws-runtime:aws-config") include(":aws-runtime:aws-endpoint") include(":aws-runtime:aws-http") +include(":hll") +include(":hll:hll-codegen") +include(":hll:hll-mapping-core") include(":services") include(":tests") include(":tests:codegen:event-stream") @@ -57,6 +68,19 @@ file("services").listFiles().forEach { } } +if ("dynamodb".isBootstrappedService) { + include(":hll:dynamodb-mapper") + include(":hll:dynamodb-mapper:dynamodb-mapper") + include(":hll:dynamodb-mapper:dynamodb-mapper-codegen") + include(":hll:dynamodb-mapper:dynamodb-mapper-ops-codegen") + include(":hll:dynamodb-mapper:dynamodb-mapper-schema-codegen") + include(":hll:dynamodb-mapper:dynamodb-mapper-annotations") + include(":hll:dynamodb-mapper:dynamodb-mapper-schema-generator-plugin") + include(":hll:dynamodb-mapper:tests:dynamodb-mapper-schema-generator-plugin-test") +} else { + logger.warn(":services:dynamodb is not bootstrapped, skipping :hll:dynamodb-mapper and subprojects") +} + // Service benchmarks project val benchmarkServices = listOf( // keep this list in sync with tests/benchmarks/service-benchmarks/build.gradle.kts