From 0716518ca0d60bde6684de6f2329134625d2ddbc Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Thu, 24 Jul 2025 15:21:21 -0400 Subject: [PATCH] Implement @cacheable for servers --- codegen-core/build.gradle.kts | 7 +- .../common-test-models/cacheable-test.smithy | 71 +++ .../codegen/core/rustlang/CargoDependency.kt | 6 + .../rust/codegen/core/rustlang/RustType.kt | 22 + .../rust/codegen/core/rustlang/RustWriter.kt | 2 +- .../core/smithy/CacheableSymbolProvider.kt | 59 +++ .../rust/codegen/core/smithy/RuntimeType.kt | 5 + .../rust/codegen/core/smithy/SymbolExt.kt | 8 + .../serialize/CborSerializerGenerator.kt | 102 ++-- .../CacheableStructureGeneratorTest.kt | 95 ++++ .../src/test/resources/cacheable-test.smithy | 71 +++ codegen-server/build.gradle.kts | 1 + .../server/smithy/RustServerCodegenPlugin.kt | 15 +- .../WireCacheableTraitDecorator.kt | 118 +++++ .../server/smithy/CacheableTraitTest.kt | 168 +++++++ codegen-traits/build.gradle.kts | 73 +++ .../rust/codegen/traits/CacheableTrait.kt | 37 ++ ...re.amazon.smithy.model.traits.TraitService | 1 + .../META-INF/smithy/cacheable.smithy | 6 + .../main/resources/META-INF/smithy/manifest | 1 + .../codegen/traits/CacheableTraitTest.java | 25 + rust-runtime/Cargo.lock | 452 ++++++------------ rust-runtime/aws-smithy-cbor/Cargo.toml | 2 +- rust-runtime/aws-smithy-cbor/src/encode.rs | 9 + .../aws-smithy-cbor/tests/test_encoder.rs | 149 ++++++ .../tests/test_preserialized_data.rs | 80 ++++ rust-runtime/inlineable/src/cacheable.rs | 277 +++++++++++ rust-runtime/inlineable/src/cacheable_test.rs | 167 +++++++ rust-runtime/inlineable/src/lib.rs | 2 + settings.gradle.kts | 1 + 30 files changed, 1699 insertions(+), 333 deletions(-) create mode 100644 codegen-core/common-test-models/cacheable-test.smithy create mode 100644 codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CacheableSymbolProvider.kt create mode 100644 codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CacheableStructureGeneratorTest.kt create mode 100644 codegen-core/src/test/resources/cacheable-test.smithy create mode 100644 codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/WireCacheableTraitDecorator.kt create mode 100644 codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/CacheableTraitTest.kt create mode 100644 codegen-traits/build.gradle.kts create mode 100644 codegen-traits/src/main/kotlin/software/amazon/smithy/rust/codegen/traits/CacheableTrait.kt create mode 100644 codegen-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService create mode 100644 codegen-traits/src/main/resources/META-INF/smithy/cacheable.smithy create mode 100644 codegen-traits/src/main/resources/META-INF/smithy/manifest create mode 100644 codegen-traits/src/test/java/software/amazon/smithy/rust/codegen/traits/CacheableTraitTest.java create mode 100644 rust-runtime/aws-smithy-cbor/tests/test_encoder.rs create mode 100644 rust-runtime/aws-smithy-cbor/tests/test_preserialized_data.rs create mode 100644 rust-runtime/inlineable/src/cacheable.rs create mode 100644 rust-runtime/inlineable/src/cacheable_test.rs diff --git a/codegen-core/build.gradle.kts b/codegen-core/build.gradle.kts index 669f2c45055..632b2c2e084 100644 --- a/codegen-core/build.gradle.kts +++ b/codegen-core/build.gradle.kts @@ -23,6 +23,7 @@ val smithyVersion: String by project dependencies { implementation(kotlin("stdlib-jdk8")) + implementation(project(":codegen-traits")) implementation("org.jsoup:jsoup:1.16.2") api("software.amazon.smithy:smithy-codegen-core:$smithyVersion") api("com.moandjiezana.toml:toml4j:0.7.2") @@ -80,7 +81,8 @@ val generateBuildEnvironmentConstants = tasks.register("generateBuildEnvironment // Generate the Kotlin file. val generatedFile = file("$outputDir/BuildEnvironment.kt") - generatedFile.writeText(""" + generatedFile.writeText( + """ // This file is automatically generated. Do not modify manually. package software.amazon.smithy.rust.codegen.core.generated @@ -88,7 +90,8 @@ val generateBuildEnvironmentConstants = tasks.register("generateBuildEnvironment const val MSRV: String = "$rustMsrv" const val PROJECT_DIR: String = "$rootDir" } - """.trimIndent()) + """.trimIndent(), + ) } } diff --git a/codegen-core/common-test-models/cacheable-test.smithy b/codegen-core/common-test-models/cacheable-test.smithy new file mode 100644 index 00000000000..b9dd4240637 --- /dev/null +++ b/codegen-core/common-test-models/cacheable-test.smithy @@ -0,0 +1,71 @@ +$version: "2" + +namespace example.cacheable + +use smithy.rust#cacheable + +/// A service that uses the CBOR protocol and has cacheable members +@protocols([{name: "smithy.rpcv2.cbor"}]) +service CacheableService { + version: "2023-01-01", + operations: [GetUserData] +} + +/// Get user data operation +operation GetUserData { + input: GetUserDataInput, + output: GetUserDataOutput +} + +/// Input for GetUserData operation +structure GetUserDataInput { + /// User ID to retrieve data for + userId: String +} + +/// Output for GetUserData operation +structure GetUserDataOutput { + /// User data that can be cached + @cacheable + userData: UserData, + + /// Request ID for tracing + requestId: String +} + +/// User data structure +structure UserData { + /// User name + name: String, + + /// User email + email: String, + + /// User preferences that can be cached + @cacheable + preferences: UserPreferences +} + +/// User preferences structure +structure UserPreferences { + /// Theme preference + theme: String, + + /// Language preference + language: String, + + /// Notification settings + notifications: Boolean +} + +/// A list of users that can be cached +structure UserList { + /// List of users + @cacheable + users: Users +} + +/// List of user data +list Users { + member: UserData +} diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt index 72ece5e46c2..726a98763d7 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt @@ -180,6 +180,12 @@ class InlineDependency( "sdk_feature_tracker", CargoDependency.smithyRuntime(runtimeConfig), ) + + fun cacheable(): InlineDependency = + forInlineableRustFile( + "cacheable", + CargoDependency.Bytes, + ) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt index 203366f3111..d8ff9c7ef8b 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.rust.codegen.core.rustlang +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.deprecated import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.derive import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.serde import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType @@ -34,6 +35,8 @@ sealed class RustType { val member: RustType val namespace: kotlin.String? val name: kotlin.String + + fun map(f: (RustType) -> RustType): RustType } /* @@ -107,11 +110,16 @@ sealed class RustType { data class Slice(override val member: RustType) : RustType(), Container { override val name: kotlin.String = "" + + override fun map(f: (RustType) -> RustType): RustType = this.copy(f(member)) } data class HashMap(val key: RustType, override val member: RustType) : RustType(), Container { // validating that `key` is a string occurs in the constructor in SymbolVisitor override val name = RuntimeType.HashMap.name + + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) + override val namespace = RuntimeType.HashMap.namespace companion object { @@ -124,6 +132,8 @@ sealed class RustType { override val name = RuntimeType.Vec.name override val namespace = RuntimeType.Vec.namespace + override fun map(f: (RustType) -> RustType): RustType = this.copy(f(member)) + companion object { // This is Vec intentionally. Note the following passage from the Smithy spec: // Sets MUST be insertion ordered. Not all programming languages that support sets @@ -136,6 +146,8 @@ sealed class RustType { } data class Reference(val lifetime: kotlin.String?, override val member: RustType) : RustType(), Container { + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) + override val name = member.name } @@ -144,6 +156,8 @@ sealed class RustType { override val name = runtimeType.name override val namespace = runtimeType.namespace + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) + /** Convert `Option` to `Option<&T>` **/ fun referenced(lifetime: kotlin.String?): Option { return Option(Reference(lifetime, this.member)) @@ -154,23 +168,31 @@ sealed class RustType { private val runtimeType = RuntimeType.MaybeConstrained override val name = runtimeType.name override val namespace = runtimeType.namespace + + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) } data class Box(override val member: RustType) : RustType(), Container { private val runtimeType = RuntimeType.Box override val name = runtimeType.name override val namespace = runtimeType.namespace + + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) } data class Dyn(override val member: RustType) : RustType(), Container { override val name = "dyn" override val namespace: kotlin.String? = null + + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) } data class Vec(override val member: RustType) : RustType(), Container { private val runtimeType: RuntimeType = RuntimeType.Vec override val name = runtimeType.name override val namespace = runtimeType.namespace + + override fun map(f: (RustType) -> RustType): RustType = this.copy(member = f(member)) } data class Opaque(override val name: kotlin.String, override val namespace: kotlin.String? = null) : RustType() diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt index 80ae2915376..9b5826838e4 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt @@ -13,7 +13,6 @@ import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolDependencyContainer import software.amazon.smithy.codegen.core.SymbolWriter -import software.amazon.smithy.codegen.core.SymbolWriter.Factory import software.amazon.smithy.model.Model import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.shapes.BooleanShape @@ -844,6 +843,7 @@ class RustWriter private constructor( } } } + else -> rustBlock("") { block(variable) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CacheableSymbolProvider.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CacheableSymbolProvider.kt new file mode 100644 index 00000000000..6a91c549d1e --- /dev/null +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/CacheableSymbolProvider.kt @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.ListShape +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.rust.codegen.core.rustlang.RustType +import software.amazon.smithy.rust.codegen.traits.CacheableTrait + +/** + * Wrapping symbol provider support adding Cacheable to members + */ +class CacheableSymbolProvider(private val base: RustSymbolProvider) : WrappingSymbolProvider(base) { + private val runtimeConfig = base.config.runtimeConfig + + override fun toSymbol(shape: Shape): Symbol { + return when (shape) { + is ListShape -> + base.toSymbol(shape) + .mapRustType { ty -> (ty as RustType.Vec).copy(member = toSymbol(shape.member).rustType()) } + + is MemberShape -> { + val initial = base.toSymbol(shape) + val targetShape = model.expectShape(shape.target) + val cacheableType = RuntimeType.Cacheable.toSymbol().rustType() + + return if (shape.hasTrait(CacheableTrait::class.java)) { + val cacheable = RuntimeType.Cacheable + initial.mapRustType { initial -> + when (initial) { + is RustType.Option -> initial.map { RustType.Application(cacheableType, listOf(it)) } + + else -> + RustType.Application( + type = cacheableType, + args = listOf(initial), + ) + } + }.toBuilder().addReference(cacheable.toSymbol()).build() + } else { + val targetSymbol = toSymbol(targetShape) + initial.mapRustType { memberRustType -> + when (memberRustType) { + is RustType.Container -> memberRustType.map { targetSymbol.rustType() } + else -> memberRustType + } + } + } + } + + else -> base.toSymbol(shape) + } + } +} diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index 585f3fd2ed8..845c4ee9199 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -308,6 +308,8 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) val MaybeConstrained = RuntimeType("crate::constrained::MaybeConstrained", InlineDependency.constrained()) + val Cacheable = cacheable().resolve("Cacheable") + // smithy runtime types fun smithyAsync(runtimeConfig: RuntimeConfig) = CargoDependency.smithyAsync(runtimeConfig).toType() @@ -488,6 +490,7 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) // clients allow offsets, servers do nt TimestampFormatTrait.Format.DATE_TIME -> codegenTarget.ifClient { "DateTimeWithOffset" } ?: "DateTime" + TimestampFormatTrait.Format.HTTP_DATE -> "HttpDate" TimestampFormatTrait.Format.UNKNOWN -> TODO() } @@ -545,5 +548,7 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun clientRequestCompression(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.clientRequestCompression(runtimeConfig)) + + fun cacheable() = forInlineDependency(InlineDependency.cacheable()) } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolExt.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolExt.kt index 78b348e6c2e..34f88029344 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolExt.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolExt.kt @@ -124,6 +124,14 @@ fun Symbol.isOptional(): Boolean = else -> false } +fun Symbol.isCacheable(): Boolean { + val rustType = this.rustType().stripOuter() + if (rustType is RustType.Application) { + return rustType.type.name == "Cacheable" + } + return false +} + fun Symbol.isRustBoxed(): Boolean = rustType().stripOuter() is RustType.Box private const val RUST_TYPE_KEY = "rusttype" diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt index 3c26c6243ce..7b14c789efb 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt @@ -42,6 +42,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.Section import software.amazon.smithy.rust.codegen.core.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.core.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.core.smithy.generators.serializationError +import software.amazon.smithy.rust.codegen.core.smithy.isCacheable import software.amazon.smithy.rust.codegen.core.smithy.isOptional import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpBindingResolver import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpLocation @@ -70,8 +71,10 @@ sealed class CborSerializerSection(name: String) : Section(name) { ) : CborSerializerSection("BeforeSerializingStructureMembers") /** Manipulate the serializer context for a map prior to it being serialized. **/ - data class BeforeIteratingOverMapOrCollection(val shape: Shape, val context: CborSerializerGenerator.Context) : - CborSerializerSection("BeforeIteratingOverMapOrCollection") + data class BeforeIteratingOverMapOrCollection( + val shape: Shape, + val context: CborSerializerGenerator.Context, + ) : CborSerializerSection("BeforeIteratingOverMapOrCollection") /** Manipulate the serializer context for a non-null member prior to it being serialized. **/ data class BeforeSerializingNonNullMember(val shape: Shape, val context: CborSerializerGenerator.MemberContext) : @@ -82,24 +85,31 @@ sealed class CborSerializerSection(name: String) : Section(name) { * This customization point enables extending the serializer's interface with supplementary parameters * needed for specialized serialization behaviors. */ - data class AdditionalSerializingParameters(val structContext: CborSerializerGenerator.StructContext, val codegenContext: CodegenContext) : - CborSerializerSection("AdditionalSerializingParameters") + data class AdditionalSerializingParameters( + val structContext: CborSerializerGenerator.StructContext, + val codegenContext: CodegenContext, + ) : CborSerializerSection("AdditionalSerializingParameters") /** * Provides a way to specify additional arguments that should be passed when invoking the serializer. * This customization point allows for passing through context-specific information needed during * the serialization process. */ - data class AdditionalSerializingArguments(val structContext: CborSerializerGenerator.StructContext, val codegenContext: CodegenContext) : - CborSerializerSection("AdditionalSerializingArguments") + data class AdditionalSerializingArguments( + val structContext: CborSerializerGenerator.StructContext, + val codegenContext: CodegenContext, + ) : CborSerializerSection("AdditionalSerializingArguments") /** * Customizes how a union variant's shape ID is encoded in the CBOR format. * This section allows for specialized handling of union variant identification * during serialization. */ - data class CustomizeUnionMemberKeyEncode(val context: CborSerializerGenerator.MemberContext, val encoderBindingName: String, val codegenContext: CodegenContext) : - CborSerializerSection("CustomizeUnionMemberKeyEncode") + data class CustomizeUnionMemberKeyEncode( + val context: CborSerializerGenerator.MemberContext, + val encoderBindingName: String, + val codegenContext: CodegenContext, + ) : CborSerializerSection("CustomizeUnionMemberKeyEncode") /** * Allows customization of the CBOR map length calculation for union types. @@ -206,6 +216,7 @@ class CborSerializerGenerator( "Error" to runtimeConfig.serializationError(), "Encoder" to RuntimeType.smithyCbor(runtimeConfig).resolve("Encoder"), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + "Cacheable" to RuntimeType.Cacheable, ) private val serializerUtil = SerializerUtil(model, symbolProvider) @@ -333,8 +344,7 @@ class CborSerializerGenerator( private fun getCustomizedParamsAndArgsForStructSerializer( structContext: StructContext, sectionType: (StructContext, CodegenContext) -> CborSerializerSection, - ) = customizations - .map { it.section(sectionType(structContext, codegenContext)) } + ) = customizations.map { it.section(sectionType(structContext, codegenContext)) } .filter { it.isNotEmpty() } // Remove any empty customizations. .takeIf { it.isNotEmpty() } // Proceed only if there are remaining customizations. ?.join(", ", prefix = ", ") // Join with commas and add leading comma. @@ -407,6 +417,30 @@ class CborSerializerGenerator( ) } + private fun RustWriter.handleCacheable( + context: MemberContext, + main: Writable, + ) { + if (symbolProvider.toSymbol(context.shape).isCacheable()) { + val modeledMember = + writable { + serializeMemberValue(context.copy(valueExpression = ValueExpression.Reference("data"))) + } + rustTemplate( + """ + match ${context.valueExpression.asRef()} { + #{Cacheable}::Cached(data) => { ${context.encoderBindingName}.write_preserialized_data(&data); }, + #{Cacheable}::Modeled(data) => { #{modeledMember} } + } + """, + *codegenScope, + "modeledMember" to modeledMember, + ) + } else { + main(this) + } + } + private fun RustWriter.serializeMember(context: MemberContext) { val targetShape = model.expectShape(context.shape.target) if (symbolProvider.toSymbol(context.shape).isOptional()) { @@ -414,7 +448,9 @@ class CborSerializerGenerator( rustBlock("if let Some($local) = ${context.valueExpression.asRef()}") { context.valueExpression = ValueExpression.Reference(local) resolveValueExpressionForConstrainedType(targetShape, context) - serializeMemberValue(context, targetShape) + handleCacheable(context) { + serializeMemberValue(context) + } } if (context.writeNulls) { rustBlock("else") { @@ -426,7 +462,9 @@ class CborSerializerGenerator( resolveValueExpressionForConstrainedType(targetShape, context) with(serializerUtil) { ignoreDefaultsForNumbersAndBools(context.shape, context.valueExpression) { - serializeMemberValue(context, targetShape) + handleCacheable(context) { + serializeMemberValue(context) + } } } } @@ -446,31 +484,24 @@ class CborSerializerGenerator( } } - private fun RustWriter.serializeMemberValue( - context: MemberContext, - target: Shape, - ) { + private fun RustWriter.serializeMemberValue(context: MemberContext) { val encoder = context.encoderBindingName val value = context.valueExpression val containerShape = model.expectShape(context.shape.container) + val target = model.expectShape(context.shape.target) when (target) { // Simple shapes: https://smithy.io/2.0/spec/simple-types.html is BlobShape -> rust("$encoder.blob(${value.asRef()});") is BooleanShape -> rust("$encoder.boolean(${value.asValue()});") - is StringShape -> rust("$encoder.str(${value.name}.as_str());") - is ByteShape -> rust("$encoder.byte(${value.asValue()});") is ShortShape -> rust("$encoder.short(${value.asValue()});") is IntegerShape -> rust("$encoder.integer(${value.asValue()});") is LongShape -> rust("$encoder.long(${value.asValue()});") - is FloatShape -> rust("$encoder.float(${value.asValue()});") is DoubleShape -> rust("$encoder.double(${value.asValue()});") - is TimestampShape -> rust("$encoder.timestamp(${value.asRef()});") - is DocumentShape -> UNREACHABLE("Smithy RPC v2 CBOR does not support `document` shapes") // Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html @@ -499,7 +530,12 @@ class CborSerializerGenerator( private fun RustWriter.serializeCollection(context: Context) { for (customization in customizations) { - customization.section(CborSerializerSection.BeforeIteratingOverMapOrCollection(context.shape, context))(this) + customization.section( + CborSerializerSection.BeforeIteratingOverMapOrCollection( + context.shape, + context, + ), + )(this) } rust("encoder.array((${context.valueExpression.asValue()}).len());") val itemName = safeName("item") @@ -512,7 +548,12 @@ class CborSerializerGenerator( val keyName = safeName("key") val valueName = safeName("value") for (customization in customizations) { - customization.section(CborSerializerSection.BeforeIteratingOverMapOrCollection(context.shape, context))(this) + customization.section( + CborSerializerSection.BeforeIteratingOverMapOrCollection( + context.shape, + context, + ), + )(this) } rust("encoder.map((${context.valueExpression.asValue()}).len());") rustBlock("for ($keyName, $valueName) in ${context.valueExpression.asRef()}") { @@ -585,16 +626,13 @@ class CborSerializerGenerator( ): Writable = customizations.map { customization -> customization.section(section) - } - .filter { it.isNotEmpty() } - .also { filteredCustomizations -> - if (filteredCustomizations.size > 1) { - throw IllegalArgumentException( - "Found ${filteredCustomizations.size} $customizationName customizations, but only one is allowed.", - ) - } + }.filter { it.isNotEmpty() }.also { filteredCustomizations -> + if (filteredCustomizations.size > 1) { + throw IllegalArgumentException( + "Found ${filteredCustomizations.size} $customizationName customizations, but only one is allowed.", + ) } - .firstOrNull() ?: writable {} + }.firstOrNull() ?: writable {} /** * Process customizations for union variant encoding, ensuring only one customization exists. diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CacheableStructureGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CacheableStructureGeneratorTest.kt new file mode 100644 index 00000000000..9b9363e9b00 --- /dev/null +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/CacheableStructureGeneratorTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy.generators + +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor +import kotlin.test.assertTrue + +class CacheableStructureGeneratorTest { + companion object { + val model = + Model.assembler() + .addImport(CacheableStructureGeneratorTest::class.java.getResource("/cacheable-test.smithy")) + .assemble() + .unwrap() + } + + @Test + fun `test structure generator wraps cacheable members`() { + val symbolProvider = RustSymbolProvider(SymbolVisitor(model)) + val writer = RustWriter("test") + + // Get the GetUserDataOutput structure which has a cacheable member + val outputShape = model.expectShape(ShapeId.from("example.cacheable#GetUserDataOutput"), StructureShape::class.java) + + // Create a structure generator for the output shape + val generator = + StructureGenerator( + model, + symbolProvider, + writer, + outputShape, + emptyList(), + StructSettings(flattenVecAccessors = false), + ) + + // Generate the structure + generator.render() + + // Get the generated code + val generatedCode = writer.toString() + + // Verify that the userData field is wrapped in Cacheable + assertTrue( + generatedCode.contains("userData: Cacheable"), + "Generated code should wrap userData in Cacheable", + ) + + // Verify that the requestId field is not wrapped + assertTrue( + generatedCode.contains("requestId: String"), + "Generated code should not wrap requestId", + ) + } + + @Test + fun `test structure generator adds cacheable imports`() { + val symbolProvider = RustSymbolProvider(SymbolVisitor(model)) + val writer = RustWriter("test") + + // Get the GetUserDataOutput structure which has a cacheable member + val outputShape = model.expectShape(ShapeId.from("example.cacheable#GetUserDataOutput"), StructureShape::class.java) + + // Create a structure generator for the output shape + val generator = + StructureGenerator( + model, + symbolProvider, + writer, + outputShape, + emptyList(), + StructSettings(flattenVecAccessors = false), + ) + + // Generate the structure + generator.render() + + // Get the generated code + val generatedCode = writer.toString() + + // Verify that the Bytes import is added + assertTrue( + generatedCode.contains("use bytes::Bytes;"), + "Generated code should import bytes::Bytes", + ) + } +} diff --git a/codegen-core/src/test/resources/cacheable-test.smithy b/codegen-core/src/test/resources/cacheable-test.smithy new file mode 100644 index 00000000000..d353c5c9114 --- /dev/null +++ b/codegen-core/src/test/resources/cacheable-test.smithy @@ -0,0 +1,71 @@ +$version: "2" + +namespace example.cacheable + +use smithy.rust.#cacheable + +/// A service that uses the CBOR protocol and has cacheable members +@protocols([{name: "smithy.rpcv2.cbor"}]) +service CacheableService { + version: "2023-01-01", + operations: [GetUserData] +} + +/// Get user data operation +operation GetUserData { + input: GetUserDataInput, + output: GetUserDataOutput +} + +/// Input for GetUserData operation +structure GetUserDataInput { + /// User ID to retrieve data for + userId: String +} + +/// Output for GetUserData operation +structure GetUserDataOutput { + /// User data that can be cached + @cacheable + userData: UserData, + + /// Request ID for tracing + requestId: String +} + +/// User data structure +structure UserData { + /// User name + name: String, + + /// User email + email: String, + + /// User preferences that can be cached + @cacheable + preferences: UserPreferences +} + +/// User preferences structure +structure UserPreferences { + /// Theme preference + theme: String, + + /// Language preference + language: String, + + /// Notification settings + notifications: Boolean +} + +/// A list of users that can be cached +structure UserList { + /// List of users + @cacheable + users: Users +} + +/// List of user data +list Users { + member: UserData +} diff --git a/codegen-server/build.gradle.kts b/codegen-server/build.gradle.kts index 49e0462888c..e32cd781b51 100644 --- a/codegen-server/build.gradle.kts +++ b/codegen-server/build.gradle.kts @@ -24,6 +24,7 @@ val smithyVersion: String by project dependencies { implementation(project(":codegen-core")) + implementation(project(":codegen-traits")) implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion") implementation("software.amazon.smithy:smithy-protocol-traits:$smithyVersion") diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt index e68fa26a33c..731351c4889 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustServerCodegenPlugin.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWordSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.BaseSymbolMetadataProvider +import software.amazon.smithy.rust.codegen.core.smithy.CacheableSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget import software.amazon.smithy.rust.codegen.core.smithy.EventStreamSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig @@ -20,6 +21,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator +import software.amazon.smithy.rust.codegen.server.smithy.customizations.WireCacheableTraitDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator import software.amazon.smithy.rust.codegen.server.smithy.testutil.ServerDecoratableBuildPlugin @@ -52,6 +54,7 @@ class RustServerCodegenPlugin : ServerDecoratableBuildPlugin() { ServerRequiredCustomizations(), SmithyValidationExceptionDecorator(), CustomValidationExceptionWithReasonDecorator(), + WireCacheableTraitDecorator(), *decorator, ) logger.info("Loaded plugin to generate pure Rust bindings for the server SDK") @@ -73,12 +76,22 @@ class RustServerCodegenPlugin : ServerDecoratableBuildPlugin() { ) = SymbolVisitor(settings, model, serviceShape = serviceShape, config = rustSymbolProviderConfig) // Generate public constrained types for directly constrained shapes. .let { - if (includeConstrainedShapeProvider) ConstrainedShapeSymbolProvider(it, serviceShape, constrainedTypes) else it + if (includeConstrainedShapeProvider) { + ConstrainedShapeSymbolProvider( + it, + serviceShape, + constrainedTypes, + ) + } else { + it + } } // Generate different types for EventStream shapes (e.g. transcribe streaming) .let { EventStreamSymbolProvider(rustSymbolProviderConfig.runtimeConfig, it, CodegenTarget.SERVER) } // Generate [ByteStream] instead of `Blob` for streaming binary shapes (e.g. S3 GetObject) .let { StreamingShapeSymbolProvider(it) } + // support the cacheable trait + .let { CacheableSymbolProvider(it) } // Add Rust attributes (like `#[derive(PartialEq)]`) to generated shapes .let { BaseSymbolMetadataProvider(it, additionalAttributes = listOf()) } // Constrained shapes generate newtypes that need the same derives we place on types generated from aggregate shapes. diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/WireCacheableTraitDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/WireCacheableTraitDecorator.kt new file mode 100644 index 00000000000..db5bfd619b4 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/WireCacheableTraitDecorator.kt @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.customizations + +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureCustomization +import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureSection +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator +import software.amazon.smithy.rust.codegen.traits.CacheableTrait +import kotlin.streams.asSequence + +/** + * Decorator that adds WireCacheable trait implementations to structures that are targeted by @cacheable members. + */ +class WireCacheableTraitDecorator : ServerCodegenDecorator { + override val name: String = "WireCacheableTraitDecorator" + override val order: Byte = 0 + + override fun structureCustomizations( + codegenContext: ServerCodegenContext, + baseCustomizations: List, + ): List { + return baseCustomizations + WireCacheableTraitCustomization(codegenContext) + } +} + +/** + * Customization that adds WireCacheable trait implementation to structures that are targeted by @cacheable members. + */ +class WireCacheableTraitCustomization(private val codegenContext: ServerCodegenContext) : StructureCustomization() { + override fun section(section: StructureSection): Writable { + return when (section) { + is StructureSection.AdditionalTraitImpls -> { + if (shouldImplementWireCacheable(section.shape)) { + generateWireCacheableImpl(section.shape, section.structName) + } else { + writable { } + } + } + + else -> writable { } + } + } + + /** + * Check if this structure should implement WireCacheable. + * A structure should implement WireCacheable if it's targeted by any @cacheable member. + */ + private fun shouldImplementWireCacheable(shape: StructureShape): Boolean { + val model = codegenContext.model + + // Find all members in the model that have the @cacheable trait + return model.shapes() + .asSequence() + .filterIsInstance() + .filter { it.hasTrait(CacheableTrait::class.java) } + .any { member -> + // Check if this member targets our structure shape + val targetShape = model.expectShape(member.target) + targetShape.id == shape.id + } + } + + /** + * Generate the WireCacheable trait implementation for the given structure. + */ + private fun generateWireCacheableImpl( + shape: StructureShape, + structName: String, + ): Writable { + return writable { + val runtimeConfig = codegenContext.runtimeConfig + val cacheableModule = RuntimeType.cacheable() + + // Use the existing serializer function that should be generated + val serializerFnName = + "crate::protocol_serde::shape_${shape.id.name.lowercase()}::ser_${shape.id.name.lowercase()}" + + rustTemplate( + """ + impl #{WireCacheable} for $structName { + /// Serialize this value to CBOR bytes for caching + fn to_bytes(&self) -> #{Bytes} { + let buffer = Vec::new(); + let mut encoder = #{CborEncoder}::new(buffer); + + // Delegate to the existing CBOR serializer for this shape + $serializerFnName(&mut encoder, self) + .expect("serialization should be infallible"); + + #{Bytes}::from(encoder.into_writer()) + } + + /// Validate serialized CBOR bytes for this shape + fn validate(_bytes: &[u8]) -> ::std::result::Result<(), #{ValidationError}> { + // TODO(wireCaching): This need to actually generate a bespoke deserializer for this shape. + Ok(()) + } + } + """, + "WireCacheable" to cacheableModule.resolve("WireCacheable"), + "ValidationError" to cacheableModule.resolve("ValidationError"), + "Bytes" to RuntimeType.Bytes, + "CborEncoder" to RuntimeType.smithyCbor(runtimeConfig).resolve("Encoder"), + "CborDecoder" to RuntimeType.smithyCbor(runtimeConfig).resolve("Decoder"), + ) + } + } +} diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/CacheableTraitTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/CacheableTraitTest.kt new file mode 100644 index 00000000000..f5ea621a02d --- /dev/null +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/CacheableTraitTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy + +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.Model +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.testModule +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest + +object CacheableModels { + fun basicModel(): Model { + return """ + namespace test + + use smithy.rust#cacheable + use smithy.protocols#rpcv2Cbor + use smithy.framework#ValidationException + + @rpcv2Cbor + service SampleServiceWITHDifferentCASE { + operations: [SampleOP], + } + operation SampleOP { + input:= { + x: String + } + output := { + @cacheable + item: Item + + cachedItems: CachedItems + + } + } + + structure Item { + a: String + } + + list CachedItems { + @cacheable + member: Item + } + """.asSmithyModel(smithyVersion = "2") + } +} + +class CacheableTraitTest { + @Test + fun `test basic model compiles`() { + serverIntegrationTest(CacheableModels.basicModel()) { _, _ -> } + } + + @Test + fun `test wire caching HTTP response consistency`() { + serverIntegrationTest(CacheableModels.basicModel()) { _, rustCrate -> + rustCrate.testModule { + tokioTest("single_item_http_response_consistency") { + rust( + """ + use crate::model::Item; + use crate::output::SampleOpOutput; + use crate::cacheable::{Cacheable, WireCacheable}; + use aws_smithy_http_server::response::IntoResponse; + use hyper::body::to_bytes; + + // Create test item + let item = Item::builder() + .a(Some("test_value".to_string())) + .build(); + + // Create output with modeled cacheable + let output_modeled = SampleOpOutput::builder() + .item(Some(Cacheable::modeled(item.clone()))) + .build(); + + // Create output with cached cacheable + let output_cached = SampleOpOutput::builder() + .item(Some(Cacheable::cached(item.to_bytes()))) + .build(); + + // Convert both to HTTP responses + let http_response_modeled = output_modeled.into_response(); + let http_response_cached = output_cached.into_response(); + + // Extract response bodies + let body_modeled = to_bytes(http_response_modeled.into_body()).await + .expect("unable to extract body to bytes"); + let body_cached = to_bytes(http_response_cached.into_body()).await + .expect("unable to extract body to bytes"); + + // HTTP response bodies must be byte-for-byte identical + assert_eq!(body_modeled, body_cached, + "HTTP response bodies must be identical for modeled vs cached items"); + """, + ) + } + + tokioTest("list_http_response_consistency") { + rust( + """ + use crate::model::Item; + use crate::output::SampleOpOutput; + use crate::cacheable::{Cacheable, WireCacheable}; + use aws_smithy_http_server::response::IntoResponse; + use hyper::body::to_bytes; + + // Create test items + let item1 = Item::builder().a(Some("item1".to_string())).build(); + let item2 = Item::builder().a(Some("item2".to_string())).build(); + let item3 = Item::builder().a(Some("item3".to_string())).build(); + + // Create outputs with different caching patterns + let output_all_modeled = SampleOpOutput::builder() + .cached_items(Some(vec![ + Cacheable::modeled(item1.clone()), + Cacheable::modeled(item2.clone()), + Cacheable::modeled(item3.clone()), + ])) + .build(); + + let output_all_cached = SampleOpOutput::builder() + .cached_items(Some(vec![ + Cacheable::cached(item1.to_bytes()), + Cacheable::cached(item2.to_bytes()), + Cacheable::cached(item3.to_bytes()), + ])) + .build(); + + let output_mixed = SampleOpOutput::builder() + .cached_items(Some(vec![ + Cacheable::modeled(item1), + Cacheable::cached(item2.to_bytes()), + Cacheable::modeled(item3), + ])) + .build(); + + // Convert all to HTTP responses + let http_response_modeled = output_all_modeled.into_response(); + let http_response_cached = output_all_cached.into_response(); + let http_response_mixed = output_mixed.into_response(); + + // Extract response bodies + let body_modeled = to_bytes(http_response_modeled.into_body()).await + .expect("unable to extract body to bytes"); + let body_cached = to_bytes(http_response_cached.into_body()).await + .expect("unable to extract body to bytes"); + let body_mixed = to_bytes(http_response_mixed.into_body()).await + .expect("unable to extract body to bytes"); + + // All HTTP response bodies must be byte-for-byte identical + assert_eq!(body_modeled, body_cached, + "All-modeled vs all-cached HTTP responses must be identical"); + assert_eq!(body_modeled, body_mixed, + "All-modeled vs mixed HTTP responses must be identical"); + """, + ) + } + } + } + } +} diff --git a/codegen-traits/build.gradle.kts b/codegen-traits/build.gradle.kts new file mode 100644 index 00000000000..af35a3dc938 --- /dev/null +++ b/codegen-traits/build.gradle.kts @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + `java-library` + kotlin("jvm") + `maven-publish` +} + +description = "Smithy traits for Rust code generation" +extra["displayName"] = "Smithy :: Rust :: Codegen Traits" +extra["moduleName"] = "software.amazon.smithy.rust.codegen.traits" + +group = "software.amazon.smithy.rust.codegen" +version = "0.1.0" + +val smithyVersion: String by project + +dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation("software.amazon.smithy:smithy-model:$smithyVersion") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.compileKotlin { + kotlinOptions.jvmTarget = "11" +} + +tasks.test { + useJUnitPlatform() +} + +// Reusable license copySpec +val licenseSpec = copySpec { + from("${project.rootDir}/LICENSE") + from("${project.rootDir}/NOTICE") +} + +// Configure jars to include license related info +tasks.jar { + metaInf.with(licenseSpec) + inputs.property("moduleName", project.name) + manifest { + attributes["Automatic-Module-Name"] = project.name + } +} + +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("default") { + from(components["java"]) + artifact(sourcesJar) + } + } + repositories { maven { url = uri(layout.buildDirectory.dir("repository")) } } +} diff --git a/codegen-traits/src/main/kotlin/software/amazon/smithy/rust/codegen/traits/CacheableTrait.kt b/codegen-traits/src/main/kotlin/software/amazon/smithy/rust/codegen/traits/CacheableTrait.kt new file mode 100644 index 00000000000..6acb6121a2d --- /dev/null +++ b/codegen-traits/src/main/kotlin/software/amazon/smithy/rust/codegen/traits/CacheableTrait.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.traits + +import software.amazon.smithy.model.SourceLocation +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.AbstractTrait +import software.amazon.smithy.model.traits.Trait + +/** + * Indicates a structure member or list member should support wire caching. + * When a member is marked with this trait, the code generator will wrap the field type + * in a `Cacheable` enum, allowing the server to store and retrieve serialized response data + * without full deserialization and reserialization. + */ +class CacheableTrait(sourceLocation: SourceLocation) : AbstractTrait(ID, sourceLocation) { + override fun createNode(): Node = Node.objectNode() + + class Provider : AbstractTrait.Provider(ID) { + override fun createTrait( + target: ShapeId, + value: Node, + ): Trait { + val result = CacheableTrait(value.sourceLocation) + result.setNodeCache(value) + return result + } + } + + companion object { + val ID: ShapeId = ShapeId.from("smithy.rust#cacheable") + } +} diff --git a/codegen-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/codegen-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 00000000000..5bf87e77694 --- /dev/null +++ b/codegen-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1 @@ +software.amazon.smithy.rust.codegen.traits.CacheableTrait$Provider diff --git a/codegen-traits/src/main/resources/META-INF/smithy/cacheable.smithy b/codegen-traits/src/main/resources/META-INF/smithy/cacheable.smithy new file mode 100644 index 00000000000..3ba653a6401 --- /dev/null +++ b/codegen-traits/src/main/resources/META-INF/smithy/cacheable.smithy @@ -0,0 +1,6 @@ +$version: "2" + +namespace smithy.rust + +@trait(selector: "member") +structure cacheable {} diff --git a/codegen-traits/src/main/resources/META-INF/smithy/manifest b/codegen-traits/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 00000000000..116a735da9c --- /dev/null +++ b/codegen-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +cacheable.smithy diff --git a/codegen-traits/src/test/java/software/amazon/smithy/rust/codegen/traits/CacheableTraitTest.java b/codegen-traits/src/test/java/software/amazon/smithy/rust/codegen/traits/CacheableTraitTest.java new file mode 100644 index 00000000000..acd1075a97a --- /dev/null +++ b/codegen-traits/src/test/java/software/amazon/smithy/rust/codegen/traits/CacheableTraitTest.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.traits; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.ShapeId; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CacheableTraitTest { + + @Test + public void testCacheableTrait() { + CacheableTrait trait = new CacheableTrait(SourceLocation.NONE); + assertEquals(ShapeId.from("smithy.rust#cacheable"), trait.toShapeId()); + + // Test the Provider + CacheableTrait.Provider provider = new CacheableTrait.Provider(); + assertEquals(ShapeId.from("smithy.rust#cacheable"), provider.getShapeId()); + } +} diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 1ab003aee05..ca8df256ef6 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.5.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -106,7 +106,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.5.0", + "async-channel 2.3.1", "async-executor", "async-io", "async-lock", @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" dependencies = [ "async-lock", "cfg-if", @@ -128,9 +128,10 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.0.8", + "rustix 1.0.7", "slab", - "windows-sys 0.60.2", + "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -146,11 +147,11 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" dependencies = [ - "async-channel 2.5.0", + "async-channel 2.3.1", "async-io", "async-lock", "async-signal", @@ -159,14 +160,15 @@ dependencies = [ "cfg-if", "event-listener 5.4.0", "futures-lite", - "rustix 1.0.8", + "rustix 1.0.7", + "tracing", ] [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" dependencies = [ "async-io", "async-lock", @@ -174,10 +176,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.0.8", + "rustix 1.0.7", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -226,7 +228,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -243,7 +245,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -265,15 +267,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-fips-sys" -version = "0.13.7" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2608e5a7965cc9d58c56234d346c9c89b824c4c8652b6f047b3bd0a777c0644f" +checksum = "e99d74bb793a19f542ae870a6edafbc5ecf0bc0ba01d4636b7f7e0aba9ee9bd3" dependencies = [ "bindgen", "cc", @@ -285,9 +287,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -297,9 +299,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" dependencies = [ "bindgen", "cc", @@ -321,7 +323,7 @@ dependencies = [ [[package]] name = "aws-smithy-cbor" -version = "0.61.1" +version = "0.61.2" dependencies = [ "aws-smithy-types", "criterion", @@ -377,17 +379,14 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.10" +version = "0.60.9" dependencies = [ "arbitrary", "aws-smithy-types", "bytes", "bytes-utils", "crc32fast", - "criterion", "derive_arbitrary", - "jemallocator", - "mimalloc", ] [[package]] @@ -396,7 +395,7 @@ version = "0.2.0" [[package]] name = "aws-smithy-http" -version = "0.62.2" +version = "0.62.1" dependencies = [ "async-stream", "aws-smithy-eventstream", @@ -431,8 +430,8 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "h2 0.3.27", - "h2 0.4.11", + "h2 0.3.26", + "h2 0.4.10", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", @@ -443,10 +442,10 @@ dependencies = [ "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", - "indexmap 2.10.0", + "indexmap 2.9.0", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.29", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -515,7 +514,7 @@ dependencies = [ "rcgen", "rustls-pemfile 1.0.4", "signal-hook", - "socket2 0.5.10", + "socket2", "thiserror 2.0.12", "tls-listener", "tokio", @@ -614,7 +613,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.5" +version = "1.8.4" dependencies = [ "approx", "aws-smithy-async", @@ -642,7 +641,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.4" +version = "1.8.3" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -773,12 +772,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64-simd" version = "0.8.0" @@ -808,7 +801,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.104", + "syn 2.0.102", "which", ] @@ -850,11 +843,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.5.0", + "async-channel 2.3.1", "async-task", "futures-io", "futures-lite", @@ -872,9 +865,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" @@ -922,9 +915,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -1010,18 +1003,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstyle", "clap_lex 0.7.5", @@ -1119,15 +1112,15 @@ dependencies = [ "crc", "digest", "libc", - "rand 0.9.2", + "rand 0.9.1", "regex", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1141,7 +1134,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.41", + "clap 4.5.40", "criterion-plot", "futures", "is-terminal", @@ -1206,9 +1199,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" @@ -1243,7 +1236,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -1270,7 +1263,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -1302,12 +1295,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -1443,7 +1436,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -1535,9 +1528,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1545,7 +1538,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1554,9 +1547,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -1564,7 +1557,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1610,9 +1603,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -1723,14 +1716,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.27", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "socket2", "tokio", "tower-service", "tracing", @@ -1746,7 +1739,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.11", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1783,7 +1776,7 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.29", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -1793,11 +1786,10 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ - "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1805,16 +1797,12 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", - "socket2 0.6.0", - "system-configuration", + "socket2", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -1936,9 +1924,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -1987,30 +1975,13 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - [[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi 0.5.1", "libc", "windows-sys 0.59.0", ] @@ -2039,26 +2010,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jemalloc-sys" -version = "0.5.4+5.3.0-patched" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6c1946e1cea1788cbfde01c993b52a10e2da07f4bac608228d1bed20bfebf2" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "jemallocator" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0de374a9f8e63150e6f5e8a60cc14c668226d7a347d8aee1a45766e3c4dd3bc" -dependencies = [ - "jemalloc-sys", - "libc", -] - [[package]] name = "jobserver" version = "0.1.33" @@ -2162,9 +2113,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" @@ -2173,17 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", -] - -[[package]] -name = "libmimalloc-sys" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" -dependencies = [ - "cc", - "libc", + "windows-targets 0.53.0", ] [[package]] @@ -2244,9 +2185,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -2257,15 +2198,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mimalloc" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mime" version = "0.3.17" @@ -2290,7 +2222,7 @@ checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -2390,7 +2322,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi 0.5.1", "libc", ] @@ -2536,7 +2468,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -2592,16 +2524,17 @@ dependencies = [ [[package]] name = "polling" -version = "3.9.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi 0.5.1", "pin-project-lite", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.0.7", + "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -2646,12 +2579,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -2674,7 +2607,7 @@ dependencies = [ "bitflags 2.9.1", "lazy_static", "num-traits", - "rand 0.9.2", + "rand 0.9.1", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax 0.8.5", @@ -2758,7 +2691,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -2771,7 +2704,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -2802,9 +2735,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.3.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" @@ -2819,9 +2752,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2908,9 +2841,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.15" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.1", ] @@ -3030,15 +2963,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3055,16 +2988,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -3132,9 +3065,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -3262,9 +3195,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.10" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" [[package]] name = "security-framework" @@ -3325,16 +3258,16 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.9.0", "itoa", "memchr", "ryu", @@ -3385,7 +3318,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -3447,9 +3380,12 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" @@ -3467,16 +3403,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "spin" version = "0.5.2" @@ -3520,9 +3446,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" dependencies = [ "proc-macro2", "quote", @@ -3537,28 +3463,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "syn 2.0.102", ] [[package]] @@ -3576,7 +3481,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -3621,7 +3526,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -3632,16 +3537,17 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", + "once_cell", ] [[package]] @@ -3726,20 +3632,18 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.5.10", + "socket2", "tokio-macros", "windows-sys 0.52.0", ] @@ -3752,7 +3656,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -3771,7 +3675,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.29", + "rustls 0.23.27", "tokio", ] @@ -3908,13 +3812,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -3987,7 +3891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -4063,7 +3967,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", - "rand 0.9.2", + "rand 0.9.1", "wasm-bindgen", ] @@ -4165,7 +4069,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", "wasm-bindgen-shared", ] @@ -4200,7 +4104,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4267,41 +4171,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4320,15 +4189,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4347,9 +4207,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -4522,28 +4382,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] [[package]] @@ -4563,7 +4423,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", "synstructure", ] @@ -4603,5 +4463,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.102", ] diff --git a/rust-runtime/aws-smithy-cbor/Cargo.toml b/rust-runtime/aws-smithy-cbor/Cargo.toml index 5f86a3d6e90..f60be2a228f 100644 --- a/rust-runtime/aws-smithy-cbor/Cargo.toml +++ b/rust-runtime/aws-smithy-cbor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-cbor" -version = "0.61.1" +version = "0.61.2" authors = [ "AWS Rust SDK Team ", "David Pérez ", diff --git a/rust-runtime/aws-smithy-cbor/src/encode.rs b/rust-runtime/aws-smithy-cbor/src/encode.rs index 1651c37f9b2..dc79f9a0520 100644 --- a/rust-runtime/aws-smithy-cbor/src/encode.rs +++ b/rust-runtime/aws-smithy-cbor/src/encode.rs @@ -114,4 +114,13 @@ impl Encoder { pub fn into_writer(self) -> Vec { self.encoder.into_writer() } + + /// Write pre-serialized CBOR bytes directly to the output stream + /// + /// The caller must ensure that the bytes are a valid segment of CBOR + pub fn write_preserialized_data(&mut self, bytes: &[u8]) -> &mut Self { + // Write the bytes directly to the output buffer + self.encoder.writer_mut().extend_from_slice(bytes); + self + } } diff --git a/rust-runtime/aws-smithy-cbor/tests/test_encoder.rs b/rust-runtime/aws-smithy-cbor/tests/test_encoder.rs new file mode 100644 index 00000000000..971d855bd1d --- /dev/null +++ b/rust-runtime/aws-smithy-cbor/tests/test_encoder.rs @@ -0,0 +1,149 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_cbor::Encoder; + +#[test] +fn test_write_preserialized_data() { + // Create a new encoder + let mut encoder = Encoder::new(Vec::new()); + + // Begin a map + encoder.begin_map(); + + // Add a regular field + encoder.str("regular_field"); + encoder.str("regular_value"); + + // Add a pre-serialized field + encoder.str("preserialized_field"); + + // Create pre-serialized CBOR data (a simple string "test") + // In CBOR, a string is encoded as: + // - Major type 3 (text string) in the first 3 bits + // - Length in the remaining 5 bits (or following bytes for longer strings) + // - The string data + // For "test", the length is 4, so we use 0x64 (0x60 + 4) followed by "test" + let preserialized_data = [0x64, b't', b'e', b's', b't']; + + // Write the pre-serialized data + encoder + .write_preserialized_data(&preserialized_data) + .unwrap(); + + // End the map + encoder.end(); + + // Get the final bytes + let bytes = encoder.into_writer(); + + // Since we can't easily decode and inspect the CBOR data without relying on the specific + // minicbor API, we'll verify the output bytes directly + + // Expected CBOR encoding: + // - 0xBF: Start indefinite-length map + // - 0x6C: Text string of length 12 + // - "regular_field": The key + // - 0x6D: Text string of length 13 + // - "regular_value": The value + // - 0x71: Text string of length 17 + // - "preserialized_field": The key + // - 0x64: Text string of length 4 + // - "test": The value + // - 0xFF: End of indefinite-length map + + // Check that the output contains our pre-serialized data + let preserialized_field_key = b"preserialized_field"; + let mut found_key = false; + let mut found_value = false; + + // Find the key in the output + for i in 0..bytes.len() - preserialized_field_key.len() { + if bytes[i..i + preserialized_field_key.len()] == preserialized_field_key[..] { + found_key = true; + break; + } + } + + // Find the pre-serialized value in the output + for i in 0..bytes.len() - preserialized_data.len() { + if bytes[i..i + preserialized_data.len()] == preserialized_data[..] { + found_value = true; + break; + } + } + + assert!(found_key, "Preserialized field key not found in output"); + assert!(found_value, "Preserialized data not found in output"); +} + +#[test] +fn test_write_preserialized_complex_data() { + // Create a new encoder + let mut encoder = Encoder::new(Vec::new()); + + // Begin a map + encoder.begin_map(); + + // Add a field that will contain a pre-serialized nested structure + encoder.str("nested_structure"); + + // Create pre-serialized CBOR data for a nested map with two fields + // In CBOR: + // - 0xA2 is a map with 2 pairs + // - 0x63 is a text string of length 3 + // - "foo" is the first key + // - 0x63 is a text string of length 3 + // - "bar" is the first value + // - 0x63 is a text string of length 3 + // - "baz" is the second key + // - 0x02 is the integer 2 (second value) + let preserialized_map = [ + 0xA2, // map with 2 pairs + 0x63, b'f', b'o', b'o', // key: "foo" + 0x63, b'b', b'a', b'r', // value: "bar" + 0x63, b'b', b'a', b'z', // key: "baz" + 0x02, // value: 2 + ]; + + // Write the pre-serialized map + encoder + .write_preserialized_data(&preserialized_map) + .unwrap(); + + // Add another regular field + encoder.str("regular_field"); + encoder.str("regular_value"); + + // End the map + encoder.end(); + + // Get the final bytes + let bytes = encoder.into_writer(); + + // Check that the output contains our pre-serialized data + let nested_structure_key = b"nested_structure"; + let mut found_key = false; + let mut found_value = false; + + // Find the key in the output + for i in 0..bytes.len() - nested_structure_key.len() { + if bytes[i..i + nested_structure_key.len()] == nested_structure_key[..] { + found_key = true; + break; + } + } + + // Find the pre-serialized value in the output + for i in 0..bytes.len() - preserialized_map.len() { + if bytes[i..i + preserialized_map.len()] == preserialized_map[..] { + found_value = true; + break; + } + } + + assert!(found_key, "Nested structure key not found in output"); + assert!(found_value, "Preserialized map not found in output"); +} diff --git a/rust-runtime/aws-smithy-cbor/tests/test_preserialized_data.rs b/rust-runtime/aws-smithy-cbor/tests/test_preserialized_data.rs new file mode 100644 index 00000000000..631b675bf9e --- /dev/null +++ b/rust-runtime/aws-smithy-cbor/tests/test_preserialized_data.rs @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_cbor::Encoder; + +// This test simulates a server response with a cacheable field +#[test] +fn test_preserialized_data_with_cached_response() { + // Simulate a server response with a cacheable field + + // First, create a "normal" response with all fields serialized normally + let mut normal_encoder = Encoder::new(Vec::new()); + normal_encoder.begin_map(); + + // Add a requestId field + normal_encoder.str("requestId"); + normal_encoder.str("12345"); + + // Add a userData field with a nested structure + normal_encoder.str("userData"); + + // Serialize the user data directly in the normal flow + serialize_user_data(&mut normal_encoder); + + // End the main response + normal_encoder.end(); + + // Get the serialized bytes + let normal_bytes = normal_encoder.into_writer(); + + // Now, simulate a cached response where userData is pre-serialized + + // First, serialize just the userData to a separate buffer (simulating cached data) + let mut user_data_encoder = Encoder::new(Vec::new()); + serialize_user_data(&mut user_data_encoder); + let user_data_bytes = user_data_encoder.into_writer(); + + // Create a new response with the cached userData + let mut cached_encoder = Encoder::new(Vec::new()); + cached_encoder.begin_map(); + + // Add the same requestId field + cached_encoder.str("requestId"); + cached_encoder.str("12345"); + + // Add the userData field with pre-serialized bytes + cached_encoder.str("userData"); + cached_encoder + .write_preserialized_data(&user_data_bytes) + .unwrap(); + + // End the main response + cached_encoder.end(); + + // Get the serialized bytes + let cached_bytes = cached_encoder.into_writer(); + + // Verify that both responses produce the same CBOR data + assert_eq!( + normal_bytes, cached_bytes, + "Cached response should match normal response" + ); +} + +// Helper function to serialize user data +// This represents the common serialization logic that would be used both for +// direct serialization and for generating cached data +fn serialize_user_data(encoder: &mut Encoder) { + // Create a nested user data structure + encoder.begin_map(); + encoder.str("name"); + encoder.str("John Doe"); + encoder.str("age"); + encoder.integer(30); + encoder.str("isActive"); + encoder.boolean(true); + encoder.end(); +} diff --git a/rust-runtime/inlineable/src/cacheable.rs b/rust-runtime/inlineable/src/cacheable.rs new file mode 100644 index 00000000000..9a3a42af9f3 --- /dev/null +++ b/rust-runtime/inlineable/src/cacheable.rs @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// Represents a value that can be either fully deserialized or cached in wire format +#[derive(Debug, Clone, Eq, PartialOrd, PartialEq, Hash)] +pub enum Cacheable { + /// The value has been deserialized into the target type + Modeled(T), + /// The value is stored as raw CBOR bytes from the wire + Cached(bytes::Bytes), +} + +impl Cacheable { + /// Create a new Cacheable with a modeled value + pub fn modeled(value: T) -> Self { + Cacheable::Modeled(value) + } + + /// Create a new Cacheable with cached bytes + pub fn cached(bytes: bytes::Bytes) -> Self { + Cacheable::Cached(bytes) + } + + /// Returns true if this is a Modeled variant + pub fn is_modeled(&self) -> bool { + matches!(self, Cacheable::Modeled(_)) + } + + /// Returns true if this is a Cached variant + pub fn is_cached(&self) -> bool { + matches!(self, Cacheable::Cached(_)) + } + + /// Get a reference to the modeled value if available + pub fn as_modeled(&self) -> Option<&T> { + match self { + Cacheable::Modeled(value) => Some(value), + _ => None, + } + } + + /// Get a mutable reference to the modeled value if available + pub fn as_modeled_mut(&mut self) -> Option<&mut T> { + match self { + Cacheable::Modeled(value) => Some(value), + _ => None, + } + } + + /// Get a reference to the cached bytes if available + pub fn as_cached(&self) -> Option<&bytes::Bytes> { + match self { + Cacheable::Cached(bytes) => Some(bytes), + _ => None, + } + } + + /// Convert into the modeled value if available + pub fn into_modeled(self) -> Option { + match self { + Cacheable::Modeled(value) => Some(value), + _ => None, + } + } + + /// Convert into the cached bytes if available + pub fn into_cached(self) -> Option { + match self { + Cacheable::Cached(bytes) => Some(bytes), + _ => None, + } + } +} + +/// Error type for validation errors +#[derive(Debug)] +pub enum ValidationError { + /// The data is invalid for the target shape + InvalidData { + /// The name of the shape that failed validation + shape: &'static str, + /// The underlying error that caused validation to fail + source: Box, + }, +} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidationError::InvalidData { shape, .. } => { + write!(f, "Invalid data for shape {}", shape) + } + } + } +} + +impl std::error::Error for ValidationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ValidationError::InvalidData { source, .. } => Some(source.as_ref()), + } + } +} + +/// Trait for types that can be wire-cached +pub trait WireCacheable: Sized { + /// Serialize this value to CBOR bytes for caching + fn to_bytes(&self) -> bytes::Bytes; + + /// Validate serialized CBOR bytes for this shape + fn validate(bytes: &[u8]) -> Result<(), ValidationError>; +} + +#[cfg(test)] +mod tests { + use super::{Cacheable, ValidationError, WireCacheable}; + use bytes::Bytes; + use std::error::Error; + + // A simple test struct to use with Cacheable + #[derive(Debug, Clone, PartialEq)] + struct TestData { + id: String, + value: i32, + } + + // Mock implementation of WireCacheable for TestData + impl WireCacheable for TestData { + fn to_bytes(&self) -> Bytes { + // Simple serialization for testing + let serialized = format!("{}:{}", self.id, self.value); + Bytes::from(serialized) + } + + fn validate(bytes: &[u8]) -> Result<(), ValidationError> { + // Simple validation for testing + match std::str::from_utf8(bytes) { + Ok(s) => { + if s.contains(':') { + Ok(()) + } else { + Err(ValidationError::InvalidData { + shape: "TestData", + source: Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid format: missing colon", + )), + }) + } + } + Err(e) => Err(ValidationError::InvalidData { + shape: "TestData", + source: Box::new(e), + }), + } + } + } + + #[test] + fn test_cacheable_modeled_constructor() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let cacheable = Cacheable::modeled(test_data.clone()); + + assert!(cacheable.is_modeled()); + assert!(!cacheable.is_cached()); + assert_eq!(cacheable.as_modeled().unwrap().id, "test"); + assert_eq!(cacheable.as_modeled().unwrap().value, 42); + } + + #[test] + fn test_cacheable_cached_constructor() { + let bytes = Bytes::from_static(b"test:42"); + let cacheable: Cacheable = Cacheable::cached(bytes.clone()); + + assert!(!cacheable.is_modeled()); + assert!(cacheable.is_cached()); + assert_eq!(cacheable.as_cached().unwrap(), &bytes); + } + + #[test] + fn test_cacheable_as_modeled_mut() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let mut cacheable = Cacheable::modeled(test_data); + + if let Some(modeled) = cacheable.as_modeled_mut() { + modeled.id = "modified".to_string(); + modeled.value = 100; + } + + assert_eq!(cacheable.as_modeled().unwrap().id, "modified"); + assert_eq!(cacheable.as_modeled().unwrap().value, 100); + } + + #[test] + fn test_cacheable_into_modeled() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let cacheable = Cacheable::modeled(test_data); + let extracted = cacheable.into_modeled().unwrap(); + + assert_eq!(extracted.id, "test"); + assert_eq!(extracted.value, 42); + } + + #[test] + fn test_cacheable_into_cached() { + let bytes = Bytes::from_static(b"test:42"); + let cacheable: Cacheable = Cacheable::cached(bytes.clone()); + let extracted = cacheable.into_cached().unwrap(); + + assert_eq!(extracted, bytes); + } + + #[test] + fn test_validation_error_display() { + let error = ValidationError::InvalidData { + shape: "TestShape", + source: Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "test error", + )), + }; + + assert_eq!(error.to_string(), "Invalid data for shape TestShape"); + } + + #[test] + fn test_validation_error_source() { + let inner_error = std::io::Error::new(std::io::ErrorKind::InvalidData, "test error"); + let error = ValidationError::InvalidData { + shape: "TestShape", + source: Box::new(inner_error), + }; + + let source = error.source().unwrap(); + let io_error = source.downcast_ref::().unwrap(); + assert_eq!(io_error.kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn test_wire_cacheable_implementation() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + // Test to_bytes + let bytes = test_data.to_bytes(); + assert_eq!(bytes, Bytes::from("test:42")); + + // Test validate with valid data + let result = TestData::validate(b"valid:123"); + assert!(result.is_ok()); + + // Test validate with invalid data + let result = TestData::validate(b"invalid_no_colon"); + assert!(result.is_err()); + if let Err(ValidationError::InvalidData { shape, .. }) = result { + assert_eq!(shape, "TestData"); + } else { + panic!("Expected ValidationError::InvalidData"); + } + } +} diff --git a/rust-runtime/inlineable/src/cacheable_test.rs b/rust-runtime/inlineable/src/cacheable_test.rs new file mode 100644 index 00000000000..a816cffe9bb --- /dev/null +++ b/rust-runtime/inlineable/src/cacheable_test.rs @@ -0,0 +1,167 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#[cfg(test)] +mod tests { + use super::super::cacheable::{Cacheable, ValidationError, WireCacheable}; + use bytes::Bytes; + use std::error::Error; + + // A simple test struct to use with Cacheable + #[derive(Debug, Clone, PartialEq)] + struct TestData { + id: String, + value: i32, + } + + // Mock implementation of WireCacheable for TestData + impl WireCacheable for TestData { + fn to_bytes(&self) -> Bytes { + // Simple serialization for testing + let serialized = format!("{}:{}", self.id, self.value); + Bytes::from(serialized) + } + + fn validate(bytes: &[u8]) -> Result<(), ValidationError> { + // Simple validation for testing + match std::str::from_utf8(bytes) { + Ok(s) => { + if s.contains(':') { + Ok(()) + } else { + Err(ValidationError::InvalidData { + shape: "TestData", + source: Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid format: missing colon", + )), + }) + } + } + Err(e) => Err(ValidationError::InvalidData { + shape: "TestData", + source: Box::new(e), + }), + } + } + } + + #[test] + fn test_cacheable_modeled_constructor() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let cacheable = Cacheable::modeled(test_data.clone()); + + assert!(cacheable.is_modeled()); + assert!(!cacheable.is_cached()); + assert_eq!(cacheable.as_modeled().unwrap().id, "test"); + assert_eq!(cacheable.as_modeled().unwrap().value, 42); + } + + #[test] + fn test_cacheable_cached_constructor() { + let bytes = Bytes::from_static(b"test:42"); + let cacheable: Cacheable = Cacheable::cached(bytes.clone()); + + assert!(!cacheable.is_modeled()); + assert!(cacheable.is_cached()); + assert_eq!(cacheable.as_cached().unwrap(), &bytes); + } + + #[test] + fn test_cacheable_as_modeled_mut() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let mut cacheable = Cacheable::modeled(test_data); + + if let Some(modeled) = cacheable.as_modeled_mut() { + modeled.id = "modified".to_string(); + modeled.value = 100; + } + + assert_eq!(cacheable.as_modeled().unwrap().id, "modified"); + assert_eq!(cacheable.as_modeled().unwrap().value, 100); + } + + #[test] + fn test_cacheable_into_modeled() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + let cacheable = Cacheable::modeled(test_data); + let extracted = cacheable.into_modeled().unwrap(); + + assert_eq!(extracted.id, "test"); + assert_eq!(extracted.value, 42); + } + + #[test] + fn test_cacheable_into_cached() { + let bytes = Bytes::from_static(b"test:42"); + let cacheable: Cacheable = Cacheable::cached(bytes.clone()); + let extracted = cacheable.into_cached().unwrap(); + + assert_eq!(extracted, bytes); + } + + #[test] + fn test_validation_error_display() { + let error = ValidationError::InvalidData { + shape: "TestShape", + source: Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "test error", + )), + }; + + assert_eq!(error.to_string(), "Invalid data for shape TestShape"); + } + + #[test] + fn test_validation_error_source() { + let inner_error = std::io::Error::new(std::io::ErrorKind::InvalidData, "test error"); + let error = ValidationError::InvalidData { + shape: "TestShape", + source: Box::new(inner_error), + }; + + let source = ::source(&error).unwrap(); + let io_error = source.downcast_ref::().unwrap(); + assert_eq!(io_error.kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn test_wire_cacheable_implementation() { + let test_data = TestData { + id: "test".to_string(), + value: 42, + }; + + // Test to_bytes + let bytes = test_data.to_bytes(); + assert_eq!(bytes, Bytes::from("test:42")); + + // Test validate with valid data + let result = TestData::validate(b"valid:123"); + assert!(result.is_ok()); + + // Test validate with invalid data + let result = TestData::validate(b"invalid_no_colon"); + assert!(result.is_err()); + if let Err(ValidationError::InvalidData { shape, .. }) = result { + assert_eq!(shape, "TestData"); + } else { + panic!("Expected ValidationError::InvalidData"); + } + } +} diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs index bf20f6c0917..9b7fa4c9420 100644 --- a/rust-runtime/inlineable/src/lib.rs +++ b/rust-runtime/inlineable/src/lib.rs @@ -9,6 +9,8 @@ #[allow(dead_code)] mod aws_query_compatible_errors; #[allow(unused)] +mod cacheable; +#[allow(unused)] mod cbor_errors; #[allow(unused)] mod client_http_checksum_required; diff --git a/settings.gradle.kts b/settings.gradle.kts index bf89c0cfc7a..2628ee57e62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ include(":codegen-server:typescript") include(":codegen-server-test") include(":codegen-server-test:python") include(":codegen-server-test:typescript") +include(":codegen-traits") include(":rust-runtime") include(":aws:rust-runtime") include(":aws:sdk")