diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a3f798992649009e89c7dc09c2cd386aeff873..8ecc4bd545246a0c1521f44509b6ddbf694b8125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v0.27.0-alpha.1 (November 3rd, 2021) - `moduleDescription` in `smithy-build.json` settings is now optional - Upgrade to Smithy 1.12 - `hyper::Error(IncompleteMessage)` will now be retried (smithy-rs#815) +- Unions will optionally generate an `Unknown` variant to support parsing variants that don't exist on the client. These variants will fail to serialize if they are ever included in requests. - Fix generated docs on unions. (smithy-rs#826) v0.27 (October 20th, 2021) diff --git a/aws/SDK_CHANGELOG.md b/aws/SDK_CHANGELOG.md index f5c5cc9c5e3afab7c4cf43642232e338f50be311..f6b867064ed77b8a9e3d9a6ad559ab1ab8d8992e 100644 --- a/aws/SDK_CHANGELOG.md +++ b/aws/SDK_CHANGELOG.md @@ -9,11 +9,12 @@ v0.0.23-alpha (November 3rd, 2021) - :bug: Fix `native-tls` feature in `aws-config` (aws-sdk-rust#265, smithy-rs#803) - Add example to aws-sig-auth for generating an IAM Token for RDS (smithy-rs#811, aws-sdk-rust#147) - :bug: `hyper::Error(IncompleteMessage)` will now be retried (smithy-rs#815) +- :bug: S3 request metadata signing now correctly trims headers fixing [problems like this](https://github.com/awslabs/aws-sdk-rust/issues/248) (smithy-rs#761) +- All unions (eg. `dynamodb::model::AttributeValue`) now include an additional `Unknown` variant. These support cases where a new union variant has been added on the server but the client has not been updated. - :bug: Fix generated docs on unions like `dynamodb::AttributeValue`. (smithy-rs#826) **Breaking Changes** - `.make_operation(&config)` is now an `async` function for all operations. Code should be updated to call `.await`. This will only impact users using the low-level API. (smithy-rs#797) -- :bug: S3 request metadata signing now correctly trims headers fixing [problems like this](https://github.com/awslabs/aws-sdk-rust/issues/248) (smithy-rs#761) v0.0.22-alpha (October 20th, 2021) ================================== diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointConfigCustomizationTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointConfigCustomizationTest.kt index 01285e9fa03e1954a75462405b27ad12e3dd7849..e6c3b5be5a0e88018940d8196887d1c3ac63685c 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointConfigCustomizationTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointConfigCustomizationTest.kt @@ -124,6 +124,7 @@ internal class EndpointConfigCustomizationTest { it.addDependency(awsTypes(AwsTestRuntimeConfig)) it.addDependency(CargoDependency.Http) it.unitTest( + "region_override", """ use aws_types::region::Region; use http::Uri; @@ -147,6 +148,7 @@ internal class EndpointConfigCustomizationTest { it.addDependency(awsTypes(AwsTestRuntimeConfig)) it.addDependency(CargoDependency.Http) it.unitTest( + "global_services", """ use aws_types::region::Region; use http::Uri; diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt index a5f75f9cc6486d42f5b1ff1d1182e26397d32d0d..fef0fb337597faad6b248037ba2e415c5df7cc88 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/SigV4SigningCustomizationTest.kt @@ -25,6 +25,7 @@ internal class SigV4SigningCustomizationTest { ) project.lib { it.unitTest( + "signing_service_override", """ let conf = crate::config::Config::builder().build(); assert_eq!(conf.signing_service(), "test-service"); diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt index 8d6a6d24b020e0ef918e5f0ebde2ce8379235801..6e88803980c1f548b4db097d191d835a66c1f9ec 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt @@ -71,6 +71,6 @@ class RustCodegenServerPlugin : SmithyBuildPlugin { .let { StreamingShapeMetadataProvider(it, model) } // Rename shapes that clash with Rust reserved words & and other SDK specific features e.g. `send()` cannot // be the name of an operation input - .let { RustReservedWordSymbolProvider(it) } + .let { RustReservedWordSymbolProvider(it, model) } } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index e7454270401101baf0686eac4a76240376023733..ec6baf05ba0ddf4a39c8f3bec56dd56a73163fb8 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerServiceGenerator import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader import software.amazon.smithy.rust.codegen.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.DefaultPublicModules import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.RustSettings @@ -87,7 +88,7 @@ class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator: symbolProvider = codegenDecorator.symbolProvider(generator.symbolProvider(model, baseProvider)) - codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings) + codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings, mode = CodegenMode.Server) rustCrate = RustCrate(context.fileManifest, symbolProvider, DefaultPublicModules) protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext) @@ -208,7 +209,7 @@ class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator: override fun unionShape(shape: UnionShape) { logger.info("[rust-server-codegen] Generating an union $shape") rustCrate.useShapeWriter(shape) { - UnionGenerator(model, symbolProvider, it, shape).render() + UnionGenerator(model, symbolProvider, it, shape, renderUnknownVariant = false).render() } } @@ -260,6 +261,12 @@ class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator: } } + impl From for Error { + fn from(err: aws_smithy_http::operation::SerializationError) -> Self { + Self::BuildInput(err.into()) + } + } + impl From for Error { fn from(err: aws_smithy_http::header::ParseError) -> Self { Self::DeserializeHeader(err) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWords.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWords.kt index 06d06d0ce5152b7b46ea2342aa55f469f064a75a..1e0a74d5aafdb8b8e49efa3a64f8d6ab8d7cb046 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWords.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWords.kt @@ -8,33 +8,77 @@ package software.amazon.smithy.rust.codegen.rustlang import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider import software.amazon.smithy.codegen.core.ReservedWords import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumDefinition import software.amazon.smithy.rust.codegen.smithy.MaybeRenamed import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.WrappingSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.letIf +import software.amazon.smithy.rust.codegen.smithy.renamedFrom import software.amazon.smithy.rust.codegen.util.orNull import software.amazon.smithy.rust.codegen.util.toPascalCase -class RustReservedWordSymbolProvider(private val base: RustSymbolProvider) : WrappingSymbolProvider(base) { +class RustReservedWordSymbolProvider(private val base: RustSymbolProvider, private val model: Model) : + WrappingSymbolProvider(base) { private val internal = ReservedWordSymbolProvider.builder().symbolProvider(base).memberReservedWords(RustReservedWords).build() override fun toMemberName(shape: MemberShape): String { - return when (val baseName = internal.toMemberName(shape)) { - "build" -> "build_value" - "default" -> "default_value" - "send" -> "send_value" - // To avoid conflicts with the `make_operation` and `presigned` functions on generated inputs - "make_operation" -> "make_operation_value" - "presigned" -> "presigned_value" - else -> baseName + val baseName = internal.toMemberName(shape) + return when (val container = model.expectShape(shape.container)) { + is StructureShape -> when (baseName) { + "build" -> "build_value" + "default" -> "default_value" + "send" -> "send_value" + // To avoid conflicts with the `make_operation` and `presigned` functions on generated inputs + "make_operation" -> "make_operation_value" + "presigned" -> "presigned_value" + else -> baseName + } + is UnionShape -> when (baseName) { + // Unions contain an `Unknown` variant. This exists to support parsing data returned from the server + // that represent union variants that have been added since this SDK was generated. + UnionGenerator.UnknownVariantName -> "${UnionGenerator.UnknownVariantName}Value" + "${UnionGenerator.UnknownVariantName}Value" -> "${UnionGenerator.UnknownVariantName}Value_" + // Self cannot be used as a raw identifier, so we can't use the normal escaping strategy + // https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094/4 + "Self" -> "SelfValue" + // Real models won't end in `_` so it's safe to stop here + "SelfValue" -> "SelfValue_" + else -> baseName + } + else -> error("unexpected container: $container") } } + /** + * Convert shape to a Symbol + * + * If this symbol provider renamed the symbol, a `renamedFrom` field will be set on the symbol, enabling + * code generators to generate special docs. + */ override fun toSymbol(shape: Shape): Symbol { - return internal.toSymbol(shape) + return when (shape) { + is MemberShape -> { + val container = model.expectShape(shape.container) + if (!(container is StructureShape || container is UnionShape)) { + return base.toSymbol(shape) + } + val previousName = base.toMemberName(shape) + val escapedName = this.toMemberName(shape) + val baseSymbol = base.toSymbol(shape) + // if the names don't match and it isn't a simple escaping with `r#`, record a rename + baseSymbol.letIf(escapedName != previousName && !escapedName.contains("r#")) { + it.toBuilder().renamedFrom(previousName).build() + } + } + else -> base.toSymbol(shape) + } } override fun toEnumVariantName(definition: EnumDefinition): MaybeRenamed? { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt index 97d727fe7f853003cd1c6fa75214ab30e87fccb2..7e256583906ad3ce1a30bf6bcbcd7f51f6e64d59 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt @@ -230,7 +230,6 @@ sealed class Attribute { * indicates that more fields may be added in the future */ val NonExhaustive = Custom("non_exhaustive") - val AllowUnused = Custom("allow(dead_code)") val AllowUnusedMut = Custom("allow(unused_mut)") } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt index 802212b4ec5c4e11bab7b59c29bafb1f3293813f..9b5114e496ae60ef8b1ad3a4613d89d4a8426432 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt @@ -178,13 +178,20 @@ fun T.rustBlock( /** * Generate a RustDoc comment for [shape] */ -fun T.documentShape(shape: Shape, model: Model, autoSuppressMissingDocs: Boolean = true): T { +fun T.documentShape(shape: Shape, model: Model, autoSuppressMissingDocs: Boolean = true, note: String? = null): T { // TODO: support additional Smithy documentation traits like @example val docTrait = shape.getMemberTrait(model, DocumentationTrait::class.java).orNull() when (docTrait?.value?.isNotBlank()) { // If docs are modeled, then place them on the code generated shape - true -> this.docs(escape(docTrait.value)) + true -> { + this.docs(escape(docTrait.value)) + note?.also { + // Add a blank line between the docs and the note to visually differentiate + write("///") + docs("_Note: ${it}_") + } + } // Otherwise, suppress the missing docs lint for this shape since // the lack of documentation is a modeling issue rather than a codegen issue. else -> if (autoSuppressMissingDocs) { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenContext.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenContext.kt index 88036a6b52399b88ad3c3853f619e007c83ac0a8..3a3293f0fa425ee759c1f931f428ff3c5753878a 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenContext.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenContext.kt @@ -9,6 +9,11 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId +sealed class CodegenMode { + object Server : CodegenMode() + object Client : CodegenMode() +} + /** * Configuration needed to generate the client for a given Service<->Protocol pair */ @@ -44,6 +49,12 @@ data class CodegenContext( * Settings loaded from smithy-build.json */ val settings: RustSettings, + /** + * Server vs. Client codegen + * + * Some settings are dependent on whether server vs. client codegen is being invoked. + */ + val mode: CodegenMode, ) { constructor( model: Model, @@ -51,5 +62,6 @@ data class CodegenContext( serviceShape: ServiceShape, protocol: ShapeId, settings: RustSettings, - ) : this(model, symbolProvider, settings.runtimeConfig, serviceShape, protocol, settings.moduleName, settings) + mode: CodegenMode, + ) : this(model, symbolProvider, settings.runtimeConfig, serviceShape, protocol, settings.moduleName, settings, mode) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt index b498e8b7bf94c47da1f65883059e11287122b3cd..5ab6faca0da13d29263f3dc4b5002f2a17a93405 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt @@ -68,7 +68,7 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC val baseProvider = RustCodegenPlugin.baseSymbolProvider(model, service, symbolVisitorConfig) symbolProvider = codegenDecorator.symbolProvider(generator.symbolProvider(model, baseProvider)) - codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings) + codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings, mode = CodegenMode.Client) rustCrate = RustCrate( context.fileManifest, symbolProvider, @@ -200,7 +200,7 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC */ override fun unionShape(shape: UnionShape) { rustCrate.useShapeWriter(shape) { - UnionGenerator(model, symbolProvider, it, shape).render() + UnionGenerator(model, symbolProvider, it, shape, renderUnknownVariant = true).render() } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt index b058ce673eccc37585ba500ec3fca652111878a1..5755932d3f2166e988f18985cd14e774010acbc7 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt @@ -56,6 +56,6 @@ class RustCodegenPlugin : SmithyBuildPlugin { .let { StreamingShapeMetadataProvider(it, model) } // Rename shapes that clash with Rust reserved words & and other SDK specific features e.g. `send()` cannot // be the name of an operation input - .let { RustReservedWordSymbolProvider(it) } + .let { RustReservedWordSymbolProvider(it, model) } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt index 15a0519fd60c0cba3f774cad1ad395dbcb38b999..b7a31e5a36834afb3e47cd454919b9b4dc20c09d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt @@ -153,7 +153,11 @@ class SymbolVisitor( return MaybeRenamed(baseName, null) } - override fun toMemberName(shape: MemberShape): String = shape.memberName.toSnakeCase() + override fun toMemberName(shape: MemberShape): String = when (val container = model.expectShape(shape.container)) { + is StructureShape -> shape.memberName.toSnakeCase() + is UnionShape -> shape.memberName.toPascalCase() + else -> error("unexpected container shape: $container") + } override fun blobShape(shape: BlobShape?): Symbol { return RuntimeType.Blob(config.runtimeConfig).toSymbol() @@ -299,11 +303,18 @@ class SymbolVisitor( private const val RUST_TYPE_KEY = "rusttype" private const val SHAPE_KEY = "shape" private const val SYMBOL_DEFAULT = "symboldefault" +private const val RENAMED_FROM_KEY = "renamedfrom" fun Symbol.Builder.rustType(rustType: RustType): Symbol.Builder { return this.putProperty(RUST_TYPE_KEY, rustType) } +fun Symbol.Builder.renamedFrom(name: String): Symbol.Builder { + return this.putProperty(RENAMED_FROM_KEY, name) +} + +fun Symbol.renamedFrom(): String? = this.getProperty(RENAMED_FROM_KEY, String::class.java).orNull() + fun Symbol.defaultValue(): Default = this.getProperty(SYMBOL_DEFAULT, Default::class.java).orElse(Default.NoDefault) fun Symbol.Builder.setDefault(default: Default): Symbol.Builder { return this.putProperty(SYMBOL_DEFAULT, default) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/EndpointPrefixGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/EndpointPrefixGenerator.kt index 0df9ff4b8bf652a4b6ac59adbcee797e6eb4e363..a8dbfbb900dc3358f6d75c9df8cd9e4308d3d5d2 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/EndpointPrefixGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/EndpointPrefixGenerator.kt @@ -9,14 +9,12 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.EndpointTrait import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.rust -import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.rustlang.writable import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.customize.OperationCustomization import software.amazon.smithy.rust.codegen.smithy.customize.OperationSection import software.amazon.smithy.rust.codegen.smithy.generators.EndpointTraitBindings -import software.amazon.smithy.rust.codegen.smithy.generators.OperationBuildError class EndpointPrefixGenerator(private val codegenContext: CodegenContext, private val shape: OperationShape) : OperationCustomization() { @@ -30,14 +28,10 @@ class EndpointPrefixGenerator(private val codegenContext: CodegenContext, privat shape, epTrait ) - val buildError = OperationBuildError(codegenContext.runtimeConfig) - withBlock("let endpoint_prefix = ", ";") { + withBlock("let endpoint_prefix = ", "?;") { endpointTraitBindings.render(this, "self") } - rustBlock("match endpoint_prefix") { - rust("Ok(prefix) => { request.properties_mut().insert(prefix); },") - rust("Err(err) => return Err(${buildError.serializationError(this, "err")})") - } + rust("request.properties_mut().insert(endpoint_prefix);") } } else -> emptySection diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/HttpChecksumRequiredGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/HttpChecksumRequiredGenerator.kt index d0c666d4e69cfc41763c00f2e1db538fc069418a..9b92064bdf92175bea6d68566d44ad873595b3fe 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/HttpChecksumRequiredGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/HttpChecksumRequiredGenerator.kt @@ -41,9 +41,7 @@ class HttpChecksumRequiredGenerator( let data = req .body() .bytes() - .ok_or_else(||#{BuildError}::SerializationError( - "checksum can only be computed for non-streaming operations".into()) - )?; + .expect("checksum can only be computed for non-streaming operations"); let checksum = #{md5}::compute(data); req.headers_mut().insert( #{http}::header::HeaderName::from_static("content-md5"), diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt index 0ca899a62bdf7aa6c3cf0aa7915cea8b803fe852..3118e8517031db5a499968aef2bb8fda9e942065 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/BuilderGenerator.kt @@ -41,6 +41,7 @@ fun StructureShape.builderSymbol(symbolProvider: RustSymbolProvider): RuntimeTyp } fun RuntimeConfig.operationBuildError() = RuntimeType.operationModule(this).member("BuildError") +fun RuntimeConfig.serializationError() = RuntimeType.operationModule(this).member("SerializationError") class OperationBuildError(private val runtimeConfig: RuntimeConfig) { fun missingField(w: RustWriter, field: String, details: String) = "${w.format(runtimeConfig.operationBuildError())}::MissingField { field: ${field.dq()}, details: ${details.dq()} }" diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt index 541732e8f0e6a96212f3483a6e4c6aa165df4fb2..cfbd2e03b157ecce0396079ab9417ea924b4f59e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt @@ -49,7 +49,6 @@ import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectMember import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.isStreaming -import software.amazon.smithy.rust.codegen.util.toPascalCase /** * Instantiator generates code to instantiate a given Shape given a `Node` representing the value @@ -223,8 +222,7 @@ class Instantiator( val variant = data.members.iterator().next() val memberName = variant.key.value val member = shape.expectMember(memberName) - // TODO: refactor this detail into UnionGenerator - writer.write("#T::${memberName.toPascalCase()}", unionSymbol) + writer.write("#T::${symbolProvider.toMemberName(member)}", unionSymbol) // unions should specify exactly one member writer.withBlock("(", ")") { renderMember(this, member, variant.value, ctx) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt index 334940f6cbbb0f270bc3be1207ed656d40576101..2a749f7556cf746a97b7a37607e9410da7cf6815 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt @@ -23,6 +23,7 @@ import software.amazon.smithy.rust.codegen.smithy.canUseDefault import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata import software.amazon.smithy.rust.codegen.smithy.generators.error.ErrorGenerator import software.amazon.smithy.rust.codegen.smithy.isOptional +import software.amazon.smithy.rust.codegen.smithy.renamedFrom import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.util.dq @@ -100,8 +101,11 @@ class StructureGenerator( rust("""let mut formatter = f.debug_struct(${name.dq()});""") members.forEach { member -> val memberName = symbolProvider.toMemberName(member) + val fieldValue = redactIfNecessary( + member, model, "self.$memberName" + ) rust( - "formatter.field(${memberName.dq()}, &${redactIfNecessary(member, model, "self.$memberName")});", + "formatter.field(${memberName.dq()}, &$fieldValue);", ) } rust("formatter.finish()") @@ -119,8 +123,14 @@ class StructureGenerator( writer.rustBlock("struct $name ${lifetimeDeclaration()}") { members.forEach { member -> val memberName = symbolProvider.toMemberName(member) - writer.documentShape(member, model) - symbolProvider.toSymbol(member).expectRustMetadata().render(this) + val memberSymbol = symbolProvider.toSymbol(member) + writer.documentShape( + member, + model, + note = memberSymbol.renamedFrom() + ?.let { oldName -> "This member has been renamed from `$oldName`." } + ) + memberSymbol.expectRustMetadata().render(this) write("$memberName: #T,", symbolProvider.toSymbol(member)) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt index 7f5f202755a25129f487dc38976cc04ca7bc37ac..a95776d37d40ca55d427fed3d9193e80f8b6f28f 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/UnionGenerator.kt @@ -11,18 +11,39 @@ import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.rust.codegen.rustlang.Attribute import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.docs import software.amazon.smithy.rust.codegen.rustlang.documentShape import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata -import software.amazon.smithy.rust.codegen.util.toPascalCase +import software.amazon.smithy.rust.codegen.smithy.renamedFrom import software.amazon.smithy.rust.codegen.util.toSnakeCase +fun CodegenMode.renderUnknownVariant() = when (this) { + is CodegenMode.Server -> false + is CodegenMode.Client -> true +} + +/** + * Generate an `enum` for a Smithy Union Shape + * + * This generator will render a Rust enum representing [shape] when [render] is called. It will also render convenience + * methods: + * - `is_()` + * - `as_()` + * + * for each variant. + * + * Finally, if `[renderUnknownVariant]` is true (the default), it will render an `Unknown` variant. This is used by + * clients to allow response parsing to succeed, even if the server has added a new variant since the client was generated. + */ class UnionGenerator( val model: Model, private val symbolProvider: SymbolProvider, private val writer: RustWriter, - private val shape: UnionShape + private val shape: UnionShape, + private val renderUnknownVariant: Boolean = true, ) { private val sortedMembers: List = shape.allMembers.values.sortedBy { symbolProvider.toMemberName(it) } @@ -39,16 +60,30 @@ class UnionGenerator( writer.rustBlock("enum ${unionSymbol.name}") { sortedMembers.forEach { member -> val memberSymbol = symbolProvider.toSymbol(member) - documentShape(member, model) + val note = + memberSymbol.renamedFrom()?.let { oldName -> "This variant has been renamed from `$oldName`." } + documentShape(member, model, note = note) memberSymbol.expectRustMetadata().renderAttributes(this) - write("${member.memberName.toPascalCase()}(#T),", symbolProvider.toSymbol(member)) + write("${symbolProvider.toMemberName(member)}(#T),", symbolProvider.toSymbol(member)) + } + if (renderUnknownVariant) { + docs("""The `Unknown` variant represents cases where new union variant was received. Consider upgrading the SDK to the latest available version.""") + rust("/// An unknown enum variant") + rust("///") + rust("/// _Note: If you encounter this error, consider upgrading your SDK to the latest version._") + rust("/// The `Unknown` variant represents cases where the server sent a value that wasn't recognized") + rust("/// by the client. This can happen when the server adds new functionality, but the client has not been updated.") + rust("/// To investigate this, consider turning on debug logging to print the raw HTTP response.") + // at some point in the future, we may start actually putting things like the raw data in here. + Attribute.NonExhaustive.render(this) + rust("Unknown,") } } writer.rustBlock("impl ${unionSymbol.name}") { sortedMembers.forEach { member -> val memberSymbol = symbolProvider.toSymbol(member) - val variantName = member.memberName.toPascalCase() val funcNamePart = member.memberName.toSnakeCase() + val variantName = symbolProvider.toMemberName(member) if (sortedMembers.size == 1) { Attribute.Custom("allow(irrefutable_let_patterns)").render(this) @@ -63,6 +98,20 @@ class UnionGenerator( rust("self.as_$funcNamePart().is_ok()") } } + if (renderUnknownVariant) { + rust("/// Returns true if the enum instance is the `Unknown` variant.") + rustBlock("pub fn is_unknown(&self) -> bool") { + rust("matches!(self, Self::Unknown)") + } + } } } + + companion object { + const val UnknownVariantName = "Unknown" + } } +fun unknownVariantError(union: String) = + "Cannot serialize `$union::${UnionGenerator.UnknownVariantName}` for the request. " + + "The `Unknown` variant is intended for responses only. " + + "It occurs when an outdated client is used after a new enum variant was added on the server side." diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt index 1d3358520f91549d50dbf49358d8a88a336bba60..135743ca23c891dd6d854a2ee906a6295811c97d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt @@ -52,6 +52,7 @@ class ResponseBindingGenerator( ) { private val runtimeConfig = codegenContext.runtimeConfig private val symbolProvider = codegenContext.symbolProvider + private val mode = codegenContext.mode private val model = codegenContext.model private val service = codegenContext.serviceShape private val index = HttpBindingIndex.of(model) @@ -179,7 +180,8 @@ class ResponseBindingGenerator( runtimeConfig, symbolProvider, operationShape, - target + target, + mode ).render() rustTemplate( """ diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt index f255db693e0ed54d407c081ae7b9684df960d628..2d637f8ceb254a4900763adf92832e8fd73977ac 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt @@ -19,6 +19,7 @@ import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport +import software.amazon.smithy.rust.codegen.smithy.generators.serializationError import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator @@ -106,7 +107,7 @@ class AwsJsonSerializerGenerator( ) : StructuredDataSerializerGenerator by jsonSerializerGenerator { private val runtimeConfig = codegenContext.runtimeConfig private val codegenScope = arrayOf( - "Error" to CargoDependency.SmithyTypes(runtimeConfig).asType().member("Error"), + "Error" to runtimeConfig.serializationError(), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), ) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt index 6b11fdba209f98c7a7803d1565b97d92ff06c4fd..fccfa99e09f307811047a5b44ac9cd4aa04e9dc5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpBoundProtocolGenerator.kt @@ -413,6 +413,7 @@ class HttpBoundProtocolBodyGenerator( private val symbolProvider = codegenContext.symbolProvider private val model = codegenContext.model private val runtimeConfig = codegenContext.runtimeConfig + private val mode = codegenContext.mode private val httpBindingResolver = protocol.httpBindingResolver private val operationSerModule = RustModule.private("operation_ser") @@ -458,9 +459,8 @@ class HttpBoundProtocolBodyGenerator( if (payloadMemberName == null) { serializerGenerator.operationSerializer(operationShape)?.let { serializer -> writer.rust( - "#T(&self).map_err(|err|#T::SerializationError(err.into()))?", + "#T(&self)?", serializer, - runtimeConfig.operationBuildError() ) } ?: writer.rustTemplate("#{SdkBody}::from(\"\")", *codegenScope) } else { @@ -483,11 +483,13 @@ class HttpBoundProtocolBodyGenerator( val marshallerConstructorFn = EventStreamMarshallerGenerator( model, + mode, runtimeConfig, symbolProvider, unionShape, serializerGenerator, - httpBindingResolver.requestContentType(operationShape) ?: throw CodegenException("event streams must set a content type"), + httpBindingResolver.requestContentType(operationShape) + ?: throw CodegenException("event streams must set a content type"), ).render() // TODO(EventStream): [RPC] RPC protocols need to send an initial message with the @@ -585,15 +587,14 @@ class HttpBoundProtocolBodyGenerator( // JSON serialize the structure or union targeted rust( - """#T(&$payloadName).map_err(|err|#T::SerializationError(err.into()))?""", - serializer.payloadSerializer(member), runtimeConfig.operationBuildError() + "#T(&$payloadName)?", + serializer.payloadSerializer(member) ) } is DocumentShape -> { rust( - "#T(&$payloadName).map_err(|err|#T::SerializationError(err.into()))?", + "#T(&$payloadName)?", serializer.documentSerializer(), - runtimeConfig.operationBuildError() ) } else -> TODO("Unexpected payload target type") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt index e95eb22063363d6e13233164a86300e179ab984b..694ecebea0a3508f06f657634abeb982a0b8abf6 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGenerator.kt @@ -31,10 +31,13 @@ import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.withBlock +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticEventStreamUnionTrait import software.amazon.smithy.rust.codegen.util.dq @@ -49,6 +52,7 @@ class EventStreamUnmarshallerGenerator( private val symbolProvider: RustSymbolProvider, private val operationShape: OperationShape, private val unionShape: UnionShape, + private val mode: CodegenMode, ) { private val unionSymbol = symbolProvider.toSymbol(unionShape) private val operationErrorSymbol = operationShape.errorSymbol(symbolProvider) @@ -139,18 +143,24 @@ class EventStreamUnmarshallerGenerator( renderUnmarshallUnionMember(member, target) } } - rustBlock("smithy_type => ") { - // TODO: Handle this better once unions support unknown variants - rustTemplate( - "return Err(#{Error}::Unmarshalling(format!(\"unrecognized :event-type: {}\", smithy_type)));", - *codegenScope - ) + rustBlock("_unknown_variant => ") { + when (mode.renderUnknownVariant()) { + true -> rustTemplate( + "Ok(#{UnmarshalledMessage}::Event(#{Output}::${UnionGenerator.UnknownVariantName}))", + "Output" to unionSymbol, + *codegenScope + ) + false -> rustTemplate( + "return Err(#{Error}::Unmarshalling(format!(\"unrecognized :event-type: {}\", _unknown_variant)));", + *codegenScope + ) + } } } } private fun RustWriter.renderUnmarshallUnionMember(unionMember: MemberShape, unionStruct: StructureShape) { - val unionMemberName = unionMember.memberName.toPascalCase() + val unionMemberName = symbolProvider.toMemberName(unionMember) val empty = unionStruct.members().isEmpty() val payloadOnly = unionStruct.members().none { it.hasTrait() || it.hasTrait() } @@ -268,7 +278,7 @@ class EventStreamUnmarshallerGenerator( private fun RustWriter.renderParseProtocolPayload(member: MemberShape) { val parser = protocol.structuredDataParser(operationShape).payloadParser(member) - val memberName = member.memberName.toPascalCase() + val memberName = symbolProvider.toMemberName(member) rustTemplate( """ #{parser}(&message.payload()[..]) @@ -312,7 +322,7 @@ class EventStreamUnmarshallerGenerator( })?; return Ok(#{UnmarshalledMessage}::Error( #{OpError}::new( - #{OpError}Kind::${member.memberName.toPascalCase()}(builder.build()), + #{OpError}Kind::${symbolProvider.toMemberName(member)}(builder.build()), generic, ) )) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt index 191208a22c5f2412807ae61db4f4cafb03235e4f..61337af8e1c218263171d2bef62ee0ab030cf99f 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt @@ -38,7 +38,9 @@ import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.canUseDefault import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.smithy.generators.setterName import software.amazon.smithy.rust.codegen.smithy.isBoxed import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver @@ -49,7 +51,6 @@ import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.outputShape -import software.amazon.smithy.rust.codegen.util.toPascalCase import software.amazon.smithy.utils.StringUtils class JsonParserGenerator( @@ -59,6 +60,7 @@ class JsonParserGenerator( private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider private val runtimeConfig = codegenContext.runtimeConfig + private val mode = codegenContext.mode private val smithyJson = CargoDependency.smithyJson(runtimeConfig).asType() private val jsonDeserModule = RustModule.private("json_deser") private val codegenScope = arrayOf( @@ -86,7 +88,11 @@ class JsonParserGenerator( * We still generate the parser symbol even if there are no included members because the server * generation requires parsers for all input structures. */ - private fun structureParser(fnName: String, structureShape: StructureShape, includedMembers: List): RuntimeType { + private fun structureParser( + fnName: String, + structureShape: StructureShape, + includedMembers: List + ): RuntimeType { val unusedMut = if (includedMembers.isEmpty()) "##[allow(unused_mut)] " else "" return RuntimeType.forInlineFun(fnName, jsonDeserModule) { it.rustBlockTemplate( @@ -419,7 +425,7 @@ class JsonParserGenerator( ) withBlock("variant = match key.to_unescaped()?.as_ref() {", "};") { for (member in shape.members()) { - val variantName = member.memberName.toPascalCase() + val variantName = symbolProvider.toMemberName(member) rustBlock("${member.wireName().dq()} =>") { withBlock("Some(#T::$variantName(", "))", symbol) { deserializeMember(member) @@ -427,13 +433,28 @@ class JsonParserGenerator( } } } - // TODO: Handle unrecognized union variants (https://github.com/awslabs/smithy-rs/issues/185) - rust("_ => None") + when (mode.renderUnknownVariant()) { + // in client mode, resolve an unknown union variant to the unknown variant + true -> rustTemplate( + """ + _ => { + #{skip_value}(tokens)?; + Some(#{Union}::${UnionGenerator.UnknownVariantName}) + } + """, + "Union" to symbol, *codegenScope + ) + // in server mode, use strict parsing + false -> rustTemplate( + """variant => return Err(#{Error}::custom(format!("unexpected union variant: {}", variant)))""", + *codegenScope + ) + } } } } rustTemplate( - "_ => return Err(#{Error}::custom(\"expected start object or null\"))", + """_ => return Err(#{Error}::custom("expected start object or null"))""", *codegenScope ) } @@ -470,7 +491,7 @@ class JsonParserGenerator( inner() } rustTemplate( - "_ => return Err(#{Error}::custom(\"expected object key or end object\"))", + """other => return Err(#{Error}::custom(format!("expected object key or end object, found: {:?}", other)))""", *codegenScope ) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt index 26507f63e902949a5cedfe5996a94d82b1ef59e6..f7b414437ffad2a9b42d3c67ea8aef96db6eeefd 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt @@ -40,7 +40,9 @@ import software.amazon.smithy.rust.codegen.rustlang.withBlockTemplate import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.smithy.generators.setterName import software.amazon.smithy.rust.codegen.smithy.isBoxed import software.amazon.smithy.rust.codegen.smithy.isOptional @@ -51,7 +53,6 @@ import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectMember import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.outputShape -import software.amazon.smithy.rust.codegen.util.toPascalCase // The string argument is the name of the XML ScopedDecoder to continue parsing from typealias OperationInnerWriteable = RustWriter.(String) -> Unit @@ -109,6 +110,7 @@ class XmlBindingTraitParserGenerator( private val model = codegenContext.model private val index = HttpBindingIndex.of(model) private val xmlIndex = XmlNameIndex.of(model) + private val mode = codegenContext.mode private val xmlDeserModule = RustModule.private("xml_deser") /** @@ -385,9 +387,9 @@ class XmlBindingTraitParserGenerator( ) { val members = shape.members() rustTemplate("let mut base: Option<#{Shape}> = None;", *codegenScope, "Shape" to symbol) - parseLoop(Ctx(tag = "decoder", accum = null)) { ctx -> + parseLoop(Ctx(tag = "decoder", accum = null), ignoreUnexpected = false) { ctx -> members.forEach { member -> - val variantName = member.memberName.toPascalCase() + val variantName = symbolProvider.toMemberName(member) case(member) { val current = """ @@ -403,6 +405,10 @@ class XmlBindingTraitParserGenerator( rust("base = Some(#T::$variantName(tmp));", symbol) } } + when (mode.renderUnknownVariant()) { + true -> rust("_unknown => base = Some(#T::${UnionGenerator.UnknownVariantName}),", symbol) + false -> rustTemplate("""variant => return Err(#{XmlError}::custom(format!("unexpected union variant: {:?}", variant)))""", *codegenScope) + } } rustTemplate("""base.ok_or_else(||#{XmlError}::custom("expected union, got nothing"))""", *codegenScope) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt index 3e7d3770ed9fa924b1ecbc1bcad14f8e53b30b00..8a654e05cf13f5d2dc713a04c512a4bb7cb167bd 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGenerator.kt @@ -30,9 +30,13 @@ import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.withBlock +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant +import software.amazon.smithy.rust.codegen.smithy.generators.unknownVariantError import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.util.dq @@ -41,6 +45,7 @@ import software.amazon.smithy.rust.codegen.util.toPascalCase class EventStreamMarshallerGenerator( private val model: Model, + private val mode: CodegenMode, runtimeConfig: RuntimeConfig, private val symbolProvider: RustSymbolProvider, private val unionShape: UnionShape, @@ -96,12 +101,22 @@ class EventStreamMarshallerGenerator( rustBlock("let payload = match input") { for (member in unionShape.members()) { val eventType = member.memberName // must be the original name, not the Rust-safe name - rustBlock("Self::Input::${member.memberName.toPascalCase()}(inner) => ") { + rustBlock("Self::Input::${symbolProvider.toMemberName(member)}(inner) => ") { addStringHeader(":event-type", "${eventType.dq()}.into()") val target = model.expectShape(member.target, StructureShape::class.java) renderMarshallEvent(member, target) } } + if (mode.renderUnknownVariant()) { + rustTemplate( + """ + Self::Input::${UnionGenerator.UnknownVariantName} => return Err( + #{Error}::Marshalling(${unknownVariantError(unionSymbol.rustType().name).dq()}.to_owned()) + ) + """, + *codegenScope + ) + } } rustTemplate("; Ok(#{Message}::new_from_parts(headers, payload))", *codegenScope) } @@ -164,6 +179,7 @@ class EventStreamMarshallerGenerator( val optional = symbolProvider.toSymbol(member).isOptional() if (target is BlobShape || target is StringShape) { data class PayloadContext(val conversionFn: String, val contentType: String) + val ctx = when (target) { is BlobShape -> PayloadContext("into_inner", "application/octet-stream") is StringShape -> PayloadContext("into_bytes", "text/plain") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt index 3c4be4845986edf2515024ce8615e56ecbd8a271..6388cc253ae75689499a971df830b8fd3c25a941 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -35,6 +35,9 @@ import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant +import software.amazon.smithy.rust.codegen.smithy.generators.serializationError import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation @@ -45,7 +48,6 @@ import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.outputShape -import software.amazon.smithy.rust.codegen.util.toPascalCase class JsonSerializerGenerator( codegenContext: CodegenContext, @@ -56,6 +58,7 @@ class JsonSerializerGenerator( val writerExpression: String, /** Expression representing the value to write to the JsonValueWriter */ val valueExpression: ValueExpression, + /** Path in the JSON to get here, used for errors */ val shape: T, ) @@ -126,13 +129,13 @@ class JsonSerializerGenerator( private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider + private val mode = codegenContext.mode private val runtimeConfig = codegenContext.runtimeConfig private val smithyTypes = CargoDependency.SmithyTypes(runtimeConfig).asType() private val smithyJson = CargoDependency.smithyJson(runtimeConfig).asType() - private val serializerError = smithyTypes.member("Error") private val codegenScope = arrayOf( "String" to RuntimeType.String, - "Error" to serializerError, + "Error" to runtimeConfig.serializationError(), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), "JsonObjectWriter" to smithyJson.member("serialize::JsonObjectWriter"), "JsonValueWriter" to smithyJson.member("serialize::JsonValueWriter"), @@ -147,7 +150,11 @@ class JsonSerializerGenerator( * We still generate the serializer symbol even if there are no included members because the server * generation requires serializers for all output/error structures. */ - private fun structureSerializer(fnName: String, structureShape: StructureShape, includedMembers: List): RuntimeType { + private fun structureSerializer( + fnName: String, + structureShape: StructureShape, + includedMembers: List + ): RuntimeType { return RuntimeType.forInlineFun(fnName, operationSerModule) { it.rustBlockTemplate( "pub fn $fnName(value: &#{target}) -> Result", @@ -244,7 +251,9 @@ class JsonSerializerGenerator( override fun serverErrorSerializer(shape: ShapeId): RuntimeType { val errorShape = model.expectShape(shape, StructureShape::class.java) - val includedMembers = httpBindingResolver.errorResponseBindings(shape).filter { it.location == HttpLocation.DOCUMENT }.map { it.member } + val includedMembers = + httpBindingResolver.errorResponseBindings(shape).filter { it.location == HttpLocation.DOCUMENT } + .map { it.member } val fnName = symbolProvider.serializeFunctionName(errorShape) return structureSerializer(fnName, errorShape, includedMembers) } @@ -257,7 +266,7 @@ class JsonSerializerGenerator( val structureSymbol = symbolProvider.toSymbol(context.shape) val structureSerializer = RuntimeType.forInlineFun(fnName, jsonSerModule) { writer -> writer.rustBlockTemplate( - "pub fn $fnName(object: &mut #{JsonObjectWriter}, input: &#{Input})", + "pub fn $fnName(object: &mut #{JsonObjectWriter}, input: &#{Input}) -> Result<(), #{Error}>", "Input" to structureSymbol, *codegenScope, ) { @@ -270,9 +279,10 @@ class JsonSerializerGenerator( serializeMember(MemberContext.structMember(inner, member, symbolProvider)) } } + rust("Ok(())") } } - rust("#T(&mut ${context.objectName}, ${context.localName});", structureSerializer) + rust("#T(&mut ${context.objectName}, ${context.localName})?;", structureSerializer) } private fun RustWriter.serializeMember(context: MemberContext) { @@ -387,20 +397,28 @@ class JsonSerializerGenerator( val unionSymbol = symbolProvider.toSymbol(context.shape) val unionSerializer = RuntimeType.forInlineFun(fnName, jsonSerModule) { writer -> writer.rustBlockTemplate( - "pub fn $fnName(${context.writerExpression}: &mut #{JsonObjectWriter}, input: &#{Input})", + "pub fn $fnName(${context.writerExpression}: &mut #{JsonObjectWriter}, input: &#{Input}) -> Result<(), #{Error}>", "Input" to unionSymbol, *codegenScope, ) { rustBlock("match input") { for (member in context.shape.members()) { - val variantName = member.memberName.toPascalCase() + val variantName = symbolProvider.toMemberName(member) withBlock("#T::$variantName(inner) => {", "},", unionSymbol) { serializeMember(MemberContext.unionMember(context, "inner", member)) } } + if (mode.renderUnknownVariant()) { + rustTemplate( + "#{Union}::${UnionGenerator.UnknownVariantName} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", + "Union" to unionSymbol, + *codegenScope + ) + } } + rust("Ok(())") } } - rust("#T(&mut ${context.writerExpression}, ${context.valueExpression.asRef()});", unionSerializer) + rust("#T(&mut ${context.writerExpression}, ${context.valueExpression.asRef()})?;", unionSerializer) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt index 44809874e91aff2b1c313eec0b54c1a5850f57b1..cd7e8613a71943f737a3c735c93f4b7a8ec7fa87 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt @@ -34,6 +34,9 @@ import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant +import software.amazon.smithy.rust.codegen.smithy.generators.serializationError import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.protocols.serializeFunctionName import software.amazon.smithy.rust.codegen.smithy.rustType @@ -42,7 +45,6 @@ import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.orNull -import software.amazon.smithy.rust.codegen.util.toPascalCase abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : StructuredDataSerializerGenerator { protected data class Context( @@ -61,14 +63,22 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct val shape: MemberShape, ) { companion object { - fun structMember(context: Context, member: MemberShape, symProvider: RustSymbolProvider): MemberContext = + fun structMember( + context: Context, + member: MemberShape, + symProvider: RustSymbolProvider + ): MemberContext = MemberContext( context.writerExpression, ValueExpression.Value("${context.valueExpression.name}.${symProvider.toMemberName(member)}"), member ) - fun unionMember(context: Context, variantReference: String, member: MemberShape): MemberContext = + fun unionMember( + context: Context, + variantReference: String, + member: MemberShape + ): MemberContext = MemberContext( context.writerExpression, ValueExpression.Reference(variantReference), @@ -80,8 +90,9 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct protected val model = codegenContext.model protected val symbolProvider = codegenContext.symbolProvider protected val runtimeConfig = codegenContext.runtimeConfig + private val mode = codegenContext.mode private val serviceShape = codegenContext.serviceShape - private val serializerError = RuntimeType.Infallible + private val serializerError = runtimeConfig.serializationError() private val smithyTypes = CargoDependency.SmithyTypes(runtimeConfig).asType() private val smithyQuery = CargoDependency.smithyQuery(runtimeConfig).asType() private val serdeUtil = SerializerUtil(model) @@ -146,7 +157,7 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct val structureSerializer = RuntimeType.forInlineFun(fnName, querySerModule) { writer -> Attribute.AllowUnusedMut.render(writer) writer.rustBlockTemplate( - "pub fn $fnName(mut writer: #{QueryValueWriter}, input: &#{Input})", + "pub fn $fnName(mut writer: #{QueryValueWriter}, input: &#{Input}) -> Result<(), #{Error}>", "Input" to structureSymbol, *codegenScope ) { @@ -154,9 +165,10 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct rust("let (_, _) = (writer, input);") // Suppress unused argument warnings } serializeStructureInner(context) + rust("Ok(())") } } - rust("#T(${context.writerExpression}, ${context.valueExpression.name});", structureSerializer) + rust("#T(${context.writerExpression}, ${context.valueExpression.name})?;", structureSerializer) } private fun RustWriter.serializeStructureInner(context: Context) { @@ -294,13 +306,13 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct val unionSerializer = RuntimeType.forInlineFun(fnName, querySerModule) { writer -> Attribute.AllowUnusedMut.render(writer) writer.rustBlockTemplate( - "pub fn $fnName(mut writer: #{QueryValueWriter}, input: &#{Input})", + "pub fn $fnName(mut writer: #{QueryValueWriter}, input: &#{Input}) -> Result<(), #{Error}>", "Input" to unionSymbol, *codegenScope, ) { rustBlock("match input") { for (member in context.shape.members()) { - val variantName = member.memberName.toPascalCase() + val variantName = symbolProvider.toMemberName(member) withBlock("#T::$variantName(inner) => {", "},", unionSymbol) { serializeMember( MemberContext.unionMember( @@ -311,9 +323,17 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct ) } } + if (mode.renderUnknownVariant()) { + rustTemplate( + "#{Union}::${UnionGenerator.UnknownVariantName} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", + "Union" to unionSymbol, + *codegenScope + ) + } } + rust("Ok(())") } } - rust("#T(${context.writerExpression}, ${context.valueExpression.asRef()});", unionSerializer) + rust("#T(${context.writerExpression}, ${context.valueExpression.asRef()})?;", unionSerializer) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt index 0d80e01c0b5e3e408fb95c4a539123cb23072dcd..6110844c2231d48b35ea8493b4a27c8e21f87b61 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt @@ -39,6 +39,9 @@ import software.amazon.smithy.rust.codegen.rustlang.stripOuter import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant +import software.amazon.smithy.rust.codegen.smithy.generators.serializationError import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation @@ -50,7 +53,6 @@ import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape -import software.amazon.smithy.rust.codegen.util.toPascalCase class XmlBindingTraitSerializerGenerator( codegenContext: CodegenContext, @@ -60,16 +62,13 @@ class XmlBindingTraitSerializerGenerator( private val runtimeConfig = codegenContext.runtimeConfig private val model = codegenContext.model private val smithyXml = CargoDependency.smithyXml(runtimeConfig).asType() + private val mode = codegenContext.mode private val codegenScope = arrayOf( "XmlWriter" to smithyXml.member("encode::XmlWriter"), "ElementWriter" to smithyXml.member("encode::ElWriter"), "SdkBody" to RuntimeType.sdkBody(runtimeConfig), - // TODO: currently this doesn't ever actually fail, however, once unions have an unknown member - // serialization can fail here and we should replace this with a real error type. - // Currently the serialization errors just get converted into `OperationBuildError` by the code - // that calls this code - "Error" to RuntimeType("String", null, "std::string") + "Error" to runtimeConfig.serializationError() ) private val operationSerModule = RustModule.private("operation_ser") private val xmlSerModule = RustModule.private("xml_ser") @@ -145,7 +144,7 @@ class XmlBindingTraitSerializerGenerator( return RuntimeType.forInlineFun(fnName, xmlSerModule) { val t = symbolProvider.toSymbol(member).rustType().stripOuter().render(true) it.rustBlockTemplate( - "pub fn $fnName(input: &$t) -> std::result::Result, String>", + "pub fn $fnName(input: &$t) -> std::result::Result, #{Error}>", *codegenScope ) { rust("let mut out = String::new();") @@ -303,7 +302,7 @@ class XmlBindingTraitSerializerGenerator( val fnName = symbolProvider.serializeFunctionName(structureShape) val structureSerializer = RuntimeType.forInlineFun(fnName, xmlSerModule) { it.rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter})", + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> Result<(), #{Error}>", "Input" to structureSymbol, *codegenScope ) { @@ -312,9 +311,10 @@ class XmlBindingTraitSerializerGenerator( rust("let _ = input;") } structureInner(members, Ctx.Element("writer", "&input")) + rust("Ok(())") } } - rust("#T(&${ctx.input}, ${ctx.elementWriter})", structureSerializer) + rust("#T(&${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeUnion(unionShape: UnionShape, ctx: Ctx.Element) { @@ -322,7 +322,7 @@ class XmlBindingTraitSerializerGenerator( val unionSymbol = symbolProvider.toSymbol(unionShape) val structureSerializer = RuntimeType.forInlineFun(fnName, xmlSerModule) { it.rustBlockTemplate( - "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter})", + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter}) -> Result<(), #{Error}>", "Input" to unionSymbol, *codegenScope ) { @@ -330,15 +330,24 @@ class XmlBindingTraitSerializerGenerator( rustBlock("match input") { val members = unionShape.members() members.forEach { member -> - val variantName = member.memberName.toPascalCase() + val variantName = symbolProvider.toMemberName(member) withBlock("#T::$variantName(inner) =>", ",", unionSymbol) { serializeMember(member, Ctx.Scope("scope_writer", "inner")) } } + + if (mode.renderUnknownVariant()) { + rustTemplate( + "#{Union}::${UnionGenerator.UnknownVariantName} => return Err(#{Error}::unknown_variant(${unionSymbol.name.dq()}))", + "Union" to unionSymbol, + *codegenScope + ) + } } + rust("Ok(())") } } - rust("#T(&${ctx.input}, ${ctx.elementWriter})", structureSerializer) + rust("#T(&${ctx.input}, ${ctx.elementWriter})?", structureSerializer) } private fun RustWriter.serializeList(listShape: CollectionShape, ctx: Ctx.Scope) { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt index 39b3f8c71bff952f299f6b22b630857ea2180a9a..f3f498e5c1b74fa32a6525c95665cf37445d7e21 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/Rust.kt @@ -143,8 +143,8 @@ fun generatePluginContext(model: Model): Pair { } fun RustWriter.unitTest( - @Language("Rust", prefix = "fn test() {", suffix = "}") test: String, - name: String? = null + name: String? = null, + @Language("Rust", prefix = "fn test() {", suffix = "}") test: String ) { val testName = name ?: safeName("test") raw("#[test]") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestConfigCustomization.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestConfigCustomization.kt index 2b0c099215350b5dd26292d59a4e094ee6cf5621..4ccba848db5c043223f1713ea52273f1dd74d397 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestConfigCustomization.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestConfigCustomization.kt @@ -60,10 +60,10 @@ fun stubConfigProject(customization: ConfigCustomization, project: TestWriterDel project.withModule(RustModule.Config) { generator.render(it) it.unitTest( + "config_send_sync", """ fn assert_send_sync() {} assert_send_sync::(); - """ ) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt index d298b80beee12dcdd61b2ebeaf774423d541f0ae..06dcbf80989c5ad0553b7b46b288b1a8715b938e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt @@ -17,6 +17,7 @@ import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.smithy.CodegenConfig import software.amazon.smithy.rust.codegen.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.smithy.RuntimeCrateLocation import software.amazon.smithy.rust.codegen.smithy.RustCodegenPlugin @@ -30,7 +31,8 @@ import software.amazon.smithy.rust.codegen.smithy.letIf import software.amazon.smithy.rust.codegen.util.dq import java.io.File -val TestRuntimeConfig = RuntimeConfig(runtimeCrateLocation = RuntimeCrateLocation.Path(File("../rust-runtime/").absolutePath)) +val TestRuntimeConfig = + RuntimeConfig(runtimeCrateLocation = RuntimeCrateLocation.Path(File("../rust-runtime/").absolutePath)) val TestSymbolVisitorConfig = SymbolVisitorConfig( runtimeConfig = TestRuntimeConfig, codegenConfig = CodegenConfig(), @@ -72,7 +74,8 @@ fun testSymbolProvider(model: Model, serviceShape: ServiceShape? = null): RustSy fun testCodegenContext( model: Model, serviceShape: ServiceShape? = null, - settings: RustSettings = testRustSettings(model) + settings: RustSettings = testRustSettings(model), + mode: CodegenMode = CodegenMode.Client ): CodegenContext = CodegenContext( model, testSymbolProvider(model), @@ -80,7 +83,7 @@ fun testCodegenContext( serviceShape ?: ServiceShape.builder().version("test").id("test#Service").build(), ShapeId.from("test#Protocol"), settings.moduleName, - settings + settings, mode ) private const val SmithyVersion = "1.0" diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt index 9da2fc1ce7daec648a4ed6b6c644eb9116520eff..eabe406d1a0fbe52b7b5cf36c05786833455bb9b 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt @@ -78,6 +78,7 @@ class StructureGeneratorTest { StructureGenerator(model, provider, writer, inner).render() StructureGenerator(model, provider, writer, struct).render() writer.unitTest( + "struct_fields_optional", """ let s: Option = None; s.map(|i|println!("{:?}, {:?}", i.ts, i.byte_value)); @@ -139,6 +140,7 @@ class StructureGeneratorTest { val generator = StructureGenerator(model, provider, writer, credentials) generator.render() writer.unitTest( + "sensitive_fields_redacted", """ let creds = Credentials { username: Some("not_redacted".to_owned()), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt index 8e24bac7bec61c243fba04653eb02b3152d7489c..e0ead720fecea982e8736ac69bd2e33df448fc9a 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt @@ -77,11 +77,29 @@ class UnionGeneratorTest { ) } - private fun generateUnion(modelSmithy: String, unionName: String = "MyUnion"): RustWriter { + @Test + fun `render a union without an unknown variant`() { + val writer = generateUnion("union MyUnion { a: String, b: String }", unknownVariant = false) + writer.compileAndTest() + } + + @Test + fun `render an unknown variant`() { + val writer = generateUnion("union MyUnion { a: String, b: String }", unknownVariant = true) + writer.compileAndTest( + """ + let union = MyUnion::Unknown; + assert!(union.is_unknown()); + + """ + ) + } + + private fun generateUnion(modelSmithy: String, unionName: String = "MyUnion", unknownVariant: Boolean = true): RustWriter { val model = "namespace test\n$modelSmithy".asSmithyModel() val provider: SymbolProvider = testSymbolProvider(model) val writer = RustWriter.forModule("model") - UnionGenerator(model, provider, writer, model.lookup("test#$unionName")).render() + UnionGenerator(model, provider, writer, model.lookup("test#$unionName"), renderUnknownVariant = unknownVariant).render() return writer } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt index 00647d277928ba44ad43a319a4e324f5bb4fcadc..3717fdc84efde82e2e84ed9c525164f0ddcb7ec8 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/ResponseBindingGeneratorTest.kt @@ -96,6 +96,7 @@ class ResponseBindingGeneratorTest { testProject.withModule(RustModule.public("output")) { it.renderOperation() it.unitTest( + "http_header_deser", """ use crate::http_serde; let resp = http::Response::builder() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWordSymbolProviderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWordSymbolProviderTest.kt index 5696470fbccbf02f73f2c60754bd8388e8a5ecc2..8f19e5ecac35f988604964686485cd30473aefe3 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWordSymbolProviderTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustReservedWordSymbolProviderTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.model.traits.EnumDefinition import software.amazon.smithy.rust.codegen.smithy.MaybeRenamed import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.SymbolVisitorConfig +import software.amazon.smithy.rust.codegen.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.util.orNull import software.amazon.smithy.rust.codegen.util.toPascalCase @@ -34,7 +35,13 @@ internal class RustReservedWordSymbolProviderTest { @Test fun `member names are escaped`() { - val provider = RustReservedWordSymbolProvider(Stub()) + val model = """ + namespace namespace + structure container { + async: String + } + """.trimMargin().asSmithyModel() + val provider = RustReservedWordSymbolProvider(Stub(), model) provider.toMemberName( MemberShape.builder().id("namespace#container\$async").target("namespace#Integer").build() ) shouldBe "r##async" @@ -57,7 +64,8 @@ internal class RustReservedWordSymbolProviderTest { } private fun expectEnumRename(original: String, expected: MaybeRenamed) { - val provider = RustReservedWordSymbolProvider(Stub()) + val model = "namespace foo".asSmithyModel() + val provider = RustReservedWordSymbolProvider(Stub(), model) provider.toEnumVariantName(EnumDefinition.builder().name(original).value("foo").build()) shouldBe expected } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EndpointTraitBindingsTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EndpointTraitBindingsTest.kt index 30123c80690bbc4be0c5a24d8b6f81a9186d0c72..0b43212e92013c0a7fcf79a611953816df5e314d 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EndpointTraitBindingsTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EndpointTraitBindingsTest.kt @@ -76,10 +76,10 @@ internal class EndpointTraitBindingsTest { TestRuntimeConfig.operationBuildError() ) { endpointBindingGenerator.render(this, "self") - rust(".map_err(|e|#T::SerializationError(e.into()))", TestRuntimeConfig.operationBuildError()) } } it.unitTest( + "valid_prefix", """ let inp = GetStatusInput { foo: Some("test_value".to_string()) }; let prefix = inp.endpoint_prefix().unwrap(); @@ -87,6 +87,7 @@ internal class EndpointTraitBindingsTest { """ ) it.unitTest( + "invalid_prefix", """ // not a valid URI component let inp = GetStatusInput { foo: Some("test value".to_string()) }; @@ -95,6 +96,7 @@ internal class EndpointTraitBindingsTest { ) it.unitTest( + "unset_prefix", """ // unset is invalid let inp = GetStatusInput { foo: None }; @@ -103,6 +105,7 @@ internal class EndpointTraitBindingsTest { ) it.unitTest( + "empty_prefix", """ // empty is invalid let inp = GetStatusInput { foo: Some("".to_string()) }; diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGeneratorTest.kt index 25a41720984303504a092dff6dd9dd6ea86b719d..0794aa755797567877549779bb361c4a4d52decf 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/config/ServiceConfigGeneratorTest.kt @@ -96,6 +96,7 @@ internal class ServiceConfigGeneratorTest { project.withModule(RustModule.Config) { sut.render(it) it.unitTest( + "set_config_fields", """ let mut builder = Config::builder(); builder.config_field = Some(99); diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt index cc37eadf7df774f52c9c876135f5ae2614177190..1946d6f8d1351c042a41fe0a675f6b12f5b7f32b 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/EventStreamTestTools.kt @@ -19,12 +19,14 @@ import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.rust.codegen.rustlang.RustModule import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.BuilderGenerator import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.generators.error.CombinedErrorGenerator import software.amazon.smithy.rust.codegen.smithy.generators.implBlock +import software.amazon.smithy.rust.codegen.smithy.generators.renderUnknownVariant import software.amazon.smithy.rust.codegen.smithy.transformers.EventStreamNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.testutil.TestWorkspace @@ -123,6 +125,7 @@ object EventStreamTestModels { val validTestUnion: String, val validSomeError: String, val validUnmodeledError: String, + val mode: CodegenMode = CodegenMode.Client, val protocolBuilder: (CodegenContext) -> Protocol, ) { override fun toString(): String = protocolShapeId @@ -144,6 +147,21 @@ object EventStreamTestModels { validUnmodeledError = """{"Message":"unmodeled error"}""", ) { RestJson(it) }, + // + // restJson1, server mode + // + TestCase( + protocolShapeId = "aws.protocols#restJson1", + model = restJson1(), + requestContentType = "application/json", + responseContentType = "application/json", + validTestStruct = """{"someString":"hello","someInt":5}""", + validMessageWithNoHeaderPayloadTraits = """{"someString":"hello","someInt":5}""", + validTestUnion = """{"Foo":"hello"}""", + validSomeError = """{"Message":"some error"}""", + validUnmodeledError = """{"Message":"unmodeled error"}""", + ) { RestJson(it) }, + // // awsJson1_1 // @@ -285,7 +303,7 @@ object EventStreamTestModels { """.trimIndent(), ) { Ec2QueryProtocol(it) }, - ) + ).flatMap { listOf(it, it.copy(mode = CodegenMode.Server)) } class UnmarshallTestCasesProvider : ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream = @@ -311,8 +329,8 @@ data class TestEventStreamProject( ) object EventStreamTestTools { - fun generateTestProject(model: Model): TestEventStreamProject { - val model = EventStreamNormalizer.transform(OperationNormalizer.transform(model)) + fun generateTestProject(testCase: EventStreamTestModels.TestCase): TestEventStreamProject { + val model = EventStreamNormalizer.transform(OperationNormalizer.transform(testCase.model)) val serviceShape = model.expectShape(ShapeId.from("test#TestService")) as ServiceShape val operationShape = model.expectShape(ShapeId.from("test#TestStreamOp")) as OperationShape val unionShape = model.expectShape(ShapeId.from("test#TestStream")) as UnionShape @@ -332,7 +350,7 @@ object EventStreamTestTools { } project.withModule(RustModule.public("model")) { val inputOutput = model.lookup("test#TestStreamInputOutput") - recursivelyGenerateModels(model, symbolProvider, inputOutput, it) + recursivelyGenerateModels(model, symbolProvider, inputOutput, it, testCase.mode) } project.withModule(RustModule.public("output")) { operationShape.outputShape(model).renderWithModelBuilder(model, symbolProvider, it) @@ -351,7 +369,8 @@ object EventStreamTestTools { model: Model, symbolProvider: RustSymbolProvider, shape: Shape, - writer: RustWriter + writer: RustWriter, + mode: CodegenMode ) { for (member in shape.members()) { val target = model.expectShape(member.target) @@ -359,9 +378,9 @@ object EventStreamTestTools { if (target is StructureShape) { target.renderWithModelBuilder(model, symbolProvider, writer) } else if (target is UnionShape) { - UnionGenerator(model, symbolProvider, writer, target).render() + UnionGenerator(model, symbolProvider, writer, target, renderUnknownVariant = mode.renderUnknownVariant()).render() } - recursivelyGenerateModels(model, symbolProvider, target, writer) + recursivelyGenerateModels(model, symbolProvider, target, writer, mode) } } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt index 4fbb59874bcde2079f70b65ee2f9fae5706cbf1c..a43cfaf54d456ba1f7de9286a6a37b77d704b8a5 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/Ec2QueryParserGeneratorTest.kt @@ -53,16 +53,16 @@ class Ec2QueryParserGeneratorTest { project.lib { writer -> writer.unitTest( - name = "valid_input", - test = """ - let xml = br#" - - Some value - - "#; - let output = ${writer.format(operationParser)}(xml, output::some_operation_output::Builder::default()).unwrap().build(); - assert_eq!(output.some_attribute, Some(5)); - assert_eq!(output.some_val, Some("Some value".to_string())); + "valid_input", + """ + let xml = br#" + + Some value + + "#; + let output = ${writer.format(operationParser)}(xml, output::some_operation_output::Builder::default()).unwrap().build(); + assert_eq!(output.some_attribute, Some(5)); + assert_eq!(output.some_val, Some("Some value".to_string())); """ ) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt index 3e538cfff5408b02778aee2b8ce010bf6ebd4462..af5599a5a315a73843ac2273feb2344a82afa615 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestModels import software.amazon.smithy.rust.codegen.smithy.protocols.EventStreamTestTools import software.amazon.smithy.rust.codegen.testutil.TestRuntimeConfig @@ -21,7 +22,7 @@ class EventStreamUnmarshallerGeneratorTest { @ParameterizedTest @ArgumentsSource(EventStreamTestModels.UnmarshallTestCasesProvider::class) fun test(testCase: EventStreamTestModels.TestCase) { - val test = EventStreamTestTools.generateTestProject(testCase.model) + val test = EventStreamTestTools.generateTestProject(testCase) val codegenContext = CodegenContext( test.model, @@ -30,7 +31,8 @@ class EventStreamUnmarshallerGeneratorTest { test.serviceShape, ShapeId.from(testCase.protocolShapeId), "test", - testRustSettings(test.model) + testRustSettings(test.model), + mode = testCase.mode ) val protocol = testCase.protocolBuilder(codegenContext) val generator = EventStreamUnmarshallerGenerator( @@ -39,7 +41,8 @@ class EventStreamUnmarshallerGeneratorTest { TestRuntimeConfig, test.symbolProvider, test.operationShape, - test.streamShape + test.streamShape, + mode = testCase.mode ) test.project.lib { writer -> @@ -81,7 +84,8 @@ class EventStreamUnmarshallerGeneratorTest { ) writer.unitTest( - """ + name = "message_with_blob", + test = """ let message = msg("event", "MessageWithBlob", "application/octet-stream", b"hello, world!"); let result = ${writer.format(generator.render())}().unmarshall(&message); assert!(result.is_ok(), "expected ok, got: {:?}", result); @@ -92,10 +96,25 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_blob", ) + if (testCase.mode == CodegenMode.Client) { + writer.unitTest( + "unknown_message", + """ + let message = msg("event", "NewUnmodeledMessageType", "application/octet-stream", b"hello, world!"); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::Unknown, + expect_event(result.unwrap()) + ); + """, + ) + } + writer.unitTest( + "message_with_string", """ let message = msg("event", "MessageWithString", "text/plain", b"hello, world!"); let result = ${writer.format(generator.render())}().unmarshall(&message); @@ -105,10 +124,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_string", ) writer.unitTest( + "message_with_struct", """ let message = msg( "event", @@ -128,10 +147,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_struct", ) writer.unitTest( + "message_with_union", """ let message = msg( "event", @@ -148,10 +167,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_union", ) writer.unitTest( + "message_with_headers", """ let message = msg("event", "MessageWithHeaders", "application/octet-stream", b"") .add_header(Header::new("blob", HeaderValue::ByteArray((&b"test"[..]).into()))) @@ -179,10 +198,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_headers", ) writer.unitTest( + "message_with_header_and_payload", """ let message = msg("event", "MessageWithHeaderAndPayload", "application/octet-stream", b"payload") .add_header(Header::new("header", HeaderValue::String("header".into()))); @@ -197,10 +216,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_header_and_payload", ) writer.unitTest( + "message_with_no_header_payload_traits", """ let message = msg( "event", @@ -219,10 +238,10 @@ class EventStreamUnmarshallerGeneratorTest { expect_event(result.unwrap()) ); """, - "message_with_no_header_payload_traits", ) writer.unitTest( + "some_error", """ let message = msg( "exception", @@ -237,10 +256,10 @@ class EventStreamUnmarshallerGeneratorTest { kind => panic!("expected SomeError, but got {:?}", kind), } """, - "some_error", ) writer.unitTest( + "generic_error", """ let message = msg( "exception", @@ -253,14 +272,14 @@ class EventStreamUnmarshallerGeneratorTest { match expect_error(result.unwrap()).kind { TestStreamOpErrorKind::Unhandled(err) => { assert!(format!("{}", err).contains("message: \"unmodeled error\"")); - }, + } kind => panic!("expected generic error, but got {:?}", kind), } """, - "generic_error", ) writer.unitTest( + "bad_content_type", """ let message = msg( "event", @@ -272,7 +291,6 @@ class EventStreamUnmarshallerGeneratorTest { assert!(result.is_err(), "expected error, got: {:?}", result); assert!(format!("{}", result.err().unwrap()).contains("expected :content-type to be")); """, - "bad_content_type", ) } test.project.compileAndTest() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt index af309dd330480011412de991dbbd4119227d42b8..b665de5a4e2adab677a8ca7c2f4e2c68b8d03e05 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt @@ -125,6 +125,7 @@ class JsonParserGeneratorTest { val project = TestWorkspace.testProject(testSymbolProvider(model)) project.lib { writer -> writer.unitTest( + "json_parser", """ use model::Choice; @@ -136,9 +137,10 @@ class JsonParserGeneratorTest { { "top": { "extra": 45, "field": "something", - "choice": - { "int": 5 }, - "empty": { "not_empty": true }}} + "choice": { "int": 5 }, + "empty": { "not_empty": true } + } + } "#; let output = ${writer.format(operationGenerator!!)}(json, output::op_output::Builder::default()).unwrap().build(); @@ -146,16 +148,40 @@ class JsonParserGeneratorTest { assert_eq!(Some(45), top.extra); assert_eq!(Some("something".to_string()), top.field); assert_eq!(Some(Choice::Int(5)), top.choice); - let output = ${writer.format(operationGenerator!!)}(b"", output::op_output::Builder::default()).unwrap().build(); + """ + ) + writer.unitTest( + "empty_body", + """ + // empty body + let output = ${writer.format(operationGenerator)}(b"", output::op_output::Builder::default()).unwrap().build(); assert_eq!(output.top, None); + """ + ) + writer.unitTest( + "unknown_variant", + """ + // unknown variant + let input = br#"{ "top": { "choice": { "somenewvariant": "data" } } }"#; + let output = ${writer.format(operationGenerator)}(input, output::op_output::Builder::default()).unwrap().build(); + assert!(output.top.unwrap().choice.unwrap().is_unknown()); + """ + ) - + writer.unitTest( + "empty_error", + """ // empty error let error_output = ${writer.format(errorParser!!)}(b"", error::error::Builder::default()).unwrap().build(); assert_eq!(error_output.message, None); + """ + ) + writer.unitTest( + "error_with_message", + """ // error with message - let error_output = ${writer.format(errorParser!!)}(br#"{"message": "hello"}"#, error::error::Builder::default()).unwrap().build(); + let error_output = ${writer.format(errorParser)}(br#"{"message": "hello"}"#, error::error::Builder::default()).unwrap().build(); assert_eq!(error_output.message.expect("message should be set"), "hello"); """ ) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt index b4ef50239756ec1d09709bfe21930411a6397ec9..3b9254277b18a6185f32037f4261498db4f3dcc1 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt @@ -169,7 +169,25 @@ internal class XmlBindingTraitParserGeneratorTest { "#; - ${writer.format(operationParser)}(xml, output::op_output::Builder::default()).expect_err("invalid input"); + ${writer.format(operationParser)}(xml, output::op_output::Builder::default()).expect("unknown union variant does not cause failure"); + """ + ) + writer.unitTest( + name = "unknown_union_variant", + test = """ + let xml = br#" + + + some key + + hello + + + + + "#; + let output = ${writer.format(operationParser)}(xml, output::op_output::Builder::default()).unwrap().build(); + assert!(output.choice.unwrap().is_unknown()); """ ) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt index c89deb0e5abc0a16e621737f6410efbe612b79ed..01aef3f9699e971355ac3703183e00f4b4c41e33 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt @@ -5,11 +5,13 @@ package software.amazon.smithy.rust.codegen.smithy.protocols.serialize -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.rustlang.RustModule +import software.amazon.smithy.rust.codegen.smithy.CodegenMode import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer @@ -84,16 +86,22 @@ class AwsQuerySerializerGeneratorTest { } """.asSmithyModel() - @Test - fun `generates valid serializers`() { + @ParameterizedTest + @CsvSource("true", "false") + fun `generates valid serializers`(generateUnknownVariant: Boolean) { val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel)) val symbolProvider = testSymbolProvider(model) - val parserGenerator = AwsQuerySerializerGenerator(testCodegenContext(model)) + val mode = when (generateUnknownVariant) { + true -> CodegenMode.Client + false -> CodegenMode.Server + } + val parserGenerator = AwsQuerySerializerGenerator(testCodegenContext(model).copy(mode = mode)) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) val project = TestWorkspace.testProject(testSymbolProvider(model)) project.lib { writer -> writer.unitTest( + "query_serializer", """ use model::Top; @@ -126,7 +134,7 @@ class AwsQuerySerializerGeneratorTest { } project.withModule(RustModule.public("model")) { model.lookup("test#Top").renderWithModelBuilder(model, symbolProvider, it) - UnionGenerator(model, symbolProvider, it, model.lookup("test#Choice")).render() + UnionGenerator(model, symbolProvider, it, model.lookup("test#Choice"), renderUnknownVariant = generateUnknownVariant).render() val enum = model.lookup("test#FooEnum") EnumGenerator(model, symbolProvider, it, enum, enum.expectTrait()).render() } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt index a0141780c5657bc3c4fdc307aa385a9739f75246..50c9dbb26df2be1be35abdd38af5fa59aa40820e 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt @@ -93,6 +93,7 @@ class Ec2QuerySerializerGeneratorTest { val project = TestWorkspace.testProject(testSymbolProvider(model)) project.lib { writer -> writer.unitTest( + "ec2query_serializer", """ use model::Top; diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt index 64ee57f1b9dbb227ce2975a4b838f46c216bd85b..0cdbf77fa9216cc380971a532c64a15baadb20d7 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt @@ -24,7 +24,7 @@ class EventStreamMarshallerGeneratorTest { @ParameterizedTest @ArgumentsSource(EventStreamTestModels.MarshallTestCasesProvider::class) fun test(testCase: EventStreamTestModels.TestCase) { - val test = EventStreamTestTools.generateTestProject(testCase.model) + val test = EventStreamTestTools.generateTestProject(testCase) val codegenContext = CodegenContext( test.model, @@ -33,11 +33,13 @@ class EventStreamMarshallerGeneratorTest { test.serviceShape, ShapeId.from(testCase.protocolShapeId), "test", - testRustSettings(test.model) + testRustSettings(test.model), + mode = testCase.mode ) val protocol = testCase.protocolBuilder(codegenContext) val generator = EventStreamMarshallerGenerator( test.model, + testCase.mode, TestRuntimeConfig, test.symbolProvider, test.streamShape, @@ -76,6 +78,7 @@ class EventStreamMarshallerGeneratorTest { ) writer.unitTest( + "message_with_blob", """ let event = TestStream::MessageWithBlob( MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() @@ -89,10 +92,10 @@ class EventStreamMarshallerGeneratorTest { assert_eq!(&str_header("application/octet-stream"), *headers.get(":content-type").unwrap()); assert_eq!(&b"hello, world!"[..], message.payload()); """, - "message_with_blob", ) writer.unitTest( + "message_with_string", """ let event = TestStream::MessageWithString( MessageWithString::builder().data("hello, world!").build() @@ -106,10 +109,10 @@ class EventStreamMarshallerGeneratorTest { assert_eq!(&str_header("text/plain"), *headers.get(":content-type").unwrap()); assert_eq!(&b"hello, world!"[..], message.payload()); """, - "message_with_string", ) writer.unitTest( + "message_with_struct", """ let event = TestStream::MessageWithStruct( MessageWithStruct::builder().some_struct( @@ -133,10 +136,10 @@ class EventStreamMarshallerGeneratorTest { MediaType::from(${testCase.requestContentType.dq()}) ).unwrap(); """, - "message_with_struct", ) writer.unitTest( + "message_with_union", """ let event = TestStream::MessageWithUnion(MessageWithUnion::builder().some_union( TestUnion::Foo("hello".into()) @@ -155,10 +158,10 @@ class EventStreamMarshallerGeneratorTest { MediaType::from(${testCase.requestContentType.dq()}) ).unwrap(); """, - "message_with_union", ) writer.unitTest( + "message_with_headers", """ let event = TestStream::MessageWithHeaders(MessageWithHeaders::builder() .blob(Blob::new(&b"test"[..])) @@ -187,10 +190,10 @@ class EventStreamMarshallerGeneratorTest { .add_header(Header::new("timestamp", HeaderValue::Timestamp(Instant::from_epoch_seconds(5)))); assert_eq!(expected_message, actual_message); """, - "message_with_headers", ) writer.unitTest( + "message_with_header_and_payload", """ let event = TestStream::MessageWithHeaderAndPayload(MessageWithHeaderAndPayload::builder() .header("header") @@ -207,10 +210,10 @@ class EventStreamMarshallerGeneratorTest { .add_header(Header::new(":content-type", HeaderValue::String("application/octet-stream".into()))); assert_eq!(expected_message, actual_message); """, - "message_with_header_and_payload", ) writer.unitTest( + "message_with_no_header_payload_traits", """ let event = TestStream::MessageWithNoHeaderPayloadTraits(MessageWithNoHeaderPayloadTraits::builder() .some_int(5) @@ -231,7 +234,6 @@ class EventStreamMarshallerGeneratorTest { MediaType::from(${testCase.requestContentType.dq()}) ).unwrap(); """, - "message_with_no_header_payload_traits", ) } test.project.compileAndTest() diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt index 1449663c51243ea936a68c4e6a40af7648c5651e..026e5910c2552a229921427d0fc69f559a5d892c 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt @@ -111,8 +111,9 @@ class JsonSerializerGeneratorTest { val project = TestWorkspace.testProject(testSymbolProvider(model)) project.lib { writer -> writer.unitTest( + "json_serializers", """ - use model::Top; + use model::{Top, Choice}; // Generate the document serializer even though it's not tested directly // ${writer.format(documentGenerator)} @@ -127,6 +128,13 @@ class JsonSerializerGeneratorTest { let serialized = ${writer.format(operationGenerator!!)}(&input).unwrap(); let output = std::str::from_utf8(serialized.bytes().unwrap()).unwrap(); assert_eq!(output, r#"{"top":{"field":"hello!","extra":45,"rec":[{"extra":55}]}}"#); + + let input = crate::input::OpInput::builder().top( + Top::builder() + .choice(Choice::Unknown) + .build() + ).build().unwrap(); + let serialized = ${writer.format(operationGenerator)}(&input).expect_err("cannot serialize unknown variant"); """ ) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt index 4d0fedf9d9aa7b5aa15a46a6b23853ada87ba8e3..437dcc8d36264f5247c61a8e99310545ca9485fe 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt @@ -110,11 +110,12 @@ internal class XmlBindingTraitSerializerGeneratorTest { testCodegenContext(model), HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/xml")) ) - val operationParser = parserGenerator.payloadSerializer(model.lookup("test#OpInput\$payload")) + val operationSerializer = parserGenerator.payloadSerializer(model.lookup("test#OpInput\$payload")) val project = TestWorkspace.testProject(testSymbolProvider(model)) project.lib { writer -> writer.unitTest( + "serialize_xml", """ use model::Top; let inp = crate::input::OpInput::builder().payload( @@ -124,11 +125,23 @@ internal class XmlBindingTraitSerializerGeneratorTest { .recursive(Top::builder().extra(55).build()) .build() ).build().unwrap(); - let serialized = ${writer.format(operationParser)}(&inp.payload.unwrap()).unwrap(); + let serialized = ${writer.format(operationSerializer)}(&inp.payload.unwrap()).unwrap(); let output = std::str::from_utf8(&serialized).unwrap(); assert_eq!(output, "hello!"); """ ) + writer.unitTest( + "unknown_variants", + """ + use model::{Top, Choice}; + let input = crate::input::OpInput::builder().payload( + Top::builder() + .choice(Choice::Unknown) + .build() + ).build().unwrap(); + ${writer.format(operationSerializer)}(&input.payload.unwrap()).expect_err("cannot serialize unknown variant"); + """ + ) } project.withModule(RustModule.public("model")) { model.lookup("test#Top").renderWithModelBuilder(model, symbolProvider, it) diff --git a/rust-runtime/aws-smithy-http/Cargo.toml b/rust-runtime/aws-smithy-http/Cargo.toml index 2bb4926d8daed030f94bb0c69c10eaeccc45faaf..c579dba8ad0380f17633259bec649c8c4310d78c 100644 --- a/rust-runtime/aws-smithy-http/Cargo.toml +++ b/rust-runtime/aws-smithy-http/Cargo.toml @@ -21,7 +21,6 @@ http = "0.2.3" http-body = "0.4.0" percent-encoding = "2.1.0" pin-project = "1" -thiserror = "1" tracing = "0.1" # We are using hyper for our streaming body implementation, but this is an internal detail. diff --git a/rust-runtime/aws-smithy-http/src/endpoint.rs b/rust-runtime/aws-smithy-http/src/endpoint.rs index 3fbc0f837ed4f1d05f226952415cd84d4a28517d..b47c49ad68a2f153d06ff1fd816bfacc693a3245 100644 --- a/rust-runtime/aws-smithy-http/src/endpoint.rs +++ b/rust-runtime/aws-smithy-http/src/endpoint.rs @@ -3,10 +3,13 @@ * SPDX-License-Identifier: Apache-2.0. */ -use http::uri::{Authority, InvalidUri, Uri}; use std::borrow::Cow; use std::str::FromStr; +use http::uri::{Authority, Uri}; + +use crate::operation::BuildError; + /// API Endpoint /// /// This implements an API endpoint as specified in the @@ -22,10 +25,16 @@ pub struct Endpoint { #[derive(Clone, Debug, Eq, PartialEq)] pub struct EndpointPrefix(String); impl EndpointPrefix { - pub fn new(prefix: impl Into) -> Result { + pub fn new(prefix: impl Into) -> Result { let prefix = prefix.into(); - let _ = Authority::from_str(&prefix)?; - Ok(EndpointPrefix(prefix)) + match Authority::from_str(&prefix) { + Ok(_) => Ok(EndpointPrefix(prefix)), + Err(err) => Err(BuildError::InvalidUri { + uri: prefix, + err, + message: "invalid prefix".into(), + }), + } } pub fn as_str(&self) -> &str { @@ -109,9 +118,10 @@ impl Endpoint { #[cfg(test)] mod test { - use crate::endpoint::{Endpoint, EndpointPrefix}; use http::Uri; + use crate::endpoint::{Endpoint, EndpointPrefix}; + #[test] fn prefix_endpoint() { let ep = Endpoint::mutable(Uri::from_static("https://us-east-1.dynamo.amazonaws.com")); diff --git a/rust-runtime/aws-smithy-http/src/operation.rs b/rust-runtime/aws-smithy-http/src/operation.rs index 272153b2c877ef60dbee9b41a2e512c12b2525fd..50a9c03b4822d680a94d98ac9f21d2d9afdb55e7 100644 --- a/rust-runtime/aws-smithy-http/src/operation.rs +++ b/rust-runtime/aws-smithy-http/src/operation.rs @@ -5,10 +5,11 @@ use crate::body::SdkBody; use crate::property_bag::{PropertyBag, SharedPropertyBag}; +use http::uri::InvalidUri; use std::borrow::Cow; use std::error::Error; +use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; -use thiserror::Error; #[derive(Clone, Debug)] pub struct Metadata { @@ -50,25 +51,102 @@ pub struct Parts { /// protocol serialization (e.g. fields that can only be a subset ASCII because they are serialized /// as the name of an HTTP header) #[non_exhaustive] -#[derive(Debug, Error)] +#[derive(Debug)] pub enum BuildError { - #[error("Invalid field in input: {field} (Details: {details})")] + /// A field contained an invalid value InvalidField { field: &'static str, details: String, }, - #[error("{field} was missing. {details}")] + /// A field was missing MissingField { field: &'static str, details: &'static str, }, - #[error("Failed during serialization: {0}")] - SerializationError(#[from] Box), + /// The serializer could not serialize the input + SerializationError(SerializationError), + + /// The serializer did not produce a valid URI + /// + /// This typically indicates that a field contained invalid characters. + InvalidUri { + uri: String, + err: InvalidUri, + message: Cow<'static, str>, + }, - #[error("Error during request construction: {0}")] + /// An error occurred request construction Other(Box), } +impl From for BuildError { + fn from(err: SerializationError) -> Self { + BuildError::SerializationError(err) + } +} + +impl Display for BuildError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BuildError::InvalidField { field, details } => write!( + f, + "Invalid field in input: {} (Details: {})", + field, details + ), + BuildError::MissingField { field, details } => { + write!(f, "{} was missing. {}", field, details) + } + BuildError::SerializationError(inner) => { + write!(f, "failed to serialize input: {}", inner) + } + BuildError::Other(inner) => write!(f, "error during request construction: {}", inner), + BuildError::InvalidUri { uri, err, message } => { + write!( + f, + "generated URI `{}` was not a valid URI ({}): {}", + uri, err, message + ) + } + } + } +} + +impl Error for BuildError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + BuildError::SerializationError(inner) => Some(inner as _), + BuildError::Other(inner) => Some(inner.as_ref()), + _ => None, + } + } +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum SerializationError { + #[non_exhaustive] + CannotSerializeUnknownVariant { union: &'static str }, +} + +impl SerializationError { + pub fn unknown_variant(union: &'static str) -> Self { + Self::CannotSerializeUnknownVariant { union } + } +} + +impl Display for SerializationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SerializationError::CannotSerializeUnknownVariant { union } => write!(f, "Cannot serialize `{}::Unknown`.\ + Unknown union variants cannot be serialized. This can occur when round-tripping a \ + response from the server that was not recognized by the SDK. Consider upgrading to the \ + latest version of the SDK.", union) + } + } +} + +impl Error for SerializationError {} + #[derive(Debug)] pub struct Operation { request: Request, diff --git a/rust-runtime/aws-smithy-json/src/deserialize/error.rs b/rust-runtime/aws-smithy-json/src/deserialize/error.rs index f5f6d0ce2fcaeb9733d4afde4cd43f8b55db9311..7db421ab3a4d5ebd8248e30d026ef94935bfc630 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize/error.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize/error.rs @@ -34,7 +34,7 @@ impl Error { } /// Returns a custom error without an offset. - pub fn custom(message: &'static str) -> Error { + pub fn custom(message: impl Into>) -> Error { Error::new(ErrorReason::Custom(message.into()), None) } }