From e751ed6965ef1e738d99b3a13373d0d5e552d6ca Mon Sep 17 00:00:00 2001 From: david-perez Date: Tue, 5 Jul 2022 17:05:55 +0200 Subject: [PATCH] Make `ServerOperationRegistryGenerator` protocol-agnostic (#1525) `ServerOperationRegistryGenerator` is the only server generator that is currently not protocol-agnostic, in the sense that it is the only generator that contains protocol-specific logic, as opposed to delegating to the `Protocol` interface like other generators do. With this change, we should be good to implement a `RustCodegenDecorator` loaded from the classpath that implements support for any protocol, provided the decorator provides a class implementing the `Protocol` interface. This commit also contains some style changes that are making `ktlint` fail. It seems like our `ktlint` config was recently broken and some style violations slipped through the cracks in previous commits that touched these files. --- .../smithy/PythonCodegenServerPlugin.kt | 2 +- .../smithy/PythonServerCodegenVisitor.kt | 4 +- .../PythonServerServiceGenerator.kt | 8 +- .../server/smithy/RustCodegenServerPlugin.kt | 2 +- .../server/smithy/ServerCodegenVisitor.kt | 4 +- .../ServerOperationHandlerGenerator.kt | 5 +- .../ServerOperationRegistryGenerator.kt | 153 ++++-------------- .../generators/ServerServiceGenerator.kt | 8 +- .../server/smithy/protocols/ServerAwsJson.kt | 15 +- .../smithy/protocols/ServerRestJsonFactory.kt | 2 +- .../ServerOperationRegistryGeneratorTest.kt | 85 +++++----- .../smithy/customize/RustCodegenDecorator.kt | 2 +- .../http/RestRequestSpecGenerator.kt | 94 +++++++++++ .../rust/codegen/smithy/protocols/AwsJson.kt | 21 ++- .../rust/codegen/smithy/protocols/AwsQuery.kt | 14 ++ .../rust/codegen/smithy/protocols/Ec2Query.kt | 14 ++ .../rust/codegen/smithy/protocols/Protocol.kt | 17 ++ .../rust/codegen/smithy/protocols/RestJson.kt | 11 ++ .../rust/codegen/smithy/protocols/RestXml.kt | 11 ++ 19 files changed, 288 insertions(+), 184 deletions(-) create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RestRequestSpecGenerator.kt diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonCodegenServerPlugin.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonCodegenServerPlugin.kt index 414d5775df..42400cc0f7 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonCodegenServerPlugin.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonCodegenServerPlugin.kt @@ -17,10 +17,10 @@ import software.amazon.smithy.rust.codegen.smithy.BaseSymbolMetadataProvider import software.amazon.smithy.rust.codegen.smithy.EventStreamSymbolProvider import software.amazon.smithy.rust.codegen.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.smithy.StreamingShapeMetadataProvider +import software.amazon.smithy.rust.codegen.smithy.StreamingShapeSymbolProvider import software.amazon.smithy.rust.codegen.smithy.SymbolVisitor import software.amazon.smithy.rust.codegen.smithy.SymbolVisitorConfig import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecorator -import software.amazon.smithy.rust.codegen.smithy.StreamingShapeSymbolProvider import java.util.logging.Level import java.util.logging.Logger diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt index 2b315f9137..6a4d79cbe2 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt @@ -143,8 +143,8 @@ class PythonServerCodegenVisitor( rustCrate, protocolGenerator, protocolGeneratorFactory.support(), - protocolGeneratorFactory.protocol(codegenContext).httpBindingResolver, - codegenContext, + protocolGeneratorFactory.protocol(codegenContext), + codegenContext ) .render() } diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerServiceGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerServiceGenerator.kt index ca39f29277..7eb5b29fc1 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerServiceGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerServiceGenerator.kt @@ -13,21 +13,21 @@ import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolGenerator import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport -import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver +import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol /** * PythonServerServiceGenerator * - * Service generator is the main codegeneration entry point for Smithy services. Individual structures and unions are + * Service generator is the main code generation entry point for Smithy services. Individual structures and unions are * generated in codegen visitor, but this class handles all protocol-specific code generation (i.e. operations). */ class PythonServerServiceGenerator( private val rustCrate: RustCrate, protocolGenerator: ProtocolGenerator, protocolSupport: ProtocolSupport, - httpBindingResolver: HttpBindingResolver, + protocol: Protocol, private val context: CoreCodegenContext, -) : ServerServiceGenerator(rustCrate, protocolGenerator, protocolSupport, httpBindingResolver, context) { +) : ServerServiceGenerator(rustCrate, protocolGenerator, protocolSupport, protocol, context) { override fun renderCombinedErrors(writer: RustWriter, operation: OperationShape) { PythonServerCombinedErrorGenerator(context.model, context.symbolProvider, operation).render(writer) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt index 1d2440038c..8395ceb44e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/RustCodegenServerPlugin.kt @@ -45,7 +45,7 @@ class RustCodegenServerPlugin : SmithyBuildPlugin { CombinedCodegenDecorator.fromClasspath(context, ServerRequiredCustomizations()) // ServerCodegenVisitor is the main driver of code generation that traverses the model and generates code - logger.info("Loaded plugin to generate pure Rust bindings for the server SSDK") + logger.info("Loaded plugin to generate pure Rust bindings for the server SDK") ServerCodegenVisitor(context, codegenDecorator).execute() } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index b6d7fc1266..e1ea6af164 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -223,8 +223,8 @@ open class ServerCodegenVisitor( rustCrate, protocolGenerator, protocolGeneratorFactory.support(), - protocolGeneratorFactory.protocol(codegenContext).httpBindingResolver, - codegenContext, + protocolGeneratorFactory.protocol(codegenContext), + codegenContext ) .render() } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationHandlerGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationHandlerGenerator.kt index 5dd3266a24..eb09b3e8f1 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationHandlerGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationHandlerGenerator.kt @@ -34,7 +34,6 @@ open class ServerOperationHandlerGenerator( private val model = coreCodegenContext.model private val protocol = coreCodegenContext.protocol private val symbolProvider = coreCodegenContext.symbolProvider - private val operationNames = operations.map { symbolProvider.toSymbol(it).name } private val runtimeConfig = coreCodegenContext.runtimeConfig private val codegenScope = arrayOf( "AsyncTrait" to ServerCargoDependency.AsyncTrait.asType(), @@ -52,7 +51,7 @@ open class ServerOperationHandlerGenerator( renderHandlerImplementations(writer, true) } - /* + /** * Renders the implementation of the `Handler` trait for all operations. * Handlers are implemented for `FnOnce` function types whose signatures take in state or not. */ @@ -126,7 +125,7 @@ open class ServerOperationHandlerGenerator( } } - /* + /** * Generates the trait bounds of the `Handler` trait implementation, depending on: * - the presence of state; and * - whether the operation is fallible or not. diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt index c3631b5b41..5beb889591 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGenerator.kt @@ -5,10 +5,6 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators -import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait -import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait -import software.amazon.smithy.aws.traits.protocols.RestJson1Trait -import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.DocumentationTrait import software.amazon.smithy.rust.codegen.rustlang.Attribute @@ -32,7 +28,7 @@ import software.amazon.smithy.rust.codegen.smithy.Inputs import software.amazon.smithy.rust.codegen.smithy.Outputs import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol -import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver +import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.outputShape @@ -51,12 +47,11 @@ import software.amazon.smithy.rust.codegen.util.toSnakeCase */ class ServerOperationRegistryGenerator( coreCodegenContext: CoreCodegenContext, - private val httpBindingResolver: HttpBindingResolver, + private val protocol: Protocol, private val operations: List, ) { private val crateName = coreCodegenContext.settings.moduleName private val model = coreCodegenContext.model - private val protocol = coreCodegenContext.protocol private val symbolProvider = coreCodegenContext.symbolProvider private val serviceName = coreCodegenContext.serviceShape.toShapeId().name private val operationNames = operations.map { symbolProvider.toSymbol(it).name.toSnakeCase() } @@ -89,14 +84,20 @@ class ServerOperationRegistryGenerator( } private fun renderOperationRegistryRustDocs(writer: RustWriter) { + val inputOutputErrorsImport = if (operations.any { it.errors.isNotEmpty() }) { + "/// use $crateName::{${Inputs.namespace}, ${Outputs.namespace}, ${Errors.namespace}};" + } else { + "/// use $crateName::{${Inputs.namespace}, ${Outputs.namespace}};" + } + writer.rustTemplate( """ ##[allow(clippy::tabs_in_doc_comments)] -/// The `${operationRegistryName}` is the place where you can register +/// The `$operationRegistryName` is the place where you can register /// your service's operation implementations. /// -/// Use [`${operationRegistryBuilderName}`] to construct the -/// `${operationRegistryName}`. For each of the [operations] modeled in +/// Use [`$operationRegistryBuilderName`] to construct the +/// `$operationRegistryName`. For each of the [operations] modeled in /// your Smithy service, you need to provide an implementation in the /// form of a Rust async function or closure that takes in the /// operation's input as their first parameter, and returns the @@ -120,17 +121,13 @@ class ServerOperationRegistryGenerator( /// /// ```rust /// use std::net::SocketAddr; -${ if (operations.any { it.errors.isNotEmpty() }) { -"/// use ${crateName}::{${Inputs.namespace}, ${Outputs.namespace}, ${Errors.namespace}};" -} else { -"/// use ${crateName}::{${Inputs.namespace}, ${Outputs.namespace}};" -} } -/// use ${crateName}::operation_registry::${operationRegistryBuilderName}; +$inputOutputErrorsImport +/// use $crateName::operation_registry::$operationRegistryBuilderName; /// use #{Router}; /// /// ##[#{Tokio}::main] /// pub async fn main() { -/// let app: Router = ${operationRegistryBuilderName}::default() +/// let app: Router = $operationRegistryBuilderName::default() ${operationNames.map { ".$it($it)" }.joinToString("\n") { it.prependIndent("/// ") }} /// .build() /// .expect("unable to build operation registry") @@ -206,10 +203,10 @@ ${operationImplementationStubs(operations)} Attribute.Derives(setOf(RuntimeType.Debug)).render(writer) writer.rustTemplate( """ - pub enum ${operationRegistryErrorName}{ + pub enum $operationRegistryErrorName { UninitializedField(&'static str) } - impl #{Display} for ${operationRegistryErrorName}{ + impl #{Display} for $operationRegistryErrorName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UninitializedField(v) => write!(f, "{}", v), @@ -263,14 +260,14 @@ ${operationImplementationStubs(operations)} ) } - rustBlock("pub fn build(self) -> Result<$operationRegistryNameWithArguments, ${operationRegistryErrorName}>") { + rustBlock("pub fn build(self) -> Result<$operationRegistryNameWithArguments, $operationRegistryErrorName>") { withBlock("Ok( $operationRegistryName {", "})") { for (operationName in operationNames) { rust( """ $operationName: match self.$operationName { Some(v) => v, - None => return Err(${operationRegistryErrorName}::UninitializedField("$operationName")), + None => return Err($operationRegistryErrorName::UninitializedField("$operationName")), }, """ ) @@ -320,7 +317,11 @@ ${operationImplementationStubs(operations)} ) } - withBlockTemplate("#{Router}::${runtimeRouterConstructor()}(vec![", "])", *codegenScope) { + withBlockTemplate( + "#{Router}::${protocol.serverRouterRuntimeConstructor()}(vec![", + "])", + *codegenScope + ) { requestSpecsVarNames.zip(operationNames).forEach { (requestSpecVarName, operationName) -> rustTemplate( "(#{Tower}::util::BoxCloneService::new(#{ServerOperationHandler}::operation(registry.$operationName)), $requestSpecVarName),", @@ -337,100 +338,6 @@ ${operationImplementationStubs(operations)} */ private fun phantomMembers() = operationNames.mapIndexed { i, _ -> "In$i" }.joinToString(separator = ",\n") - /** - * Finds the runtime function to construct a new `Router` based on the Protocol. - */ - private fun runtimeRouterConstructor(): String = - when (protocol) { - RestJson1Trait.ID -> "new_rest_json_router" - RestXmlTrait.ID -> "new_rest_xml_router" - AwsJson1_0Trait.ID -> "new_aws_json_10_router" - AwsJson1_1Trait.ID -> "new_aws_json_11_router" - else -> TODO("Protocol $protocol not supported yet") - } - - /** - * Returns a writable for the `RequestSpec` for an operation based on the service's protocol. - */ - private fun OperationShape.requestSpec(): Writable = - when (protocol) { - RestJson1Trait.ID, RestXmlTrait.ID -> restRequestSpec() - AwsJson1_0Trait.ID, AwsJson1_1Trait.ID -> awsJsonOperationName() - else -> TODO("Protocol $protocol not supported yet") - } - - /** - * Returns the operation name as required by the awsJson1.x protocols. - */ - private fun OperationShape.awsJsonOperationName(): Writable { - val operationName = symbolProvider.toSymbol(this).name - return writable { - rust("""String::from("$serviceName.$operationName")""") - } - } - - /** - * Generates a restJson1 or restXml specific `RequestSpec`. - */ - private fun OperationShape.restRequestSpec(): Writable { - val httpTrait = httpBindingResolver.httpTrait(this) - val extraCodegenScope = - arrayOf("RequestSpec", "UriSpec", "PathAndQuerySpec", "PathSpec", "QuerySpec", "PathSegment", "QuerySegment").map { - it to ServerCargoDependency.SmithyHttpServer(runtimeConfig).asType().member("routing::request_spec::$it") - }.toTypedArray() - - // TODO(https://github.com/awslabs/smithy-rs/issues/950): Support the `endpoint` trait. - val pathSegmentsVec = writable { - withBlock("vec![", "]") { - for (segment in httpTrait.uri.segments) { - val variant = when { - segment.isGreedyLabel -> "Greedy" - segment.isLabel -> "Label" - else -> """Literal(String::from("${segment.content}"))""" - } - rustTemplate( - "#{PathSegment}::$variant,", - *extraCodegenScope - ) - } - } - } - - val querySegmentsVec = writable { - withBlock("vec![", "]") { - for (queryLiteral in httpTrait.uri.queryLiterals) { - val variant = if (queryLiteral.value == "") { - """Key(String::from("${queryLiteral.key}"))""" - } else { - """KeyValue(String::from("${queryLiteral.key}"), String::from("${queryLiteral.value}"))""" - } - rustTemplate("#{QuerySegment}::$variant,", *extraCodegenScope) - } - } - } - - return writable { - rustTemplate( - """ - #{RequestSpec}::new( - #{Method}::${httpTrait.method}, - #{UriSpec}::new( - #{PathAndQuerySpec}::new( - #{PathSpec}::from_vector_unchecked(#{PathSegmentsVec:W}), - #{QuerySpec}::from_vector_unchecked(#{QuerySegmentsVec:W}) - ) - ), - ) - """, - *codegenScope, - *extraCodegenScope, - "PathSegmentsVec" to pathSegmentsVec, - "QuerySegmentsVec" to querySegmentsVec, - "Method" to CargoDependency.Http.asType().member("Method"), - ) - } - } - private fun operationImplementationStubs(operations: List): String = operations.joinToString("\n///\n") { val operationDocumentation = it.getTrait()?.value @@ -438,11 +345,11 @@ ${operationImplementationStubs(operations)} operationDocumentation.replace("#", "##").prependIndent("/// /// ") + "\n" } else "" ret + - """ + """ /// ${it.signature()} { /// todo!() /// } - """.trimIndent() + """.trimIndent() } /** @@ -465,4 +372,14 @@ ${operationImplementationStubs(operations)} val operationName = symbolProvider.toSymbol(this).name.toSnakeCase() return "async fn $operationName(input: $inputT) -> $outputT" } + + /** + * Returns a writable for the `RequestSpec` for an operation based on the service's protocol. + */ + private fun OperationShape.requestSpec(): Writable = protocol.serverRouterRequestSpec( + this, + symbolProvider.toSymbol(this).name, + serviceName, + ServerCargoDependency.SmithyHttpServer(runtimeConfig).asType().member("routing::request_spec") + ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt index 42023ad4a6..5818bfe1b6 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt @@ -14,19 +14,19 @@ import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolGenerator import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport -import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver +import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol /** * ServerServiceGenerator * - * Service generator is the main codegeneration entry point for Smithy services. Individual structures and unions are + * Service generator is the main code generation entry point for Smithy services. Individual structures and unions are * generated in codegen visitor, but this class handles all protocol-specific code generation (i.e. operations). */ open class ServerServiceGenerator( private val rustCrate: RustCrate, private val protocolGenerator: ProtocolGenerator, private val protocolSupport: ProtocolSupport, - private val httpBindingResolver: HttpBindingResolver, + private val protocol: Protocol, private val coreCodegenContext: CoreCodegenContext, ) { private val index = TopDownIndex.of(coreCodegenContext.model) @@ -84,6 +84,6 @@ open class ServerServiceGenerator( // Render operations registry. private fun renderOperationRegistry(writer: RustWriter, operations: List) { - ServerOperationRegistryGenerator(coreCodegenContext, httpBindingResolver, operations).render(writer) + ServerOperationRegistryGenerator(coreCodegenContext, protocol, operations).render(writer) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt index 3770a9ced9..550b6bbc13 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerAwsJson.kt @@ -27,7 +27,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerial import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.util.hasTrait -/* +/** * AwsJson 1.0 and 1.1 server-side protocol factory. This factory creates the [ServerHttpBoundProtocolGenerator] * with AwsJson specific configurations. */ @@ -57,7 +57,7 @@ class ServerAwsJsonFactory(private val version: AwsJsonVersion) : } /** - * AwsJson requires errors to be serialized with an additional "__type" field. This + * AwsJson requires errors to be serialized in server responses with an additional `__type` field. This * customization writes the right field depending on the version of the AwsJson protocol. */ class ServerAwsJsonError(private val awsJsonVersion: AwsJsonVersion) : JsonCustomization() { @@ -79,15 +79,22 @@ class ServerAwsJsonError(private val awsJsonVersion: AwsJsonVersion) : JsonCusto } /** - * AwsJson requires errors to be serialized with an additional "__type" field. This class + * AwsJson requires operation errors to be serialized in server response with an additional `__type` field. This class * customizes [JsonSerializerGenerator] to add this functionality. + * + * https://awslabs.github.io/smithy/1.0/spec/aws/aws-json-1_0-protocol.html#operation-error-serialization */ class ServerAwsJsonSerializerGenerator( private val coreCodegenContext: CoreCodegenContext, private val httpBindingResolver: HttpBindingResolver, private val awsJsonVersion: AwsJsonVersion, private val jsonSerializerGenerator: JsonSerializerGenerator = - JsonSerializerGenerator(coreCodegenContext, httpBindingResolver, ::awsJsonFieldName, customizations = listOf(ServerAwsJsonError(awsJsonVersion))) + JsonSerializerGenerator( + coreCodegenContext, + httpBindingResolver, + ::awsJsonFieldName, + customizations = listOf(ServerAwsJsonError(awsJsonVersion)) + ) ) : StructuredDataSerializerGenerator by jsonSerializerGenerator class ServerAwsJson( diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJsonFactory.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJsonFactory.kt index 9d226b7e6c..cc1df1e346 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJsonFactory.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJsonFactory.kt @@ -12,7 +12,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.Protocol import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolGeneratorFactory import software.amazon.smithy.rust.codegen.smithy.protocols.RestJson -/* +/** * RestJson1 server-side protocol factory. This factory creates the [ServerHttpProtocolGenerator] * with RestJson1 specific configurations. */ diff --git a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGeneratorTest.kt b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGeneratorTest.kt index 8eb9566023..0404a885e5 100644 --- a/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGeneratorTest.kt +++ b/codegen-server/src/test/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationRegistryGeneratorTest.kt @@ -69,52 +69,53 @@ class ServerOperationRegistryGeneratorTest { val index = TopDownIndex.of(serverCodegenContext.model) val operations = index.getContainedOperations(serverCodegenContext.serviceShape).sortedBy { it.id } - val httpBindingResolver = protocolGeneratorFactory.protocol(serverCodegenContext).httpBindingResolver + val protocol = protocolGeneratorFactory.protocol(serverCodegenContext) - val generator = ServerOperationRegistryGenerator(serverCodegenContext, httpBindingResolver, operations) + val generator = ServerOperationRegistryGenerator(serverCodegenContext, protocol, operations) val writer = RustWriter.forModule("operation_registry") generator.render(writer) writer.toString() shouldContain - """ - /// ```rust - /// use std::net::SocketAddr; - /// use service::{input, output, error}; - /// use service::operation_registry::OperationRegistryBuilder; - /// use aws_smithy_http_server::routing::Router; - /// - /// #[tokio::main] - /// pub async fn main() { - /// let app: Router = OperationRegistryBuilder::default() - /// .frobnify(frobnify) - /// .say_hello(say_hello) - /// .build() - /// .expect("unable to build operation registry") - /// .into(); - /// - /// let bind: SocketAddr = format!("{}:{}", "127.0.0.1", "6969") - /// .parse() - /// .expect("unable to parse the server bind address and port"); - /// - /// let server = hyper::Server::bind(&bind).serve(app.into_make_service()); - /// - /// // Run your service! - /// // if let Err(err) = server.await { - /// // eprintln!("server error: {}", err); - /// // } - /// } - /// - /// /// Only the Frobnify operation is documented, - /// /// over multiple lines. - /// /// And here are #hash #tags! - /// async fn frobnify(input: input::FrobnifyInputOutput) -> Result { - /// todo!() - /// } - /// - /// async fn say_hello(input: input::SayHelloInputOutput) -> output::SayHelloInputOutput { - /// todo!() - /// } - /// ``` - ///""".trimIndent() + """ + /// ```rust + /// use std::net::SocketAddr; + /// use service::{input, output, error}; + /// use service::operation_registry::OperationRegistryBuilder; + /// use aws_smithy_http_server::routing::Router; + /// + /// #[tokio::main] + /// pub async fn main() { + /// let app: Router = OperationRegistryBuilder::default() + /// .frobnify(frobnify) + /// .say_hello(say_hello) + /// .build() + /// .expect("unable to build operation registry") + /// .into(); + /// + /// let bind: SocketAddr = format!("{}:{}", "127.0.0.1", "6969") + /// .parse() + /// .expect("unable to parse the server bind address and port"); + /// + /// let server = hyper::Server::bind(&bind).serve(app.into_make_service()); + /// + /// // Run your service! + /// // if let Err(err) = server.await { + /// // eprintln!("server error: {}", err); + /// // } + /// } + /// + /// /// Only the Frobnify operation is documented, + /// /// over multiple lines. + /// /// And here are #hash #tags! + /// async fn frobnify(input: input::FrobnifyInputOutput) -> Result { + /// todo!() + /// } + /// + /// async fn say_hello(input: input::SayHelloInputOutput) -> output::SayHelloInputOutput { + /// todo!() + /// } + /// ``` + /// + """.trimIndent() } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt index 891e03890c..affbd456d5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customize/RustCodegenDecorator.kt @@ -18,7 +18,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.ManifestCustomizati import software.amazon.smithy.rust.codegen.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolMap import software.amazon.smithy.rust.codegen.util.deepMergeWith -import java.util.* +import java.util.ServiceLoader import java.util.logging.Logger /** diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RestRequestSpecGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RestRequestSpecGenerator.kt new file mode 100644 index 0000000000..34bda11e5d --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RestRequestSpecGenerator.kt @@ -0,0 +1,94 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.smithy.generators.http + +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.rust.codegen.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.rustlang.Writable +import software.amazon.smithy.rust.codegen.rustlang.asType +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate +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.protocols.HttpBindingResolver + +/** + * [RestRequestSpecGenerator] generates a restJson1 or restXml specific `RequestSpec`. Both protocols are routed the same. + * + * This class has to live in the `codegen` subproject instead of in the `codegen-server` subproject because it is used + * by the implementations of the `serverRouterRequestSpec` of the [Protocol] interface, which is used by both subprojects + * (even though only the `codegen-server` subproject calls `serverRouterRequestSpec`). + */ +class RestRequestSpecGenerator( + private val httpBindingResolver: HttpBindingResolver, + private val requestSpecModule: RuntimeType +) { + fun generate(operationShape: OperationShape): Writable { + val httpTrait = httpBindingResolver.httpTrait(operationShape) + val extraCodegenScope = + arrayOf( + "RequestSpec", + "UriSpec", + "PathAndQuerySpec", + "PathSpec", + "QuerySpec", + "PathSegment", + "QuerySegment" + ).map { + it to requestSpecModule.member(it) + }.toTypedArray() + + // TODO(https://github.com/awslabs/smithy-rs/issues/950): Support the `endpoint` trait. + val pathSegmentsVec = writable { + withBlock("vec![", "]") { + for (segment in httpTrait.uri.segments) { + val variant = when { + segment.isGreedyLabel -> "Greedy" + segment.isLabel -> "Label" + else -> """Literal(String::from("${segment.content}"))""" + } + rustTemplate( + "#{PathSegment}::$variant,", + *extraCodegenScope + ) + } + } + } + + val querySegmentsVec = writable { + withBlock("vec![", "]") { + for (queryLiteral in httpTrait.uri.queryLiterals) { + val variant = if (queryLiteral.value == "") { + """Key(String::from("${queryLiteral.key}"))""" + } else { + """KeyValue(String::from("${queryLiteral.key}"), String::from("${queryLiteral.value}"))""" + } + rustTemplate("#{QuerySegment}::$variant,", *extraCodegenScope) + } + } + } + + return writable { + rustTemplate( + """ + #{RequestSpec}::new( + #{Method}::${httpTrait.method}, + #{UriSpec}::new( + #{PathAndQuerySpec}::new( + #{PathSpec}::from_vector_unchecked(#{PathSegmentsVec:W}), + #{QuerySpec}::from_vector_unchecked(#{QuerySegmentsVec:W}) + ) + ), + ) + """, + *extraCodegenScope, + "PathSegmentsVec" to pathSegmentsVec, + "QuerySegmentsVec" to querySegmentsVec, + "Method" to CargoDependency.Http.asType().member("Method"), + ) + } + } +} 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 a8eeee56a3..d5b6d040d8 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 @@ -15,8 +15,10 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustModule 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.rustlang.writable import software.amazon.smithy.rust.codegen.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType @@ -130,7 +132,7 @@ class AwsJsonSerializerGenerator( open class AwsJson( private val coreCodegenContext: CoreCodegenContext, - awsJsonVersion: AwsJsonVersion + private val awsJsonVersion: AwsJsonVersion ) : Protocol { private val runtimeConfig = coreCodegenContext.runtimeConfig private val errorScope = arrayOf( @@ -181,6 +183,23 @@ open class AwsJson( *errorScope ) } + + /** + * Returns the operation name as required by the awsJson1.x protocols. + */ + override fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ) = writable { + rust("""String::from("$serviceName.$operationName")""") + } + + override fun serverRouterRuntimeConstructor() = when (awsJsonVersion) { + AwsJsonVersion.Json10 -> "new_aws_json_10_router" + AwsJsonVersion.Json11 -> "new_aws_json_11_router" + } } fun awsJsonFieldName(member: MemberShape): String = member.memberName 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 57907c543a..d48a21d57f 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 @@ -14,6 +14,7 @@ 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.RustModule +import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate @@ -106,4 +107,17 @@ class AwsQueryProtocol(private val coreCodegenContext: CoreCodegenContext) : Pro rust("#T::parse_generic_error(payload.as_ref())", awsQueryErrors) } } + + override fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ): Writable { + TODO("Not yet implemented") + } + + override fun serverRouterRuntimeConstructor(): String { + TODO("Not yet implemented") + } } 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 0f047ce1a9..3c8dbd3127 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 @@ -12,6 +12,7 @@ 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.RustModule +import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate @@ -98,4 +99,17 @@ class Ec2QueryProtocol(private val coreCodegenContext: CoreCodegenContext) : Pro rust("#T::parse_generic_error(payload.as_ref())", ec2QueryErrors) } } + + override fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ): Writable { + TODO("Not yet implemented") + } + + override fun serverRouterRuntimeConstructor(): String { + TODO("Not yet implemented") + } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Protocol.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Protocol.kt index 32521b2b4d..f315a0450f 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Protocol.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/Protocol.kt @@ -20,6 +20,7 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.model.traits.Trait +import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider @@ -74,6 +75,22 @@ interface Protocol { * there are no response headers or statuses available to further inform the error parsing. */ fun parseEventStreamGenericError(operationShape: OperationShape): RuntimeType + + /** + * Returns a writable for the `RequestSpec` for an operation. + */ + fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ): Writable + + /** + * Returns the name of the constructor to be used on the `Router` type, to instantiate a `Router` using this + * protocol. + */ + fun serverRouterRuntimeConstructor(): String } typealias ProtocolMap = Map> 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 221bd735a6..40a2217397 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 @@ -16,11 +16,13 @@ import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustModule +import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.generators.http.RestRequestSpecGenerator import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator @@ -141,6 +143,15 @@ class RestJson(private val coreCodegenContext: CoreCodegenContext) : Protocol { *errorScope ) } + + override fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ): Writable = RestRequestSpecGenerator(httpBindingResolver, requestSpecModule).generate(operationShape) + + override fun serverRouterRuntimeConstructor() = "new_rest_json_router" } fun restJsonFieldName(member: MemberShape): String { 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 4632dcf4b1..6b16137750 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 @@ -11,12 +11,14 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustModule +import software.amazon.smithy.rust.codegen.rustlang.Writable 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.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext import software.amazon.smithy.rust.codegen.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.smithy.generators.http.RestRequestSpecGenerator import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport import software.amazon.smithy.rust.codegen.smithy.protocols.parse.RestXmlParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator @@ -101,4 +103,13 @@ open class RestXml(private val coreCodegenContext: CoreCodegenContext) : Protoco rust("#T::parse_generic_error(payload.as_ref())", restXmlErrors) } } + + override fun serverRouterRequestSpec( + operationShape: OperationShape, + operationName: String, + serviceName: String, + requestSpecModule: RuntimeType + ): Writable = RestRequestSpecGenerator(httpBindingResolver, requestSpecModule).generate(operationShape) + + override fun serverRouterRuntimeConstructor() = "new_rest_xml_router" }