From d3658a2ca9e953c872b0611f86948a0279f808e6 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Fri, 11 Jun 2021 16:58:51 -0700 Subject: [PATCH 1/4] Finish the smithy-json deserializer codegen --- .../rust/codegen/rustlang/CargoDependency.kt | 20 +- .../rust/codegen/smithy/RuntimeTypes.kt | 22 +- .../smithy/generators/EnumGenerator.kt | 17 - .../codegen/smithy/generators/Instantiator.kt | 20 +- .../rust/codegen/smithy/protocols/AwsJson.kt | 298 ++++-------------- .../protocols/CustomSerializerGenerator.kt | 208 ------------ .../protocols/HttpBoundProtocolGenerator.kt | 62 ++-- .../protocols/JsonSerializerSymbolProvider.kt | 89 ------ .../smithy/protocols/ProtocolLoader.kt | 4 +- .../rust/codegen/smithy/protocols/RestJson.kt | 28 +- .../parse/SerdeJsonParserGenerator.kt | 140 -------- .../serialize/QuerySerializerGenerator.kt | 2 +- rust-runtime/inlineable/Cargo.toml | 3 +- .../inlineable/src/aws_json_errors.rs | 164 ---------- rust-runtime/inlineable/src/blob_serde.rs | 33 -- rust-runtime/inlineable/src/doc_json.rs | 209 ------------ rust-runtime/inlineable/src/instant_epoch.rs | 32 -- .../inlineable/src/instant_httpdate.rs | 33 -- .../inlineable/src/instant_iso8601.rs | 33 -- rust-runtime/inlineable/src/json_errors.rs | 218 +++++++++++++ rust-runtime/inlineable/src/lib.rs | 26 +- 21 files changed, 347 insertions(+), 1314 deletions(-) delete mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGenerator.kt delete mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/JsonSerializerSymbolProvider.kt delete mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/SerdeJsonParserGenerator.kt delete mode 100644 rust-runtime/inlineable/src/aws_json_errors.rs delete mode 100644 rust-runtime/inlineable/src/blob_serde.rs delete mode 100644 rust-runtime/inlineable/src/doc_json.rs delete mode 100644 rust-runtime/inlineable/src/instant_epoch.rs delete mode 100644 rust-runtime/inlineable/src/instant_httpdate.rs delete mode 100644 rust-runtime/inlineable/src/instant_iso8601.rs create mode 100644 rust-runtime/inlineable/src/json_errors.rs diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt index 1706367d11..7bf28d4301 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/CargoDependency.kt @@ -88,25 +88,12 @@ class InlineDependency( private fun forRustFile(name: String, vararg additionalDependencies: RustDependency) = forRustFile(name, "inlineable", *additionalDependencies) - fun awsJsonErrors(runtimeConfig: RuntimeConfig) = - forRustFile("aws_json_errors", CargoDependency.Http, CargoDependency.SmithyTypes(runtimeConfig)) - - fun docJson() = forRustFile("doc_json", CargoDependency.Serde) - fun instantEpoch() = forRustFile("instant_epoch", CargoDependency.Serde) - fun instantHttpDate() = - forRustFile("instant_httpdate", CargoDependency.Serde) - - fun instant8601() = forRustFile("instant_iso8601", CargoDependency.Serde) + fun jsonErrors(runtimeConfig: RuntimeConfig) = + forRustFile("json_errors", CargoDependency.Http, CargoDependency.SmithyTypes(runtimeConfig)) fun idempotencyToken() = forRustFile("idempotency_token", CargoDependency.FastRand) - fun blobSerde(runtimeConfig: RuntimeConfig) = forRustFile( - "blob_serde", - CargoDependency.Serde, - CargoDependency.SmithyHttp(runtimeConfig) - ) - fun ec2QueryErrors(runtimeConfig: RuntimeConfig): InlineDependency = forRustFile("ec2_query_errors", CargoDependency.smithyXml(runtimeConfig)) @@ -201,9 +188,6 @@ data class CargoDependency( fun smithyQuery(runtimeConfig: RuntimeConfig): CargoDependency = runtimeConfig.runtimeCrate("query") fun smithyXml(runtimeConfig: RuntimeConfig): CargoDependency = runtimeConfig.runtimeCrate("xml") - val SerdeJson: CargoDependency = - CargoDependency("serde_json", CratesIo("1"), features = listOf("float_roundtrip")) - val Serde = CargoDependency("serde", CratesIo("1"), features = listOf("derive")) val Bytes: RustDependency = CargoDependency("bytes", CratesIo("1")) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index f5f055cf7e..7e89ceceb0 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -98,6 +98,7 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n val Debug = stdfmt.member("Debug") val Default: RuntimeType = RuntimeType("Default", dependency = null, namespace = "std::default") val From = RuntimeType("From", dependency = null, namespace = "std::convert") + val Infallible = RuntimeType("Infallible", dependency = null, namespace = "std::convert") val PartialEq = std.member("cmp::PartialEq") val StdError = RuntimeType("Error", dependency = null, namespace = "std::error") val String = RuntimeType("String", dependency = null, namespace = "std::string") @@ -160,25 +161,9 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n val HttpRequestBuilder = Http("request::Builder") val HttpResponseBuilder = Http("response::Builder") - fun Serde(path: String) = RuntimeType( - path, dependency = CargoDependency.Serde, namespace = "serde" - ) - - val Deserialize: RuntimeType = RuntimeType("Deserialize", CargoDependency.Serde, namespace = "serde") - val Deserializer = RuntimeType("Deserializer", CargoDependency.Serde, namespace = "serde") - fun SerdeJson(path: String) = - RuntimeType(path, dependency = CargoDependency.SerdeJson, namespace = "serde_json") - - val serdeJson = RuntimeType(null, dependency = CargoDependency.SerdeJson, namespace = "serde_json") - - fun awsJsonErrors(runtimeConfig: RuntimeConfig) = - forInlineDependency(InlineDependency.awsJsonErrors(runtimeConfig)) - - val DocJson by lazy { forInlineDependency(InlineDependency.docJson()) } + fun jsonErrors(runtimeConfig: RuntimeConfig) = + forInlineDependency(InlineDependency.jsonErrors(runtimeConfig)) - val InstantEpoch by lazy { forInlineDependency(InlineDependency.instantEpoch()) } - val InstantHttpDate by lazy { forInlineDependency(InlineDependency.instantHttpDate()) } - val Instant8601 by lazy { forInlineDependency(InlineDependency.instant8601()) } val IdempotencyToken by lazy { forInlineDependency(InlineDependency.idempotencyToken()) } val Config = RuntimeType("config", null, "crate") @@ -205,7 +190,6 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n ) val Bytes = RuntimeType("Bytes", dependency = CargoDependency.Bytes, namespace = "bytes") - fun BlobSerde(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.blobSerde(runtimeConfig)) fun forInlineDependency(inlineDependency: InlineDependency) = RuntimeType(inlineDependency.name, inlineDependency, namespace = "crate") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EnumGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EnumGenerator.kt index 5872b565d8..78272cacf5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EnumGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/EnumGenerator.kt @@ -15,7 +15,6 @@ 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.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.MaybeRenamed import software.amazon.smithy.rust.codegen.smithy.RuntimeType @@ -104,7 +103,6 @@ class EnumGenerator( } else { renderUnamedEnum() } - renderSerde() } private fun renderUnamedEnum() { @@ -163,21 +161,6 @@ class EnumGenerator( } } - private fun renderSerde() { - writer.rustTemplate( - """ - impl<'de> #{deserialize}<'de> for $enumName { - fn deserialize(deserializer: D) -> Result where D: #{deserializer}<'de> { - let data = <&str>::deserialize(deserializer)?; - Ok(Self::from(data)) - } - } - """, - "deserializer" to RuntimeType.Deserializer, - "deserialize" to RuntimeType.Deserialize - ) - } - private fun renderFromStr() { writer.rustBlock("impl #T<&str> for $enumName", RuntimeType.From) { writer.rustBlock("fn from(s: &str) -> Self") { 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 93687430a2..72fade3271 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 @@ -29,12 +29,15 @@ import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustType import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.conditionalBlock import software.amazon.smithy.rust.codegen.rustlang.escape import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.stripOuter import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig @@ -113,13 +116,16 @@ class Instantiator( is StringShape -> renderString(writer, shape, arg as StringNode) is NumberShape -> writer.write(arg.asNumberNode().get()) is BooleanShape -> writer.write(arg.asBooleanNode().get().toString()) - is DocumentShape -> { - writer.rust( - """{ - let as_json = #T! { ${Node.prettyPrintJson(arg)} }; - #T::json_to_doc(as_json) - }""", - RuntimeType.SerdeJson("json"), RuntimeType.DocJson + is DocumentShape -> writer.rustBlock("") { + val smithyJson = CargoDependency.smithyJson(runtimeConfig).asType() + rustTemplate( + """ + let json_bytes = br##"${Node.prettyPrintJson(arg)}"##; + let mut tokens = #{json_token_iter}(json_bytes).peekable(); + #{expect_document}(&mut tokens).expect("well formed json") + """, + "expect_document" to smithyJson.member("deserialize::token::expect_document"), + "json_token_iter" to smithyJson.member("deserialize::json_token_iter"), ) } else -> writer.writeWithNoFormatting("todo!() /* $shape $arg */") 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 bd073144e9..3380eb715f 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 @@ -5,52 +5,31 @@ package software.amazon.smithy.rust.codegen.smithy.protocols -import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.pattern.UriPattern -import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.ToShapeId import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.rust.codegen.rustlang.Attribute -import software.amazon.smithy.rust.codegen.rustlang.RustType -import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rust -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.stripOuter -import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.smithy.Serializers -import software.amazon.smithy.rust.codegen.smithy.WrappingSymbolProvider -import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata -import software.amazon.smithy.rust.codegen.smithy.generators.HttpProtocolGenerator import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolGeneratorFactory import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolSupport -import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator -import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol -import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol -import software.amazon.smithy.rust.codegen.smithy.generators.operationBuildError -import software.amazon.smithy.rust.codegen.smithy.locatedIn -import software.amazon.smithy.rust.codegen.smithy.meta 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 -import software.amazon.smithy.rust.codegen.smithy.rustType -import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait -import software.amazon.smithy.rust.codegen.smithy.traits.OutputBodyTrait +import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.RemoveEventStreamOperations import software.amazon.smithy.rust.codegen.smithy.transformers.StructureModifier -import software.amazon.smithy.rust.codegen.util.dq -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.outputShape sealed class AwsJsonVersion { abstract val value: String @@ -64,16 +43,15 @@ sealed class AwsJsonVersion { } } -class BasicAwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFactory { - override fun buildProtocolGenerator( - protocolConfig: ProtocolConfig - ): BasicAwsJsonGenerator = BasicAwsJsonGenerator(protocolConfig, version) +class AwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFactory { + override fun buildProtocolGenerator(protocolConfig: ProtocolConfig): HttpBoundProtocolGenerator { + return HttpBoundProtocolGenerator(protocolConfig, AwsJson(protocolConfig, version)) + } private val shapeIfHasMembers: StructureModifier = { _, shape: StructureShape? -> - if (shape?.members().isNullOrEmpty()) { - null - } else { - shape + when (shape?.members().isNullOrEmpty()) { + true -> null + else -> shape } } @@ -85,14 +63,6 @@ class BasicAwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGenerat ).let(RemoveEventStreamOperations::transform) } - override fun symbolProvider(model: Model, base: RustSymbolProvider): RustSymbolProvider { - return JsonSerializerSymbolProvider( - model, - SyntheticBodySymbolProvider(model, base), - TimestampFormatTrait.Format.EPOCH_SECONDS - ) - } - override fun support(): ProtocolSupport = ProtocolSupport( requestSerialization = true, requestBodySerialization = true, @@ -101,57 +71,6 @@ class BasicAwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGenerat ) } -/** - * SyntheticBodySymbolProvider makes two modifications: - * 1. Body shapes are moved to `serializer.rs` - * 2. Body shapes take a reference to all of their members: - * If the base structure was: - * ```rust - * struct { - * field: Option - * } - * ``` - * The body will generate: - * ```rust - * struct<'a> { - * field: &'a Option - * } - * - * This enables the creation of a body from a reference to an input without cloning. - */ -class SyntheticBodySymbolProvider(private val model: Model, private val base: RustSymbolProvider) : - WrappingSymbolProvider(base) { - override fun toSymbol(shape: Shape): Symbol { - val initialSymbol = base.toSymbol(shape) - val override = when (shape) { - is StructureShape -> when { - shape.hasTrait() -> - initialSymbol.toBuilder().locatedIn(Serializers).build() - shape.hasTrait() -> - initialSymbol.toBuilder().locatedIn(Serializers).meta( - initialSymbol.expectRustMetadata().withDerives(RuntimeType("Default", null, "std::default")) - ).build() - else -> null - } - is MemberShape -> { - val container = model.expectShape(shape.container) - if (container.hasTrait()) { - initialSymbol.toBuilder().rustType( - RustType.Reference( - lifetime = "a", - member = initialSymbol.rustType().stripOuter() - ) - ).build() - } else { - null - } - } - else -> null - } - return override ?: initialSymbol - } -} - class AwsJsonHttpBindingResolver( private val model: Model, private val awsJsonVersion: AwsJsonVersion, @@ -183,159 +102,74 @@ class AwsJsonHttpBindingResolver( "application/x-amz-json-${awsJsonVersion.value}" } -// TODO: Refactor to use HttpBoundProtocolGenerator -class BasicAwsJsonGenerator( +/** + * AwsJson requires all empty inputs to be sent across as `{}`. This class + * customizes wraps [JsonSerializerGenerator] to add this functionality. + */ +class AwsJsonSerializerGenerator( private val protocolConfig: ProtocolConfig, - private val awsJsonVersion: AwsJsonVersion -) : HttpProtocolGenerator(protocolConfig) { - private val model = protocolConfig.model + httpBindingResolver: HttpBindingResolver, + private val jsonSerializerGenerator: JsonSerializerGenerator = + JsonSerializerGenerator(protocolConfig, httpBindingResolver) +) : StructuredDataSerializerGenerator by jsonSerializerGenerator { private val runtimeConfig = protocolConfig.runtimeConfig - private val symbolProvider = protocolConfig.symbolProvider - private val operationIndex = OperationIndex.of(model) + private val codegenScope = arrayOf( + "Error" to CargoDependency.SmithyTypes(runtimeConfig).asType().member("Error"), + "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + ) - override fun traitImplementations(operationWriter: RustWriter, operationShape: OperationShape) { - val outputSymbol = symbolProvider.toSymbol(operationShape.outputShape(model)) - val operationName = symbolProvider.toSymbol(operationShape).name - operationWriter.rustTemplate( - """ - impl #{parse_strict} for $operationName { - type Output = std::result::Result<#{output}, #{error}>; - fn parse(&self, response: &#{response}<#{bytes}>) -> Self::Output { - self.parse_response(response) + override fun operationSerializer(operationShape: OperationShape): RuntimeType { + var serializer = jsonSerializerGenerator.operationSerializer(operationShape) + if (serializer == null) { + val inputShape = operationShape.inputShape(protocolConfig.model) + val fnName = protocolConfig.symbolProvider.serializeFunctionName(operationShape) + serializer = RuntimeType.forInlineFun(fnName, "operation_ser") { + it.rustBlockTemplate( + "pub fn $fnName(_input: &#{target}) -> Result<#{SdkBody}, #{Error}>", + *codegenScope, "target" to protocolConfig.symbolProvider.toSymbol(inputShape) + ) { + rustTemplate("""Ok(#{SdkBody}::from("{}"))""", *codegenScope) } } - """, - "parse_strict" to RuntimeType.parseStrict(runtimeConfig), - "output" to outputSymbol, - "error" to operationShape.errorSymbol(symbolProvider), - "response" to RuntimeType.Http("Response"), - "bytes" to RuntimeType.Bytes - ) - } - - override fun toHttpRequestImpl( - implBlockWriter: RustWriter, - operationShape: OperationShape, - inputShape: StructureShape - ) { - httpBuilderFun(implBlockWriter) { - write("let builder = #T::new();", RuntimeType.HttpRequestBuilder) - rust( - """ - Ok( - builder - .method("POST") - .header("Content-Type", "application/x-amz-json-${awsJsonVersion.value}") - .header("X-Amz-Target", "${protocolConfig.serviceShape.id.name}.${operationShape.id.name}") - ) - """ - ) } + return serializer } +} - override fun RustWriter.body(self: String, operationShape: OperationShape): BodyMetadata { - val generator = JsonSerializerGenerator(protocolConfig, AwsJsonHttpBindingResolver(model, awsJsonVersion)) - val serializer = generator.operationSerializer(operationShape) - serializer?.also { sym -> - rustTemplate( - "#{serialize}(&$self).map_err(|err|#{BuildError}::SerializationError(err.into()))?", - "serialize" to sym, - "BuildError" to runtimeConfig.operationBuildError() - ) - } ?: rustTemplate("""#{SdkBody}::from("{}")""", "SdkBody" to RuntimeType.sdkBody(runtimeConfig)) - return BodyMetadata(takesOwnership = false) - } +class AwsJson( + private val protocolConfig: ProtocolConfig, + awsJsonVersion: AwsJsonVersion +) : Protocol { + private val runtimeConfig = protocolConfig.runtimeConfig - override fun operationImplBlock(implBlockWriter: RustWriter, operationShape: OperationShape) { - val outputShape = operationIndex.getOutput(operationShape).get() - val errorSymbol = operationShape.errorSymbol(symbolProvider) - val jsonErrors = RuntimeType.awsJsonErrors(protocolConfig.runtimeConfig) - val generator = JsonParserGenerator(protocolConfig, AwsJsonHttpBindingResolver(model, awsJsonVersion)) + override val httpBindingResolver: HttpBindingResolver = + AwsJsonHttpBindingResolver(protocolConfig.model, awsJsonVersion) - fromResponseFun(implBlockWriter, operationShape) { - rustBlock("if #T::is_error(&response)", jsonErrors) { - rustTemplate( - """ - let body = #{sj}::from_slice(response.body().as_ref()) - .unwrap_or_else(|_|#{sj}::json!({})); - let generic = #{aws_json_errors}::parse_generic_error(&response, &body); - """, - "aws_json_errors" to jsonErrors, "sj" to RuntimeType.serdeJson - ) - if (operationShape.errors.isNotEmpty()) { - rustTemplate( - """ + override val defaultTimestampFormat: TimestampFormatTrait.Format = TimestampFormatTrait.Format.EPOCH_SECONDS - let error_code = match generic.code() { - Some(code) => code, - None => return Err(#{error_symbol}::unhandled(generic)) - };""", - "error_symbol" to errorSymbol - ) - withBlock("return Err(match error_code {", "})") { - // approx: - /* - match error_code { - "Code1" => deserialize(body), - "Code2" => deserialize(body) - } - */ - parseErrorVariants(operationShape, errorSymbol) - } - } else { - write("return Err(#T::generic(generic))", errorSymbol) - } - } - val parser = generator.operationParser(operationShape) - Attribute.AllowUnusedMut.render(this) - rust("let mut builder = #T::default();", outputShape.builderSymbol(symbolProvider)) - parser?.also { - rustTemplate( - "builder = #{parse}(response.body().as_ref(), builder).map_err(#{error_symbol}::unhandled)?;", - "parse" to it, - "error_symbol" to errorSymbol - ) - } - withBlock("Ok(builder.build()", ")") { - if (StructureGenerator.fallibleBuilder(outputShape, symbolProvider)) { - rust(""".map_err(#T::unhandled)?""", errorSymbol) - } - } - } - } + override fun additionalHeaders(operationShape: OperationShape): List> = + listOf("X-Amz-Target" to "${protocolConfig.serviceShape.id.name}.${operationShape.id.name}") - private fun fromResponseFun( - implBlockWriter: RustWriter, - operationShape: OperationShape, - block: RustWriter.() -> Unit - ) { - Attribute.Custom("allow(clippy::unnecessary_wraps)").render(implBlockWriter) - Attribute.AllowUnused.render(implBlockWriter) - implBlockWriter.rustBlock( - "fn parse_response(&self, response: & #T<#T>) -> std::result::Result<#T, #T>", - RuntimeType.Http("response::Response"), - RuntimeType.Bytes, - symbolProvider.toSymbol(operationShape.outputShape(model)), - operationShape.errorSymbol(symbolProvider) - ) { - block(this) - } - } + override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator = + JsonParserGenerator(protocolConfig, httpBindingResolver) + + override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator = + AwsJsonSerializerGenerator(protocolConfig, httpBindingResolver) - private fun RustWriter.parseErrorVariants( - operationShape: OperationShape, - errorSymbol: RuntimeType - ) { - operationShape.errors.forEach { error -> - rustBlock("${error.name.dq()} => match #T(body)", RuntimeType.SerdeJson("from_value")) { - val variantName = symbolProvider.toSymbol(model.expectShape(error)).name - write( - "Ok(body) => #1T { kind: #1TKind::$variantName(body), meta: generic },", - errorSymbol + override fun parseGenericError(operationShape: OperationShape): RuntimeType { + return RuntimeType.forInlineFun("parse_generic_error", "json_deser") { + it.rustBlockTemplate( + "pub fn parse_generic_error(response: &#{Response}<#{Bytes}>) -> Result<#{Error}, #{JsonError}>", + "Response" to RuntimeType.http.member("Response"), + "Bytes" to RuntimeType.Bytes, + "Error" to RuntimeType.GenericError(runtimeConfig), + "JsonError" to CargoDependency.smithyJson(runtimeConfig).asType().member("deserialize::Error") + ) { + rust( + "#T::parse_generic_error(response)", + RuntimeType.jsonErrors(runtimeConfig) ) - write("Err(e) => #T::unhandled(e)", errorSymbol) } } - write("_ => #T::generic(generic)", errorSymbol) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGenerator.kt deleted file mode 100644 index 1d22386972..0000000000 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGenerator.kt +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.rust.codegen.smithy.protocols - -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.HttpBinding -import software.amazon.smithy.model.knowledge.HttpBindingIndex -import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.rust.codegen.rustlang.RustType -import software.amazon.smithy.rust.codegen.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.rustlang.Writable -import software.amazon.smithy.rust.codegen.rustlang.contains -import software.amazon.smithy.rust.codegen.rustlang.render -import software.amazon.smithy.rust.codegen.rustlang.rustBlock -import software.amazon.smithy.rust.codegen.rustlang.stripOuter -import software.amazon.smithy.rust.codegen.rustlang.withBlock -import software.amazon.smithy.rust.codegen.rustlang.writable -import software.amazon.smithy.rust.codegen.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.smithy.rustType - -/** - * Generate custom serialization and deserialization functions when required. - * - * The general structure is: - * For a given type that does not implement serialize/deserialize, convert it to a `newtype` that _does_ (for example, - * see `InstantEpoch` in `instant_epoch.rs`. Then, using those types, invoke the serde derived serializer. - * - * The generated code isn't optimal performance-wise. It uses `.collect()` (creating a new Vector from an iterator) - * in places that may be avoidable. - * This may be an eventual performance bottleneck, but it should be totally avoidable with slightly more complex - * code generation. Furthermore, these code paths are only hit for custom types. - */ -class CustomSerializerGenerator( - private val symbolProvider: RustSymbolProvider, - model: Model, - private val defaultTimestampFormat: TimestampFormatTrait.Format -) { - private val httpBindingIndex = HttpBindingIndex.of(model) - private val runtimeConfig = symbolProvider.config().runtimeConfig - - private val instant = RuntimeType.Instant(runtimeConfig).toSymbol().rustType() - private val blob = RuntimeType.Blob(runtimeConfig).toSymbol().rustType() - private val document = RuntimeType.Document(runtimeConfig).toSymbol().rustType() - private val customShapes = setOf(instant, blob, document) - - /** - * Generate a custom deserialization function for [memberShape], suitable to be used - * in the serde annotation `serialize_with` (See [JsonSerializerSymbolProvider]) - * - * The returned object is a RuntimeType, which generates and creates all necessary dependencies when used. - * - * If this shape does not require custom serialization, this function returns null. - * - * For example, the deserializer for `Option` when converted to epoch seconds: - * To make it more readable, I've manually removed the fully qualified types. - * ```rust - * pub fn stdoptionoptioninstant_epoch_seconds_deser<'de, D>( - * _deser: D, - * ) -> Result, D::Error> - * where - * D: Deserializer<'de>, - * { - * use ::serde::Deserialize; - * Ok( - * Option::::deserialize(_deser)? - * .map(|el| el.0), - * ) - * } - * ``` - */ - - fun deserializerFor(memberShape: MemberShape): RuntimeType? { - val symbol = symbolProvider.toSymbol(memberShape) - val rustType = symbol.rustType() - if (customShapes.none { rustType.contains(it) }) { - return null - } - val fnName = deserializerName(rustType, memberShape) - return RuntimeType.forInlineFun(fnName, "serde_util") { writer -> - deserializeFn(writer, fnName, symbol) { - deserializer(rustType, memberShape) - } - } - } - - /** - * Generate a deserializer for the given type dynamically, eg: - * ```rust - * use ::serde::Deserialize; - * Ok( - * Option::::deserialize(_deser)? - * .map(|el| el.0) - * ) - * ``` - * - * It utilizes a newtype that defines the given serialization to access the serde serializer - * then performs any necessary mapping / unmapping. This has a slight disadvantage in that - * that wrapping structures like `Vec` may be allocated twice—I think we should be able to avoid - * this eventually however. - */ - private fun RustWriter.deserializer(t: RustType, memberShape: MemberShape) { - write("use #T;", RuntimeType.Deserialize) - withBlock("Ok(", ")") { - serdeType(t, memberShape)(this) - write("::deserialize(_deser)?") - unrollDeser(t) - } - } - - private fun RustWriter.unrollDeser(realType: RustType) { - when (realType) { - is RustType.Vec -> withBlock(".into_iter().map(|el|el", ").collect()") { - unrollDeser(realType.member) - } - is RustType.Option -> withBlock(".map(|el|el", ")") { - unrollDeser(realType.member) - } - - is RustType.HashMap -> withBlock(".into_iter().map(|(k,el)|(k, el", ")).collect()") { - unrollDeser(realType.member) - } - - // We will only create HashSets of strings, so we shouldn't ever hit this - is RustType.HashSet -> TODO("https://github.com/awslabs/smithy-rs/issues/44") - - is RustType.Box -> { - unrollDeser(realType.member) - write(".into()") - } - - else -> if (customShapes.contains(realType)) { - write(".0") - } else { - TODO("unsupported type $realType") - } - } - } - - private fun RustWriter.serdeContainerType(realType: RustType.Container, memberShape: MemberShape) { - val prefix = when (realType) { - is RustType.HashMap -> "${realType.namespace}::${realType.name}:: "${realType.namespace}::${realType.name}::<" - } - withBlock(prefix, ">") { - serdeType(realType.member, memberShape)(this) - } - } - - private fun serdeType(realType: RustType, memberShape: MemberShape): Writable { - return when (realType) { - instant -> writable { - val format = tsFormat(memberShape) - when (format) { - TimestampFormatTrait.Format.DATE_TIME -> write("#T::InstantIso8601", RuntimeType.Instant8601) - TimestampFormatTrait.Format.EPOCH_SECONDS -> write("#T::InstantEpoch", RuntimeType.InstantEpoch) - TimestampFormatTrait.Format.HTTP_DATE -> write( - "#T::InstantHttpDate", - RuntimeType.InstantHttpDate - ) - else -> write("todo!() /* unknown timestamp format */") - } - } - blob -> writable { - write("#T::BlobDeser", RuntimeType.BlobSerde(runtimeConfig)) - } - document -> writable { - write("#T::DeserDoc", RuntimeType.DocJson) - } - is RustType.Container -> writable { serdeContainerType(realType, memberShape) } - else -> TODO("Deserialize for $realType is not supported") - } - } - - private fun tsFormat(memberShape: MemberShape) = - httpBindingIndex.determineTimestampFormat(memberShape, HttpBinding.Location.PAYLOAD, defaultTimestampFormat) - - private fun deserializerName(rustType: RustType, memberShape: MemberShape): String { - val context = when { - rustType.contains(instant) -> tsFormat(memberShape).name.replace('-', '_').toLowerCase() - else -> null - } - val typeToFnName = - rustType.stripOuter().render(fullyQualified = true).filter { it.isLetterOrDigit() } - .toLowerCase() - return listOfNotNull(typeToFnName, context, "deser").joinToString("_") - } - - private fun deserializeFn( - rustWriter: RustWriter, - functionName: String, - symbol: Symbol, - body: RustWriter.() -> Unit - ) { - rustWriter.rustBlock( - "pub fn $functionName<'de, D>(_deser: D) -> Result<#T, D::Error> where D: #T<'de>", - symbol, - RuntimeType.Deserializer - ) { - body(this) - } - } -} 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 f3defbeca6..bc644f41fe 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 @@ -51,6 +51,8 @@ interface Protocol { val defaultTimestampFormat: TimestampFormatTrait.Format + fun additionalHeaders(operationShape: OperationShape): List> = emptyList() + fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator @@ -212,16 +214,16 @@ class HttpBoundProtocolGenerator( val successCode = httpBindingResolver.httpTrait(operationShape).code rustTemplate( """ - impl #{ParseStrict} for $operationName { - type Output = std::result::Result<#{O}, #{E}>; - fn parse(&self, response: &#{Response}<#{Bytes}>) -> Self::Output { - if !response.status().is_success() && response.status().as_u16() != $successCode { - #{parse_error}(response) - } else { - #{parse_response}(response) - } - } - }""", + impl #{ParseStrict} for $operationName { + type Output = std::result::Result<#{O}, #{E}>; + fn parse(&self, response: &#{Response}<#{Bytes}>) -> Self::Output { + if !response.status().is_success() && response.status().as_u16() != $successCode { + #{parse_error}(response) + } else { + #{parse_response}(response) + } + } + }""", *codegenScope, "O" to outputSymbol, "E" to operationShape.errorSymbol(symbolProvider), @@ -238,21 +240,21 @@ class HttpBoundProtocolGenerator( val successCode = httpBindingResolver.httpTrait(operationShape).code rustTemplate( """ - impl #{ParseResponse}<#{SdkBody}> for $operationName { - type Output = std::result::Result<#{O}, #{E}>; - fn parse_unloaded(&self, response: &mut http::Response<#{SdkBody}>) -> Option { - // This is an error, defer to the non-streaming parser - if !response.status().is_success() && response.status().as_u16() != $successCode { - return None; - } - Some(#{parse_streaming_response}(response)) - } - fn parse_loaded(&self, response: &http::Response<#{Bytes}>) -> Self::Output { - // if streaming, we only hit this case if its an error - #{parse_error}(response) + impl #{ParseResponse}<#{SdkBody}> for $operationName { + type Output = std::result::Result<#{O}, #{E}>; + fn parse_unloaded(&self, response: &mut http::Response<#{SdkBody}>) -> Option { + // This is an error, defer to the non-streaming parser + if !response.status().is_success() && response.status().as_u16() != $successCode { + return None; } + Some(#{parse_streaming_response}(response)) + } + fn parse_loaded(&self, response: &http::Response<#{Bytes}>) -> Self::Output { + // if streaming, we only hit this case if its an error + #{parse_error}(response) } - """, + } + """, "O" to outputSymbol, "E" to operationShape.errorSymbol(symbolProvider), "parse_streaming_response" to parseStreamingResponse(operationShape), @@ -380,14 +382,12 @@ class HttpBoundProtocolGenerator( val contentType = httpBindingResolver.requestContentType(operationShape) httpBindingGenerator.renderUpdateHttpBuilder(implBlockWriter) httpBuilderFun(implBlockWriter) { - rust( - """ - let builder = #T::new(); - let builder = builder.header("Content-Type", ${contentType.dq()}); - self.update_http_builder(builder) - """, - RuntimeType.HttpRequestBuilder - ) + rust("let builder = #T::new();", RuntimeType.HttpRequestBuilder) + val additionalHeaders = listOf("Content-Type" to contentType) + protocol.additionalHeaders(operationShape) + for (header in additionalHeaders) { + rust("let builder = builder.header(${header.first.dq()}, ${header.second.dq()});") + } + rust("self.update_http_builder(builder)") } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/JsonSerializerSymbolProvider.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/JsonSerializerSymbolProvider.kt deleted file mode 100644 index 453520cfc8..0000000000 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/JsonSerializerSymbolProvider.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.rust.codegen.smithy.protocols - -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.StringShape -import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.model.traits.JsonNameTrait -import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.rust.codegen.rustlang.Attribute -import software.amazon.smithy.rust.codegen.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.smithy.Default -import software.amazon.smithy.rust.codegen.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.smithy.SymbolMetadataProvider -import software.amazon.smithy.rust.codegen.smithy.defaultValue -import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata -import software.amazon.smithy.rust.codegen.smithy.isOptional -import software.amazon.smithy.rust.codegen.smithy.letIf -import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait -import software.amazon.smithy.rust.codegen.smithy.traits.OutputBodyTrait -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait -import software.amazon.smithy.rust.codegen.util.dq -import software.amazon.smithy.rust.codegen.util.getTrait -import software.amazon.smithy.rust.codegen.util.hasTrait - -/** - * JsonSerializerSymbolProvider annotates shapes and members with `serde` attributes - */ -class JsonSerializerSymbolProvider( - private val model: Model, - private val base: RustSymbolProvider, - defaultTimestampFormat: TimestampFormatTrait.Format -) : - SymbolMetadataProvider(base) { - - data class SerdeConfig(val deserialize: Boolean) - - private fun MemberShape.serializedName() = this.getTrait()?.value ?: this.memberName - - private val serializerBuilder = CustomSerializerGenerator(base, model, defaultTimestampFormat) - override fun memberMeta(memberShape: MemberShape): RustMetadata { - val currentMeta = base.toSymbol(memberShape).expectRustMetadata() - val serdeConfig = serdeRequired(model.expectShape(memberShape.container)) - val attribs = mutableListOf() - if (serdeConfig.deserialize) { - attribs.add(Attribute.Custom("serde(rename = ${memberShape.serializedName().dq()})")) - } - if (serdeConfig.deserialize) { - serializerBuilder.deserializerFor(memberShape)?.also { - attribs.add(Attribute.Custom("serde(deserialize_with = ${it.fullyQualifiedName().dq()})", listOf(it))) - } - val memberSymbol = base.toSymbol(memberShape) - if (model.expectShape(memberShape.container) is StructureShape && (memberSymbol.isOptional() || memberSymbol.defaultValue() == Default.RustDefault)) { - attribs.add(Attribute.Custom("serde(default)")) - } - } - return currentMeta.copy(additionalAttributes = currentMeta.additionalAttributes + attribs) - } - - override fun structureMeta(structureShape: StructureShape): RustMetadata = containerMeta(structureShape) - override fun unionMeta(unionShape: UnionShape): RustMetadata = containerMeta(unionShape) - override fun enumMeta(stringShape: StringShape): RustMetadata = base.toSymbol(stringShape).expectRustMetadata() - - private fun containerMeta(container: Shape): RustMetadata { - val currentMeta = base.toSymbol(container).expectRustMetadata() - val requiredSerde = serdeRequired(container) - return currentMeta.letIf(requiredSerde.deserialize) { it.withDerives(RuntimeType.Deserialize) } - } - - private fun serdeRequired(shape: Shape): SerdeConfig { - return when { - shape.hasTrait() -> SerdeConfig(deserialize = false) - shape.hasTrait() -> SerdeConfig(deserialize = true) - - // The bodies must be serializable. The top level inputs are _not_ - shape.hasTrait() -> SerdeConfig(deserialize = false) - shape.hasTrait() -> SerdeConfig(deserialize = false) - else -> SerdeConfig(deserialize = true) - } - } -} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/ProtocolLoader.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/ProtocolLoader.kt index 5cc1654a52..2cbef22d75 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/ProtocolLoader.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/ProtocolLoader.kt @@ -38,8 +38,8 @@ class ProtocolLoader(private val supportedProtocols: ProtocolMap) { companion object { val DefaultProtocols = mapOf( - AwsJson1_0Trait.ID to BasicAwsJsonFactory(AwsJsonVersion.Json10), - AwsJson1_1Trait.ID to BasicAwsJsonFactory(AwsJsonVersion.Json11), + AwsJson1_0Trait.ID to AwsJsonFactory(AwsJsonVersion.Json10), + AwsJson1_1Trait.ID to AwsJsonFactory(AwsJsonVersion.Json11), AwsQueryTrait.ID to AwsQueryFactory(), Ec2QueryTrait.ID to Ec2QueryFactory(), RestJson1Trait.ID to RestJsonFactory(), diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt index f50d0e1bfd..bdbe3b8625 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt @@ -11,10 +11,11 @@ import software.amazon.smithy.model.knowledge.HttpBindingIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.TimestampFormatTrait +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.asType +import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate -import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolGeneratorFactory import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolSupport @@ -64,14 +65,6 @@ class RestJsonFactory : ProtocolGeneratorFactory { errorDeserialization = true ) } - - override fun symbolProvider(model: Model, base: RustSymbolProvider): RustSymbolProvider { - return JsonSerializerSymbolProvider( - model, - SyntheticBodySymbolProvider(model, base), - TimestampFormatTrait.Format.EPOCH_SECONDS - ) - } } class RestJson(private val protocolConfig: ProtocolConfig) : Protocol { @@ -91,22 +84,17 @@ class RestJson(private val protocolConfig: ProtocolConfig) : Protocol { } override fun parseGenericError(operationShape: OperationShape): RuntimeType { - val awsJsonErrors = RuntimeType.awsJsonErrors(runtimeConfig) return RuntimeType.forInlineFun("parse_generic_error", "json_deser") { it.rustBlockTemplate( - "pub fn parse_generic_error(response: &#{Response}<#{Bytes}>) -> Result<#{Error}, #{SerdeError}>", + "pub fn parse_generic_error(response: &#{Response}<#{Bytes}>) -> Result<#{Error}, #{JsonError}>", "Response" to RuntimeType.http.member("Response"), "Bytes" to RuntimeType.Bytes, "Error" to RuntimeType.GenericError(runtimeConfig), - "SerdeError" to RuntimeType.SerdeJson("Error") + "JsonError" to CargoDependency.smithyJson(runtimeConfig).asType().member("deserialize::Error") ) { - rustTemplate( - """ - let body = #{sj}::from_slice(response.body().as_ref()) - .unwrap_or_else(|_|#{sj}::json!({})); - Ok(#{aws_json_errors}::parse_generic_error(&response, &body)) - """, - "sj" to RuntimeType.serdeJson, "aws_json_errors" to awsJsonErrors + rust( + "#T::parse_generic_error(response)", + RuntimeType.jsonErrors(runtimeConfig) ) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/SerdeJsonParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/SerdeJsonParserGenerator.kt deleted file mode 100644 index eeeda3a900..0000000000 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/SerdeJsonParserGenerator.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.rust.codegen.smithy.protocols.parse - -import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.model.shapes.UnionShape -import software.amazon.smithy.rust.codegen.rustlang.conditionalBlock -import software.amazon.smithy.rust.codegen.rustlang.rust -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.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig -import software.amazon.smithy.rust.codegen.smithy.generators.builderSymbol -import software.amazon.smithy.rust.codegen.smithy.generators.setterName -import software.amazon.smithy.rust.codegen.smithy.isOptional -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait -import software.amazon.smithy.rust.codegen.util.expectTrait -import software.amazon.smithy.rust.codegen.util.outputShape -import software.amazon.smithy.rust.codegen.util.toSnakeCase - -class SerdeJsonParserGenerator(protocolConfig: ProtocolConfig) : StructuredDataParserGenerator { - private val model = protocolConfig.model - private val symbolProvider = protocolConfig.symbolProvider - private val runtimeConfig = protocolConfig.runtimeConfig - private val codegenScope = arrayOf("Error" to RuntimeType.SerdeJson("Error"), "serde_json" to RuntimeType.serdeJson) - - override fun payloadParser(member: MemberShape): RuntimeType { - val shape = model.expectShape(member.target) - check(shape is UnionShape || shape is StructureShape) { "payload parser should only be used on structures & unions" } - val fnName = - "parse_payload_" + shape.id.name.toString().toSnakeCase() + member.container.name.toString().toSnakeCase() - return RuntimeType.forInlineFun(fnName, "json_deser") { - it.rustTemplate( - """ - pub fn $fnName(inp: &[u8]) -> std::result::Result<#{Shape}, #{Error}> { - #{serde_json}::from_slice(inp) - }""", - *codegenScope, "Shape" to symbolProvider.toSymbol(shape) - ) - } - } - - override fun operationParser(operationShape: OperationShape): RuntimeType? { - val outputShape = operationShape.outputShape(model) - val fnName = operationShape.id.name.toString().toSnakeCase() + "_deser_operation" - val bodyId = outputShape.expectTrait().body - val bodyShape = bodyId?.let { model.expectShape(bodyId, StructureShape::class.java) } ?: return null - val body = symbolProvider.toSymbol(bodyShape) - if (bodyShape.members().isEmpty()) { - return null - } - - return RuntimeType.forInlineFun(fnName, "json_deser") { - it.rustBlockTemplate( - "pub fn $fnName(input: &[u8], mut builder: #{Builder}) -> std::result::Result<#{Builder}, #{Error}>", - "Builder" to outputShape.builderSymbol(symbolProvider), - *codegenScope - ) { - rustTemplate( - """ - let parsed_body: #{BodyShape} = if input.is_empty() { - // To enable JSON parsing to succeed, replace an empty body - // with an empty JSON body. If a member was required, it will fail slightly later - // during the operation construction phase when a required field was missing. - #{serde_json}::from_slice(b"{}")? - } else { - #{serde_json}::from_slice(input)? - }; - """, - "BodyShape" to body, *codegenScope - ) - bodyShape.members().forEach { member -> - withBlock("builder = builder.${member.setterName()}(", ");") { - conditionalBlock("Some(", ")", !symbolProvider.toSymbol(member).isOptional()) { - rust("parsed_body.${symbolProvider.toMemberName(member)}") - } - } - } - rust("Ok(builder)") - } - } - } - - override fun errorParser(errorShape: StructureShape): RuntimeType? { - if (errorShape.members().isEmpty()) { - return null - } - val fnName = errorShape.id.name.toString().toSnakeCase() - return RuntimeType.forInlineFun(fnName, "json_deser") { - it.rustBlockTemplate( - "pub fn $fnName(input: &[u8], mut builder: #{Builder}) -> std::result::Result<#{Builder}, #{Error}>", - "Builder" to errorShape.builderSymbol(symbolProvider), - *codegenScope - ) { - rustTemplate( - """ - let parsed_body: #{BodyShape} = if input.is_empty() { - // To enable JSON parsing to succeed, replace an empty body - // with an empty JSON body. If a member was required, it will fail slightly later - // during the operation construction phase. - #{serde_json}::from_slice(b"{}")? - } else { - #{serde_json}::from_slice(input)? - }; - """, - "BodyShape" to symbolProvider.toSymbol(errorShape), - *codegenScope - ) - errorShape.members().forEach { member -> - withBlock("builder = builder.${member.setterName()}(", ");") { - conditionalBlock("Some(", ")", !symbolProvider.toSymbol(member).isOptional()) { - rust("parsed_body.${symbolProvider.toMemberName(member)}") - } - } - } - rust("Ok(builder)") - } - } - } - - override fun documentParser(operationShape: OperationShape): RuntimeType { - val fnName = "parse_document" - return RuntimeType.forInlineFun(fnName, "json_deser") { - it.rustTemplate( - """ - pub fn $fnName(inp: &[u8]) -> Result<#{Document}, #{Error}> { - #{serde_json}::from_slice::<#{doc_json}::DeserDoc>(inp).map(|d|d.0) - } - """, - *codegenScope, "Document" to RuntimeType.Document(runtimeConfig), "doc_json" to RuntimeType.DocJson - ) - } - } -} 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 f49df6b9c7..5d64ba3ce0 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 @@ -80,7 +80,7 @@ abstract class QuerySerializerGenerator(protocolConfig: ProtocolConfig) : Struct protected val symbolProvider = protocolConfig.symbolProvider protected val runtimeConfig = protocolConfig.runtimeConfig private val serviceShape = protocolConfig.serviceShape - private val serializerError = RuntimeType.SerdeJson("error::Error") + private val serializerError = RuntimeType.Infallible private val smithyTypes = CargoDependency.SmithyTypes(runtimeConfig).asType() private val smithyQuery = CargoDependency.smithyQuery(runtimeConfig).asType() private val serdeUtil = SerializerUtil(model) diff --git a/rust-runtime/inlineable/Cargo.toml b/rust-runtime/inlineable/Cargo.toml index 50852d6760..63f402c0d3 100644 --- a/rust-runtime/inlineable/Cargo.toml +++ b/rust-runtime/inlineable/Cargo.toml @@ -9,8 +9,7 @@ are to allow this crate to be compilable and testable in isolation, no client co """ [dependencies] -"serde" = { version = "1", features = ["derive"] } -"serde_json" = "1" +"bytes" = "1" "http" = "0.2.1" "smithy-types" = { version = "0.0.1", path = "../smithy-types" } "smithy-http" = { version = "0.0.1", path = "../smithy-http" } diff --git a/rust-runtime/inlineable/src/aws_json_errors.rs b/rust-runtime/inlineable/src/aws_json_errors.rs deleted file mode 100644 index c3b21cd9de..0000000000 --- a/rust-runtime/inlineable/src/aws_json_errors.rs +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use http::header::ToStrError; - -// currently only used by AwsJson -#[allow(unused)] -pub fn is_error(response: &http::Response) -> bool { - !response.status().is_success() -} - -fn error_type_from_header(response: &http::Response) -> Result, ToStrError> { - response - .headers() - .get("X-Amzn-Errortype") - .map(|v| v.to_str()) - .transpose() -} - -fn error_type_from_body(body: &serde_json::Value) -> Option<&str> { - body.as_object() - .and_then(|b: &serde_json::Map| { - b.get("code").or_else(|| b.get("__type")) - }) - .and_then(|v| v.as_str()) -} - -fn sanitize_error_code(error_code: &str) -> &str { - // Trim a trailing URL from the error code, beginning with a `:` - let error_code = match error_code.find(':') { - Some(idx) => &error_code[..idx], - None => &error_code, - }; - - // Trim a prefixing namespace from the error code, beginning with a `#` - match error_code.find('#') { - Some(idx) => &error_code[idx + 1..], - None => &error_code, - } -} - -pub fn parse_generic_error( - response: &http::Response, - body: &serde_json::Value, -) -> smithy_types::Error { - let mut err_builder = smithy_types::Error::builder(); - let code = error_type_from_header(&response) - .unwrap_or(Some("header was not valid UTF-8")) - .or_else(|| error_type_from_body(body)) - .map(|s| sanitize_error_code(s)); - if let Some(code) = code { - err_builder.code(code); - } - let message = body - .get("message") - .or_else(|| body.get("Message")) - .or_else(|| body.get("errorMessage")) - .and_then(|v| v.as_str()); - if let Some(message) = message { - err_builder.message(message); - } - let request_id = response - .headers() - .get("X-Amzn-Requestid") - .and_then(|v| v.to_str().ok()); - if let Some(request_id) = request_id { - err_builder.request_id(request_id); - } - err_builder.build() -} - -#[cfg(test)] -mod test { - use crate::aws_json_errors::{error_type_from_body, parse_generic_error, sanitize_error_code}; - use serde_json::json; - use smithy_types::Error; - - #[test] - fn generic_error() { - let response = http::Response::builder() - .header("X-Amzn-Requestid", "1234") - .body(json!({ - "__type": "FooError", - "message": "Go to foo" - })) - .unwrap(); - assert_eq!( - parse_generic_error(&response, response.body()), - Error::builder() - .code("FooError") - .message("Go to foo") - .request_id("1234") - .build() - ) - } - - #[test] - fn error_type() { - let error_body = json!({ - "__type": "FooError" - }); - assert_eq!(error_type_from_body(&error_body), Some("FooError")); - } - - #[test] - fn code_takes_priority() { - let error_body = json!({ - "__type": "FooError", - "code": "BarError" - }); - assert_eq!(error_type_from_body(&error_body), Some("BarError")); - } - - #[test] - fn sanitize_namespace_and_url() { - assert_eq!( - sanitize_error_code("aws.protocoltests.restjson#FooError:http://internal.amazon.com/coral/com.amazon.coral.validate/"), - "FooError"); - } - - #[test] - fn sanitize_noop() { - assert_eq!(sanitize_error_code("FooError"), "FooError"); - } - - #[test] - fn sanitize_url() { - assert_eq!( - sanitize_error_code( - "FooError:http://internal.amazon.com/coral/com.amazon.coral.validate/" - ), - "FooError" - ); - } - - #[test] - fn sanitize_namespace() { - assert_eq!( - sanitize_error_code("aws.protocoltests.restjson#FooError"), - "FooError" - ); - } - - // services like lambda use an alternate `Message` instead of `message` - #[test] - fn alternative_error_message_names() { - let response = http::Response::builder() - .header("x-amzn-errortype", "ResourceNotFoundException") - .body(json!({ - "Type": "User", - "Message": "Functions from 'us-west-2' are not reachable from us-east-1" - })) - .unwrap(); - assert_eq!( - parse_generic_error(&response, response.body()), - Error::builder() - .code("ResourceNotFoundException") - .message("Functions from 'us-west-2' are not reachable from us-east-1") - .build() - ); - } -} diff --git a/rust-runtime/inlineable/src/blob_serde.rs b/rust-runtime/inlineable/src/blob_serde.rs deleted file mode 100644 index f07248f52b..0000000000 --- a/rust-runtime/inlineable/src/blob_serde.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use serde::de::{Error, Unexpected}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use smithy_types::{base64, Blob}; - -pub struct BlobSer<'a>(pub &'a Blob); - -impl Serialize for BlobSer<'_> { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, - { - serializer.serialize_str(base64::encode(self.0.as_ref()).as_str()) - } -} - -pub struct BlobDeser(pub Blob); - -impl<'de> Deserialize<'de> for BlobDeser { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let data = <&str>::deserialize(deserializer)?; - let bytes = base64::decode(data) - .map_err(|_| D::Error::invalid_value(Unexpected::Str(data), &"valid base64"))?; - Ok(BlobDeser(Blob::new(bytes))) - } -} diff --git a/rust-runtime/inlineable/src/doc_json.rs b/rust-runtime/inlineable/src/doc_json.rs deleted file mode 100644 index 5563fbf6f5..0000000000 --- a/rust-runtime/inlineable/src/doc_json.rs +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use serde::de::{Error, MapAccess, SeqAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; -use smithy_types::{Document, Number}; -use std::collections::HashMap; -use std::fmt; -use std::fmt::Formatter; - -#[allow(unused)] -pub fn json_to_doc(json: Value) -> Document { - match json { - Value::Null => Document::Null, - Value::Bool(b) => Document::Bool(b), - Value::Number(num) => Document::Number(serde_num_to_num(&num)), - Value::String(str) => Document::String(str), - Value::Array(arr) => Document::Array(arr.into_iter().map(json_to_doc).collect()), - Value::Object(map) => { - Document::Object(map.into_iter().map(|(k, v)| (k, json_to_doc(v))).collect()) - } - } -} - -pub struct SerDoc<'a>(pub &'a Document); -pub struct DeserDoc(pub Document); - -impl Serialize for SerDoc<'_> { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, - { - let doc = &self.0; - Ok(match doc { - Document::Object(obj) => { - serializer.collect_map(obj.iter().map(|(k, v)| (k, SerDoc(v))))? - } - Document::Array(arr) => serializer.collect_seq(arr.iter().map(|doc| SerDoc(doc)))?, - Document::Number(Number::PosInt(n)) => serializer.serialize_u64(*n)?, - Document::Number(Number::NegInt(n)) => serializer.serialize_i64(*n)?, - Document::Number(Number::Float(n)) => serializer.serialize_f64(*n)?, - Document::String(string) => serializer.serialize_str(&string)?, - Document::Bool(bool) => serializer.serialize_bool(*bool)?, - Document::Null => serializer.serialize_none()?, - }) - } -} - -struct DocVisitor; -impl<'de> Visitor<'de> for DocVisitor { - type Value = Document; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - write!(formatter, "Expecting a JSON-like document") - } - - fn visit_bool(self, v: bool) -> Result - where - E: Error, - { - Ok(Document::Bool(v)) - } - - fn visit_i8(self, v: i8) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_i16(self, v: i16) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_i32(self, v: i32) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_i64(self, v: i64) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_u8(self, v: u8) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_u16(self, v: u16) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_u32(self, v: u32) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_u64(self, v: u64) -> Result - where - E: Error, - { - Ok(Document::Number(serde_num_to_num( - &serde_json::Number::from(v), - ))) - } - - fn visit_f32(self, v: f32) -> Result - where - E: Error, - { - Ok(Document::Number(Number::Float(v as _))) - } - - fn visit_f64(self, v: f64) -> Result - where - E: Error, - { - Ok(Document::Number(Number::Float(v as _))) - } - - fn visit_char(self, v: char) -> Result - where - E: Error, - { - Ok(Document::String(v.to_string())) - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - Ok(Document::String(v.to_string())) - } - - fn visit_seq(self, mut seq: A) -> Result>::Error> - where - A: SeqAccess<'de>, - { - let mut out: Vec = vec![]; - while let Some(next) = seq.next_element::()? { - out.push(next.0); - } - Ok(Document::Array(out)) - } - - fn visit_map(self, mut map: A) -> Result>::Error> - where - A: MapAccess<'de>, - { - let mut out: HashMap = HashMap::new(); - while let Some((k, v)) = map.next_entry::()? { - out.insert(k, v.0); - } - Ok(Document::Object(out)) - } -} - -impl<'de> Deserialize<'de> for DeserDoc { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - Ok(DeserDoc(deserializer.deserialize_any(DocVisitor)?)) - } -} - -fn serde_num_to_num(number: &serde_json::Number) -> smithy_types::Number { - if number.is_f64() { - smithy_types::Number::Float(number.as_f64().unwrap()) - } else if number.is_i64() { - smithy_types::Number::NegInt(number.as_i64().unwrap()) - } else if number.is_u64() { - smithy_types::Number::PosInt(number.as_u64().unwrap()) - } else { - panic!("Serde nums should be either f64, i64 or u64") - } -} diff --git a/rust-runtime/inlineable/src/instant_epoch.rs b/rust-runtime/inlineable/src/instant_epoch.rs deleted file mode 100644 index 34a6f880bb..0000000000 --- a/rust-runtime/inlineable/src/instant_epoch.rs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use smithy_types::Instant; - -pub struct InstantEpoch(pub Instant); - -impl Serialize for InstantEpoch { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, - { - if self.0.has_nanos() { - serializer.serialize_f64(self.0.epoch_fractional_seconds()) - } else { - serializer.serialize_i64(self.0.epoch_seconds()) - } - } -} - -impl<'de> Deserialize<'de> for InstantEpoch { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let ts = f64::deserialize(deserializer)?; - Ok(InstantEpoch(Instant::from_f64(ts))) - } -} diff --git a/rust-runtime/inlineable/src/instant_httpdate.rs b/rust-runtime/inlineable/src/instant_httpdate.rs deleted file mode 100644 index ce2ce1f597..0000000000 --- a/rust-runtime/inlineable/src/instant_httpdate.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use serde::de::{Error, Unexpected}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use smithy_types::instant::Format; -use smithy_types::Instant; - -pub struct InstantHttpDate(pub Instant); - -impl Serialize for InstantHttpDate { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.0.fmt(Format::HttpDate)) - } -} - -impl<'de> Deserialize<'de> for InstantHttpDate { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let ts = <&str>::deserialize(deserializer)?; - Ok(InstantHttpDate( - Instant::from_str(ts, Format::HttpDate) - .map_err(|_| D::Error::invalid_value(Unexpected::Str(ts), &"valid http date"))?, - )) - } -} diff --git a/rust-runtime/inlineable/src/instant_iso8601.rs b/rust-runtime/inlineable/src/instant_iso8601.rs deleted file mode 100644 index 03222af3a5..0000000000 --- a/rust-runtime/inlineable/src/instant_iso8601.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use serde::de::{Error, Unexpected}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use smithy_types::instant::Format; -use smithy_types::Instant; - -pub struct InstantIso8601(pub Instant); - -impl Serialize for InstantIso8601 { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.0.fmt(Format::DateTime)) - } -} - -impl<'de> Deserialize<'de> for InstantIso8601 { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let ts = <&str>::deserialize(deserializer)?; - Ok(InstantIso8601( - Instant::from_str(ts, Format::DateTime) - .map_err(|_| D::Error::invalid_value(Unexpected::Str(ts), &"valid iso8601 date"))?, - )) - } -} diff --git a/rust-runtime/inlineable/src/json_errors.rs b/rust-runtime/inlineable/src/json_errors.rs new file mode 100644 index 0000000000..4cc6f233f7 --- /dev/null +++ b/rust-runtime/inlineable/src/json_errors.rs @@ -0,0 +1,218 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use bytes::Bytes; +use http::header::ToStrError; +use http::Response; +use smithy_json::deserialize::token::skip_value; +use smithy_json::deserialize::{json_token_iter, Error as DeserializeError, Token}; +use smithy_types::Error as SmithyError; +use std::borrow::Cow; + +// currently only used by AwsJson +#[allow(unused)] +pub fn is_error(response: &http::Response) -> bool { + !response.status().is_success() +} + +fn error_type_from_header(response: &http::Response) -> Result, ToStrError> { + response + .headers() + .get("X-Amzn-Errortype") + .map(|v| v.to_str()) + .transpose() +} + +fn sanitize_error_code(error_code: &str) -> &str { + // Trim a trailing URL from the error code, beginning with a `:` + let error_code = match error_code.find(':') { + Some(idx) => &error_code[..idx], + None => &error_code, + }; + + // Trim a prefixing namespace from the error code, beginning with a `#` + match error_code.find('#') { + Some(idx) => &error_code[idx + 1..], + None => &error_code, + } +} + +fn request_id(response: &Response) -> Option<&str> { + response + .headers() + .get("X-Amzn-Requestid") + .and_then(|v| v.to_str().ok()) +} + +struct ErrorBody<'a> { + code: Option>, + message: Option>, +} + +fn parse_error_body(bytes: &[u8]) -> Result { + let mut tokens = json_token_iter(bytes).peekable(); + let (mut typ, mut code, mut message) = (None, None, None); + if let Some(Token::StartObject { .. }) = tokens.next().transpose()? { + loop { + match tokens.next().transpose()? { + Some(Token::EndObject { .. }) => break, + Some(Token::ObjectKey { key, .. }) => { + if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() { + match key.as_escaped_str() { + "code" => code = Some(value.to_unescaped()?), + "__type" => typ = Some(value.to_unescaped()?), + "message" | "Message" | "errorMessage" => { + message = Some(value.to_unescaped()?) + } + _ => {} + } + } + skip_value(&mut tokens)?; + } + _ => { + return Err(DeserializeError::custom( + "expected object key or end object", + )) + } + } + } + if tokens.next().is_some() { + return Err(DeserializeError::custom( + "found more JSON tokens after completing parsing", + )); + } + } + Ok(ErrorBody { + code: code.or(typ), + message, + }) +} + +pub fn parse_generic_error(response: &Response) -> Result { + let ErrorBody { code, message } = parse_error_body(response.body().as_ref())?; + + let mut err_builder = SmithyError::builder(); + if let Some(code) = error_type_from_header(response) + .map_err(|_| DeserializeError::custom("X-Amzn-Errortype header was not valid UTF-8"))? + .or_else(|| code.as_deref()) + .map(|c| sanitize_error_code(c)) + { + err_builder.code(code); + } + if let Some(message) = message { + err_builder.message(message); + } + if let Some(request_id) = request_id(response) { + err_builder.request_id(request_id); + } + Ok(err_builder.build()) +} + +#[cfg(test)] +mod test { + use crate::json_errors::{parse_error_body, parse_generic_error, sanitize_error_code}; + use bytes::Bytes; + use smithy_types::Error; + use std::borrow::Cow; + + #[test] + fn generic_error() { + let response = http::Response::builder() + .header("X-Amzn-Requestid", "1234") + .body(Bytes::from_static( + br#"{ "__type": "FooError", "message": "Go to foo" }"#, + )) + .unwrap(); + assert_eq!( + parse_generic_error(&response).unwrap(), + Error::builder() + .code("FooError") + .message("Go to foo") + .request_id("1234") + .build() + ) + } + + #[test] + fn error_type() { + assert_eq!( + Some(Cow::Borrowed("FooError")), + parse_error_body(br#"{ "__type": "FooError" }"#) + .unwrap() + .code + ); + } + + #[test] + fn code_takes_priority() { + assert_eq!( + Some(Cow::Borrowed("BarError")), + parse_error_body(br#"{ "code": "BarError", "__type": "FooError" }"#) + .unwrap() + .code + ); + } + + #[test] + fn ignore_unrecognized_fields() { + assert_eq!( + Some(Cow::Borrowed("FooError")), + parse_error_body(br#"{ "__type": "FooError", "asdf": 5, "fdsa": {}, "foo": "1" }"#) + .unwrap() + .code + ); + } + + #[test] + fn sanitize_namespace_and_url() { + assert_eq!( + sanitize_error_code("aws.protocoltests.restjson#FooError:http://internal.amazon.com/coral/com.amazon.coral.validate/"), + "FooError"); + } + + #[test] + fn sanitize_noop() { + assert_eq!(sanitize_error_code("FooError"), "FooError"); + } + + #[test] + fn sanitize_url() { + assert_eq!( + sanitize_error_code( + "FooError:http://internal.amazon.com/coral/com.amazon.coral.validate/" + ), + "FooError" + ); + } + + #[test] + fn sanitize_namespace() { + assert_eq!( + sanitize_error_code("aws.protocoltests.restjson#FooError"), + "FooError" + ); + } + + // services like lambda use an alternate `Message` instead of `message` + #[test] + fn alternative_error_message_names() { + let response = http::Response::builder() + .header("x-amzn-errortype", "ResourceNotFoundException") + .body(Bytes::from_static( + br#"{ + "Type": "User", + "Message": "Functions from 'us-west-2' are not reachable from us-east-1" + }"#, + )) + .unwrap(); + assert_eq!( + parse_generic_error(&response).unwrap(), + Error::builder() + .code("ResourceNotFoundException") + .message("Functions from 'us-west-2' are not reachable from us-east-1") + .build() + ); + } +} diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs index 743203fe7f..da44ce3ab1 100644 --- a/rust-runtime/inlineable/src/lib.rs +++ b/rust-runtime/inlineable/src/lib.rs @@ -3,18 +3,12 @@ * SPDX-License-Identifier: Apache-2.0. */ -#[allow(dead_code)] -mod aws_json_errors; -mod blob_serde; -#[allow(dead_code)] -mod doc_json; #[allow(dead_code)] mod ec2_query_errors; #[allow(dead_code)] mod idempotency_token; -mod instant_epoch; -mod instant_httpdate; -mod instant_iso8601; +#[allow(dead_code)] +mod json_errors; #[allow(unused)] mod rest_xml_unwrapped_errors; #[allow(unused)] @@ -24,27 +18,11 @@ mod rest_xml_wrapped_errors; // requiring a proptest dependency #[cfg(test)] mod test { - use crate::doc_json::SerDoc; use crate::idempotency_token; use crate::idempotency_token::uuid_v4; use proptest::prelude::*; - use proptest::std_facade::HashMap; - use smithy_types::Document; - use smithy_types::Number; use std::sync::Mutex; - #[test] - fn nan_floats_serialize_null() { - let mut map = HashMap::new(); - map.insert("num".to_string(), Document::Number(Number::PosInt(45))); - map.insert("nan".to_string(), Document::Number(Number::Float(f64::NAN))); - let doc = Document::Object(map); - assert_eq!( - serde_json::to_value(&SerDoc(&doc)).unwrap(), - serde_json::json!({"num":45,"nan":null}) - ); - } - #[test] fn test_uuid() { assert_eq!(uuid_v4(0), "00000000-0000-4000-8000-000000000000"); From a09d2305b8086b560a2fcfdf249167e4a064b476 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 22 Jun 2021 16:40:28 -0700 Subject: [PATCH 2/4] Fix CI --- .../rustsdk/IntegrationTestDependencies.kt | 2 + .../CustomSerializerGeneratorTest.kt | 113 ------------------ 2 files changed, 2 insertions(+), 113 deletions(-) delete mode 100644 codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGeneratorTest.kt diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt index dd3cd56573..83694a60e2 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt @@ -50,6 +50,7 @@ class IntegrationTestDependencies( LibRsSection.Body -> writable { if (hasTests) { addDependency(runtimeConfig.awsHyper().copy(scope = DependencyScope.Dev)) + addDependency(SerdeJson) addDependency(Tokio) } if (hasBenches) { @@ -61,5 +62,6 @@ class IntegrationTestDependencies( } val Criterion = CargoDependency("criterion", CratesIo("0.3"), scope = DependencyScope.Dev) +val SerdeJson = CargoDependency("serde_json", CratesIo("1"), features = emptyList()) val Tokio = CargoDependency("tokio", CratesIo("1"), features = listOf("macros", "test-util"), scope = DependencyScope.Dev) fun RuntimeConfig.awsHyper() = awsRuntimeDependency("aws-hyper", features = listOf("test-util")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGeneratorTest.kt deleted file mode 100644 index 29c4b855ef..0000000000 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/CustomSerializerGeneratorTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.rust.codegen.smithy.protocols - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.rust.codegen.rustlang.rust -import software.amazon.smithy.rust.codegen.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.smithy.WrappingSymbolProvider -import software.amazon.smithy.rust.codegen.smithy.makeRustBoxed -import software.amazon.smithy.rust.codegen.testutil.TestWorkspace -import software.amazon.smithy.rust.codegen.testutil.asSmithyModel -import software.amazon.smithy.rust.codegen.testutil.compileAndTest -import software.amazon.smithy.rust.codegen.testutil.testSymbolProvider -import software.amazon.smithy.rust.codegen.util.lookup - -internal class CustomSerializerGeneratorTest { - private val model = """ - namespace test - structure S { - timestamp: Timestamp, - string: String, - blob: Blob, - blobList: BlobList, - sparseBlobList: SparseBlobList - } - list BlobList { - member: Blob - } - @sparse - list SparseBlobList { - member: Blob - } - - map Nested { - key: String, - value: BlobList - } - - structure TopLevel { - member: Nested - } - """.asSmithyModel() - private val provider = testSymbolProvider(model) - - @Test - fun `generate correct function names`() { - val serializerBuilder = CustomSerializerGenerator(provider, model, TimestampFormatTrait.Format.EPOCH_SECONDS) - serializerBuilder.deserializerFor(model.lookup("test#S\$blob"))!!.name shouldBe "stdoptionoptionsmithytypesblob_deser" - serializerBuilder.deserializerFor(model.lookup("test#S\$string")) shouldBe null - } - - private fun checkDeserializer(builder: CustomSerializerGenerator, shapeId: String) { - val symbol = builder.deserializerFor(model.lookup(shapeId)) - check(symbol != null) { "For $shapeId, expected a custom deserializer" } - checkSymbol(symbol) - } - - private fun checkSymbol(symbol: RuntimeType) { - val writer = TestWorkspace.testProject(provider) - writer.lib { - it.rust( - """ - fn foo() { - // commented out so that we generate the import & inject the serializer - // but I don't want to deal with getting the argument to compile - // let _ = #T(); - } - """, - symbol - ) - } - println("file:///${writer.baseDir}/src/serde_util.rs") - writer.compileAndTest() - } - - @ParameterizedTest(name = "{index} ==> ''{0}''") - @CsvSource( - "timestamp", - "blob", - "blobList", - "sparseBlobList" - ) - fun `generate basic deserializers that compile`(memberName: String) { - val serializerBuilder = CustomSerializerGenerator(provider, model, TimestampFormatTrait.Format.EPOCH_SECONDS) - checkDeserializer(serializerBuilder, "test#S\$$memberName") - } - - @Test - fun `support deeply nested structures`() { - val serializerBuilder = CustomSerializerGenerator(provider, model, TimestampFormatTrait.Format.EPOCH_SECONDS) - checkDeserializer(serializerBuilder, "test#TopLevel\$member") - } - - @Test - fun `generate deserializers for boxed shapes`() { - val boxingProvider = object : WrappingSymbolProvider(provider) { - override fun toSymbol(shape: Shape): Symbol { - return provider.toSymbol(shape).makeRustBoxed() - } - } - val serializerBuilder = CustomSerializerGenerator(boxingProvider, model, TimestampFormatTrait.Format.EPOCH_SECONDS) - checkDeserializer(serializerBuilder, "test#S\$timestamp") - } -} From fc836b1d9569473066f105e03c766b2b89b728ee Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 22 Jun 2021 16:51:11 -0700 Subject: [PATCH 3/4] CR feedback --- .../rust/codegen/smithy/protocols/AwsJson.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 3380eb715f..2fbf82a263 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 @@ -148,7 +148,7 @@ class AwsJson( override val defaultTimestampFormat: TimestampFormatTrait.Format = TimestampFormatTrait.Format.EPOCH_SECONDS override fun additionalHeaders(operationShape: OperationShape): List> = - listOf("X-Amz-Target" to "${protocolConfig.serviceShape.id.name}.${operationShape.id.name}") + listOf("x-amz-target" to "${protocolConfig.serviceShape.id.name}.${operationShape.id.name}") override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator = JsonParserGenerator(protocolConfig, httpBindingResolver) @@ -158,18 +158,18 @@ class AwsJson( override fun parseGenericError(operationShape: OperationShape): RuntimeType { return RuntimeType.forInlineFun("parse_generic_error", "json_deser") { - it.rustBlockTemplate( - "pub fn parse_generic_error(response: &#{Response}<#{Bytes}>) -> Result<#{Error}, #{JsonError}>", + it.rustTemplate( + """ + pub fn parse_generic_error(response: &#{Response}<#{Bytes}>) -> Result<#{Error}, #{JsonError}> { + #{json_errors}::parse_generic_error(response) + } + """, "Response" to RuntimeType.http.member("Response"), "Bytes" to RuntimeType.Bytes, "Error" to RuntimeType.GenericError(runtimeConfig), - "JsonError" to CargoDependency.smithyJson(runtimeConfig).asType().member("deserialize::Error") - ) { - rust( - "#T::parse_generic_error(response)", - RuntimeType.jsonErrors(runtimeConfig) - ) - } + "JsonError" to CargoDependency.smithyJson(runtimeConfig).asType().member("deserialize::Error"), + "json_errors" to RuntimeType.jsonErrors(runtimeConfig) + ) } } } From 531b3746fd19f3f85c21b2f0632a08c5e1553220 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 22 Jun 2021 17:23:45 -0700 Subject: [PATCH 4/4] Stop generating synthetic input/output bodies --- .../rust/codegen/smithy/SymbolVisitor.kt | 4 -- .../smithy/generators/ServiceGenerator.kt | 32 ---------- .../rust/codegen/smithy/protocols/AwsJson.kt | 6 +- .../rust/codegen/smithy/protocols/AwsQuery.kt | 5 +- .../rust/codegen/smithy/protocols/Ec2Query.kt | 5 +- .../rust/codegen/smithy/protocols/RestJson.kt | 27 +------- .../rust/codegen/smithy/protocols/RestXml.kt | 5 +- .../smithy/traits/SyntheticInputTrait.kt | 17 +---- .../smithy/traits/SyntheticOutputTrait.kt | 5 +- .../transformers/OperationNormalizer.kt | 50 +++------------ .../http/RequestBindingGeneratorTest.kt | 5 +- .../http/ResponseBindingGeneratorTest.kt | 5 +- .../StreamingShapeSymbolProviderTest.kt | 6 +- .../HttpProtocolTestGeneratorTest.kt | 63 +++++++++---------- .../parse/AwsQueryParserGeneratorTest.kt | 5 +- .../parse/Ec2QueryParserGeneratorTest.kt | 5 +- .../parse/JsonParserGeneratorTest.kt | 6 +- .../XmlBindingTraitParserGeneratorTest.kt | 2 +- .../AwsQuerySerializerGeneratorTest.kt | 7 +-- .../Ec2QuerySerializerGeneratorTest.kt | 7 +-- .../serialize/JsonSerializerGeneratorTest.kt | 7 +-- .../XmlBindingTraitSerializerGeneratorTest.kt | 7 +-- .../transformers/OperationNormalizerTest.kt | 53 ++-------------- 23 files changed, 65 insertions(+), 269 deletions(-) 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 74231cb553..6f892f3279 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 @@ -40,8 +40,6 @@ import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.model.traits.HttpLabelTrait import software.amazon.smithy.rust.codegen.rustlang.RustType import software.amazon.smithy.rust.codegen.rustlang.stripOuter -import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait -import software.amazon.smithy.rust.codegen.smithy.traits.OutputBodyTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait import software.amazon.smithy.rust.codegen.util.hasTrait @@ -257,7 +255,6 @@ class SymbolVisitor( val isError = shape.hasTrait() val isInput = shape.hasTrait() val isOutput = shape.hasTrait() - val isBody = shape.hasTrait() || shape.hasTrait() val name = StringUtils.capitalize(shape.contextName()).letIf(isError && config.codegenConfig.renameExceptions) { // TODO: Do we want to do this? // https://github.com/awslabs/smithy-rs/issues/77 @@ -268,7 +265,6 @@ class SymbolVisitor( isError -> builder.locatedIn(Errors) isInput -> builder.locatedIn(Inputs) isOutput -> builder.locatedIn(Outputs) - isBody -> builder.locatedIn(Serializers) else -> builder.locatedIn(Models) }.build() } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ServiceGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ServiceGenerator.kt index cd0b236fba..65f958a33c 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ServiceGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ServiceGenerator.kt @@ -6,17 +6,12 @@ package software.amazon.smithy.rust.codegen.smithy.generators import software.amazon.smithy.model.knowledge.TopDownIndex -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.rustlang.RustModule import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfigGenerator import software.amazon.smithy.rust.codegen.smithy.generators.error.CombinedErrorGenerator import software.amazon.smithy.rust.codegen.smithy.generators.error.TopLevelErrorGenerator -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait -import software.amazon.smithy.rust.codegen.util.expectTrait import software.amazon.smithy.rust.codegen.util.inputShape class ServiceGenerator( @@ -48,7 +43,6 @@ class ServiceGenerator( } TopLevelErrorGenerator(config, operations).render(rustCrate) - renderBodies(operations) rustCrate.withModule(RustModule.Config) { writer -> ServiceConfigGenerator.withBaseBehavior( @@ -61,30 +55,4 @@ class ServiceGenerator( it.write("pub use config::Config;") } } - - private fun renderBodies(operations: List) { - val inputBodies = operations.map { config.model.expectShape(it.input.get()) }.map { - it.expectTrait() - }.mapNotNull { // mapNotNull is flatMap but for null `map { it }.filter { it != null }` - it.body - }.map { // Lookup the Body structure by its id - config.model.expectShape(it, StructureShape::class.java) - } - val outputBodies = operations.map { config.model.expectShape(it.output.get()) }.map { - it.expectTrait() - }.mapNotNull { // mapNotNull is flatMap but for null `map { it }.filter { it != null }` - it.body - }.map { // Lookup the Body structure by its id - config.model.expectShape(it, StructureShape::class.java) - } - (inputBodies + outputBodies).map { body -> - // The body symbol controls its location, usually in the serializer module - rustCrate.useShapeWriter(body) { writer -> - with(config) { - // Generate a body via the structure generator - StructureGenerator(model, symbolProvider, writer, body).render() - } - } - } - } } 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 2fbf82a263..19a4f668ef 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 @@ -14,7 +14,6 @@ import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.asType -import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.smithy.RuntimeType @@ -57,10 +56,7 @@ class AwsJsonFactory(private val version: AwsJsonVersion) : ProtocolGeneratorFac override fun transformModel(model: Model): Model { // For AwsJson10, the body matches 1:1 with the input - return OperationNormalizer(model).transformModel( - inputBodyFactory = shapeIfHasMembers, - outputBodyFactory = shapeIfHasMembers - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport = ProtocolSupport( diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt index 1f22f74ec7..5e7b8be2ed 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsQuery.kt @@ -30,10 +30,7 @@ class AwsQueryFactory : ProtocolGeneratorFactory { HttpBoundProtocolGenerator(protocolConfig, AwsQueryProtocol(protocolConfig)) override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt index 9c2f23cc6e..f7cc2764ce 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Ec2Query.kt @@ -30,10 +30,7 @@ class Ec2QueryFactory : ProtocolGeneratorFactory { HttpBoundProtocolGenerator(protocolConfig, Ec2QueryProtocol(protocolConfig)) override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt index bdbe3b8625..b21c510fe5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt @@ -6,10 +6,7 @@ package software.amazon.smithy.rust.codegen.smithy.protocols import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.HttpBinding -import software.amazon.smithy.model.knowledge.HttpBindingIndex import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.asType @@ -31,30 +28,8 @@ class RestJsonFactory : ProtocolGeneratorFactory { protocolConfig: ProtocolConfig ): HttpBoundProtocolGenerator = HttpBoundProtocolGenerator(protocolConfig, RestJson(protocolConfig)) - /** Create a synthetic awsJsonInputBody if specified - * A body is created if any member of [shape] is bound to the `DOCUMENT` section of the `bindings. - */ - private fun restJsonBody(shape: StructureShape?, bindings: Map): StructureShape? { - if (shape == null) { - return null - } - val bodyMembers = shape.members().filter { member -> - bindings[member.memberName]?.location == HttpBinding.Location.DOCUMENT - } - - return if (bodyMembers.isNotEmpty()) { - shape.toBuilder().members(bodyMembers).build() - } else { - null - } - } - override fun transformModel(model: Model): Model { - val httpIndex = HttpBindingIndex.of(model) - return OperationNormalizer(model).transformModel( - inputBodyFactory = { op, input -> restJsonBody(input, httpIndex.getRequestBindings(op)) }, - outputBodyFactory = { op, output -> restJsonBody(output, httpIndex.getResponseBindings(op)) }, - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt index cf8cf069e1..f002fb330d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestXml.kt @@ -34,10 +34,7 @@ class RestXmlFactory(private val generator: (ProtocolConfig) -> Protocol = { Res } override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticInputTrait.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticInputTrait.kt index 7493a32899..32cedd5922 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticInputTrait.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticInputTrait.kt @@ -5,7 +5,7 @@ package software.amazon.smithy.rust.codegen.smithy.traits -import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.AnnotationTrait @@ -15,21 +15,8 @@ import software.amazon.smithy.model.traits.AnnotationTrait class SyntheticInputTrait( val operation: ShapeId, val originalId: ShapeId?, - // TODO: Remove synthetic body when cleaning up serde json generators - val body: ShapeId? -) : - AnnotationTrait(ID, ObjectNode.fromStringMap(mapOf("body" to body.toString()))) { +) : AnnotationTrait(ID, Node.objectNode()) { companion object { val ID = ShapeId.from("smithy.api.internal#syntheticInput") } } - -/** - * Indicates that a shape is a synthetic input body - */ -// TODO: Remove synthetic body when cleaning up serde json generators -class InputBodyTrait(objectNode: ObjectNode = ObjectNode.objectNode()) : AnnotationTrait(ID, objectNode) { - companion object { - val ID = ShapeId.from("smithy.api.internal#syntheticInputBody") - } -} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticOutputTrait.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticOutputTrait.kt index ab2ad43c90..b1da41d55e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticOutputTrait.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/SyntheticOutputTrait.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.rust.codegen.smithy.traits +import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.node.ObjectNode import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.traits.AnnotationTrait @@ -12,8 +13,8 @@ import software.amazon.smithy.model.traits.AnnotationTrait /** * Indicates that a shape is a synthetic input (see `OperationNormalizer.kt`) */ -class SyntheticOutputTrait constructor(val operation: ShapeId, val body: ShapeId?, val originalId: ShapeId?) : - AnnotationTrait(ID, ObjectNode.fromStringMap(mapOf("body" to body.toString()))) { +class SyntheticOutputTrait constructor(val operation: ShapeId, val originalId: ShapeId?) : + AnnotationTrait(ID, Node.objectNode()) { companion object { val ID = ShapeId.from("smithy.api.internal#syntheticOutput") } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt index 4a87d83033..054d386954 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt @@ -11,8 +11,6 @@ import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.transform.ModelTransformer -import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait -import software.amazon.smithy.rust.codegen.smithy.traits.OutputBodyTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait import software.amazon.smithy.rust.codegen.util.orNull @@ -28,21 +26,14 @@ class OperationNormalizer(private val model: Model) { /** * Add synthetic input & output shapes to every Operation in model. The generated shapes will be marked with * [SyntheticInputTrait] and [SyntheticOutputTrait] respectively. Shapes will be added _even_ if the operation does - * not specify an input or an output. The body shapes (if created) will be marked with [InputBodyTrait] and - * [OutputBodyTrait] - * - * To build input bodies, [inputBodyFactory] is called on the input shape of every operation. It MUST return a structure representing the - * shape of the request body, or null if there is no request body. - * - * To build output bodies, [outputBodyFactory] is called on the output shape of every operation. It MUST return a structure representing the - * shape of the response body, or null if there is no response body. + * not specify an input or an output. */ - fun transformModel(inputBodyFactory: StructureModifier, outputBodyFactory: StructureModifier): Model { + fun transformModel(): Model { val transformer = ModelTransformer.create() val operations = model.shapes(OperationShape::class.java).toList() val newShapes = operations.flatMap { operation -> // Generate or modify the input and output of the given `Operation` to be a unique shape - syntheticInputShapes(operation, inputBodyFactory) + syntheticOutputShapes(operation, outputBodyFactory) + syntheticInputShapes(operation) + syntheticOutputShapes(operation) } val modelWithOperationInputs = model.toBuilder().addShapes(newShapes).build() return transformer.mapShapes(modelWithOperationInputs) { @@ -58,15 +49,8 @@ class OperationNormalizer(private val model: Model) { } } - private fun syntheticOutputShapes( - operation: OperationShape, - outputBodyFactory: StructureModifier - ): List { + private fun syntheticOutputShapes(operation: OperationShape): List { val outputId = operation.syntheticOutputId() - val outputBodyShape = outputBodyFactory( - operation, - operation.output.map { model.expectShape(it, StructureShape::class.java) }.orNull() - )?.let { it.toBuilder().addTrait(OutputBodyTrait()).rename(operation.syntheticOutputBodyId()).build() } val outputShapeBuilder = operation.output.map { shapeId -> model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(outputId) }.orElse(empty(outputId)) @@ -74,24 +58,13 @@ class OperationNormalizer(private val model: Model) { SyntheticOutputTrait( operation = operation.id, originalId = operation.output.orNull(), - body = outputBodyShape?.id ) ).build() - return listOf(outputShape, outputBodyShape).mapNotNull { it } + return listOfNotNull(outputShape) } - private fun syntheticInputShapes( - operation: OperationShape, - inputBodyFactory: StructureModifier - ): List { + private fun syntheticInputShapes(operation: OperationShape): List { val inputId = operation.syntheticInputId() - val inputBodyShape = inputBodyFactory( - operation, - operation.input.map { - val inputShape = model.expectShape(it, StructureShape::class.java) - inputShape.toBuilder().addTrait(InputBodyTrait()).rename(operation.syntheticInputBodyId()).build() - }.orNull() - ) val inputShapeBuilder = operation.input.map { shapeId -> model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(inputId) }.orElse(empty(inputId)) @@ -99,26 +72,19 @@ class OperationNormalizer(private val model: Model) { inputShapeBuilder.addTrait( SyntheticInputTrait( operation = operation.id, - body = inputBodyShape?.id, originalId = operation.input.orNull() ) ).build() - return listOf(inputShape, inputBodyShape).mapNotNull { it } + return listOfNotNull(inputShape) } private fun empty(id: ShapeId) = StructureShape.builder().id(id) companion object { - // Functions to construct synthetic shape IDs—Don't rely on these in external code: The attached traits - // provide shape ids via `.body` on [SyntheticInputTrait] and [SyntheticOutputTrait] + // Functions to construct synthetic shape IDs—Don't rely on these in external code. // Rename safety: Operations cannot be renamed private fun OperationShape.syntheticInputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Input") private fun OperationShape.syntheticOutputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Output") - // TODO: Remove synthetic body when cleaning up serde json generators - private fun OperationShape.syntheticInputBodyId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}InputBody") - private fun OperationShape.syntheticOutputBodyId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}OutputBody") - - val NoBody: StructureModifier = { _: OperationShape, _: StructureShape? -> null } } } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt index 223db5a470..a575602252 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt @@ -101,10 +101,7 @@ class RequestBindingGeneratorTest { stringHeader: String } """.asSmithyModel() - private val model = OperationNormalizer(baseModel).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ) + private val model = OperationNormalizer(baseModel).transformModel() private val operationShape = model.expectShape(ShapeId.from("smithy.example#PutObject"), OperationShape::class.java) private val inputShape = model.expectShape(operationShape.input.get(), StructureShape::class.java) 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 06caa40179..79ac4016d3 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 @@ -65,10 +65,7 @@ class ResponseBindingGeneratorTest { additional: String, } """.asSmithyModel() - private val model = OperationNormalizer(baseModel).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ) + private val model = OperationNormalizer(baseModel).transformModel() private val operationShape = model.expectShape(ShapeId.from("smithy.example#PutObject"), OperationShape::class.java) private val symbolProvider = testSymbolProvider(model) private val testProtocolConfig: ProtocolConfig = testProtocolConfig(model) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt index 757704ad5e..23eafa6d88 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/StreamingShapeSymbolProviderTest.kt @@ -34,8 +34,7 @@ internal class StreamingShapeSymbolProviderTest { fun `generates a byte stream on streaming output`() { // we could test exactly the streaming shape symbol provider, but we actually care about is the full stack // "doing the right thing" - val modelWithOperationTraits = - OperationNormalizer(model).transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) + val modelWithOperationTraits = OperationNormalizer(model).transformModel() val symbolProvider = testSymbolProvider(modelWithOperationTraits) symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechOutput\$data")).name shouldBe ("byte_stream::ByteStream") symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechInput\$data")).name shouldBe ("byte_stream::ByteStream") @@ -43,8 +42,7 @@ internal class StreamingShapeSymbolProviderTest { @Test fun `streaming members have a default`() { - val modelWithOperationTraits = - OperationNormalizer(model).transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) + val modelWithOperationTraits = OperationNormalizer(model).transformModel() val symbolProvider = testSymbolProvider(modelWithOperationTraits) val outputSymbol = symbolProvider.toSymbol(modelWithOperationTraits.lookup("test#GenerateSpeechOutput\$data")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt index 61ab923cdd..4d3fb9ac4c 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolTestGeneratorTest.kt @@ -164,10 +164,7 @@ class HttpProtocolTestGeneratorTest { } override fun transformModel(model: Model): Model { - return OperationNormalizer(model).transformModel( - inputBodyFactory = OperationNormalizer.NoBody, - outputBodyFactory = OperationNormalizer.NoBody - ).let(RemoveEventStreamOperations::transform) + return OperationNormalizer(model).transformModel().let(RemoveEventStreamOperations::transform) } override fun support(): ProtocolSupport { @@ -195,10 +192,10 @@ class HttpProtocolTestGeneratorTest { fun `passing e2e protocol request test`() { val path = generateService( """ - .uri("/?Hi=Hello%20there&required") - .header("X-Greeting", "Hi") - .method("POST") - """ + .uri("/?Hi=Hello%20there&required") + .header("X-Greeting", "Hi") + .method("POST") + """ ) val testOutput = "cargo test".runCommand(path) @@ -210,10 +207,10 @@ class HttpProtocolTestGeneratorTest { fun `test incorrect response parsing`() { val path = generateService( """ - .uri("/?Hi=Hello%20there&required") - .header("X-Greeting", "Hi") - .method("POST") - """, + .uri("/?Hi=Hello%20there&required") + .header("X-Greeting", "Hi") + .method("POST") + """, correctResponse = "Ok(crate::output::SayHelloOutput::builder().build())" ) val err = assertThrows { @@ -227,10 +224,10 @@ class HttpProtocolTestGeneratorTest { fun `test invalid body`() { val path = generateService( """ - .uri("/?Hi=Hello%20there&required") - .header("X-Greeting", "Hi") - .method("POST") - """, + .uri("/?Hi=Hello%20there&required") + .header("X-Greeting", "Hi") + .method("POST") + """, """"{}".to_string().into()""" ) @@ -248,10 +245,10 @@ class HttpProtocolTestGeneratorTest { // Hard coded implementation for this 1 test val path = generateService( """ - .uri("/?Hi=INCORRECT&required") - .header("X-Greeting", "Hi") - .method("POST") - """ + .uri("/?Hi=INCORRECT&required") + .header("X-Greeting", "Hi") + .method("POST") + """ ) val err = assertThrows { @@ -266,10 +263,10 @@ class HttpProtocolTestGeneratorTest { fun `test forbidden url parameter`() { val path = generateService( """ - .uri("/?goodbye&Hi=Hello%20there&required") - .header("X-Greeting", "Hi") - .method("POST") - """ + .uri("/?goodbye&Hi=Hello%20there&required") + .header("X-Greeting", "Hi") + .method("POST") + """ ) val err = assertThrows { @@ -285,10 +282,10 @@ class HttpProtocolTestGeneratorTest { // Hard coded implementation for this 1 test val path = generateService( """ - .uri("/?Hi=Hello%20there") - .header("X-Greeting", "Hi") - .method("POST") - """ + .uri("/?Hi=Hello%20there") + .header("X-Greeting", "Hi") + .method("POST") + """ ) val err = assertThrows { @@ -303,11 +300,11 @@ class HttpProtocolTestGeneratorTest { fun `invalid header`() { val path = generateService( """ - .uri("/?Hi=Hello%20there&required") - // should be "Hi" - .header("X-Greeting", "Hey") - .method("POST") - """ + .uri("/?Hi=Hello%20there&required") + // should be "Hi" + .header("X-Greeting", "Hey") + .method("POST") + """ ) val err = assertThrows { diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt index 91e59491bd..5b34b568de 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/AwsQueryParserGeneratorTest.kt @@ -42,10 +42,7 @@ class AwsQueryParserGeneratorTest { @Test fun `it modifies operation parsing to include Response and Result tags`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel) - .transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = AwsQueryParserGenerator( testProtocolConfig(model), 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 a7be48fdb3..7d5cfa0d66 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 @@ -42,10 +42,7 @@ class Ec2QueryParserGeneratorTest { @Test fun `it modifies operation parsing to include Response and Result tags`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel) - .transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = Ec2QueryParserGenerator( testProtocolConfig(model), 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 4a418f95bd..1d1c4be8cd 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 @@ -99,11 +99,7 @@ class JsonParserGeneratorTest { @Test fun `generates valid deserializers`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel).transformModel( - OperationNormalizer.NoBody, - ) { _, shape -> shape } - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = JsonParserGenerator( testProtocolConfig(model), 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 e3e22495f7..0e20f5aeaf 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 @@ -90,7 +90,7 @@ internal class XmlBindingTraitParserGeneratorTest { @Test fun `generates valid parsers`() { - val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody)) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = XmlBindingTraitParserGenerator( testProtocolConfig(model), 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 466ca4a186..d166b36bce 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 @@ -81,12 +81,7 @@ class AwsQuerySerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel).transformModel( - OperationNormalizer.NoBody, - OperationNormalizer.NoBody - ) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = AwsQuerySerializerGenerator(testProtocolConfig(model)) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) 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 77b368a759..bcdb0a73a6 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 @@ -80,12 +80,7 @@ class Ec2QuerySerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel).transformModel( - OperationNormalizer.NoBody, - OperationNormalizer.NoBody - ) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = Ec2QuerySerializerGenerator(testProtocolConfig(model)) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) 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 ba49321e6a..f52f49b98d 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 @@ -98,12 +98,7 @@ class JsonSerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel).transformModel( - OperationNormalizer.NoBody, - OperationNormalizer.NoBody - ) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserSerializer = JsonSerializerGenerator( testProtocolConfig(model), 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 93cf0fd1ab..15186706de 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 @@ -103,12 +103,7 @@ internal class XmlBindingTraitSerializerGeneratorTest { @Test fun `generates valid serializers`() { - val model = RecursiveShapeBoxer.transform( - OperationNormalizer(baseModel).transformModel( - OperationNormalizer.NoBody, - OperationNormalizer.NoBody - ) - ) + val model = RecursiveShapeBoxer.transform(OperationNormalizer(baseModel).transformModel()) val symbolProvider = testSymbolProvider(model) val parserGenerator = XmlBindingTraitSerializerGenerator( testProtocolConfig(model), diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt index 3e7ef7eb6c..29b41bed57 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizerTest.kt @@ -11,16 +11,13 @@ import org.junit.jupiter.api.Test import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.rust.codegen.smithy.traits.InputBodyTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticOutputTrait import software.amazon.smithy.rust.codegen.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.testutil.testSymbolProvider -import software.amazon.smithy.rust.codegen.util.lookup import software.amazon.smithy.rust.codegen.util.orNull internal class OperationNormalizerTest { - @Test fun `add inputs and outputs to empty operations`() { val model = """ @@ -30,23 +27,19 @@ internal class OperationNormalizerTest { val operationId = ShapeId.from("smithy.test#Empty") model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe false val sut = OperationNormalizer(model) - val modified = sut.transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) + val modified = sut.transformModel() val operation = modified.expectShape(operationId, OperationShape::class.java) operation.input.isPresent shouldBe true operation.input.get().name shouldBe "EmptyInput" val inputShape = modified.expectShape(operation.input.get(), StructureShape::class.java) - val inputTrait = inputShape.expectTrait(SyntheticInputTrait::class.java) - // When there isn't an input, we shouldn't attach a body - inputTrait.body shouldBe null + inputShape.expectTrait(SyntheticInputTrait::class.java) operation.output.orNull() shouldNotBe null operation.output.get().name shouldBe "EmptyOutput" val outputShape = modified.expectShape(operation.output.get(), StructureShape::class.java) - val outputTrait = outputShape.expectTrait(SyntheticOutputTrait::class.java) - // When there isn't an output, we shouldn't attach a body - outputTrait.body shouldBe null + outputShape.expectTrait(SyntheticOutputTrait::class.java) } @Test @@ -63,7 +56,7 @@ internal class OperationNormalizerTest { val operationId = ShapeId.from("smithy.test#MyOp") model.expectShape(operationId, OperationShape::class.java).input.isPresent shouldBe true val sut = OperationNormalizer(model) - val modified = sut.transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) + val modified = sut.transformModel() val operation = modified.expectShape(operationId, OperationShape::class.java) operation.input.isPresent shouldBe true val inputId = operation.input.get() @@ -87,7 +80,7 @@ internal class OperationNormalizerTest { val operationId = ShapeId.from("smithy.test#MyOp") model.expectShape(operationId, OperationShape::class.java).output.isPresent shouldBe true val sut = OperationNormalizer(model) - val modified = sut.transformModel(OperationNormalizer.NoBody, OperationNormalizer.NoBody) + val modified = sut.transformModel() val operation = modified.expectShape(operationId, OperationShape::class.java) operation.output.isPresent shouldBe true val outputId = operation.output.get() @@ -96,40 +89,4 @@ internal class OperationNormalizerTest { testSymbolProvider(modified).toSymbol(outputShape).name shouldBe "MyOpOutput" outputShape.memberNames shouldBe listOf("v") } - - @Test - fun `create bodies for operations`() { - val model = """ - namespace smithy.test - structure RenameMe { - v: String, - drop: String - } - operation MyOp { - input: RenameMe, - output: RenameMe - }""".asSmithyModel() - - val sut = OperationNormalizer(model) - val modified = sut.transformModel( - inputBodyFactory = { _, input -> - input?.toBuilder()?.members(input.members().filter { it.memberName != "drop" })?.build() - }, - outputBodyFactory = { _, output -> output?.toBuilder()?.members(emptyList())?.build() } - ) - val operation = modified.lookup("smithy.test#MyOp") - operation.input.isPresent shouldBe true - val inputId = operation.input.get() - inputId.name shouldBe "MyOpInput" - val inputShape = modified.expectShape(inputId, StructureShape::class.java) - val input = inputShape.expectTrait(SyntheticInputTrait::class.java) - input.body shouldBe ShapeId.from("smithy.test#MyOpInputBody") - val inputBody = modified.expectShape(input.body, StructureShape::class.java) - inputBody.expectTrait(InputBodyTrait::class.java) - inputBody.members().size shouldBe 1 - - val outputBodyTrait = modified.expectShape(operation.output.get()).expectTrait(SyntheticOutputTrait::class.java) - val outputBody = modified.expectShape(outputBodyTrait.body, StructureShape::class.java) - outputBody.members() shouldBe emptyList() - } }