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 5e2f4a81046aa093cc7c7fc167bf6ea141ed4918..8ca07570ac963ba11f8ab72b32158cd18c94b27f 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 @@ -22,10 +22,10 @@ import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig import software.amazon.smithy.rust.codegen.core.smithy.generators.error.ErrorImplGenerator import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.isEventStream +import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonApplicationGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerEnumGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerOperationErrorGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerOperationHandlerGenerator -import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerServiceGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerStructureGenerator import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerUnionGenerator import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext @@ -224,15 +224,15 @@ class PythonServerCodegenVisitor( * - Python operation handlers */ override fun serviceShape(shape: ServiceShape) { + super.serviceShape(shape) + logger.info("[python-server-codegen] Generating a service $shape") - PythonServerServiceGenerator( - rustCrate, - protocolGenerator, - protocolGeneratorFactory.support(), - protocolGeneratorFactory.protocol(codegenContext) as ServerProtocol, - codegenContext, - ) - .render() + + val serverProtocol = protocolGeneratorFactory.protocol(codegenContext) as ServerProtocol + rustCrate.withModule(PythonServerRustModule.PythonServerApplication) { + PythonApplicationGenerator(codegenContext, serverProtocol) + .render(this) + } } override fun operationShape(shape: OperationShape) { diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt index 8db6dc84396a68be8f4f8191e8569c9ca531ab0c..cc4804bb216e996faca51d6ff8b3b0712f57a62c 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.rust.codegen.server.python.smithy.generators +import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.DocumentationTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter @@ -66,8 +67,13 @@ import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Output class PythonApplicationGenerator( codegenContext: CodegenContext, private val protocol: ServerProtocol, - private val operations: List, ) { + private val index = TopDownIndex.of(codegenContext.model) + private val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet( + compareBy { + it.id + }, + ).toList() private val symbolProvider = codegenContext.symbolProvider private val libName = codegenContext.settings.moduleName.toSnakeCase() private val runtimeConfig = codegenContext.runtimeConfig 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 deleted file mode 100644 index d96c8b1e424ec9a55f415767cd65f159494fa7fd..0000000000000000000000000000000000000000 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerServiceGenerator.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rust.codegen.server.python.smithy.generators - -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.rust.codegen.core.smithy.RustCrate -import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport -import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerRustModule -import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext -import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerServiceGenerator -import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol -import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator - -/** - * PythonServerServiceGenerator - * - * 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: ServerProtocolGenerator, - protocolSupport: ProtocolSupport, - protocol: ServerProtocol, - private val context: ServerCodegenContext, -) : ServerServiceGenerator(rustCrate, protocolGenerator, protocolSupport, protocol, context) { - override fun renderExtras(operations: List) { - rustCrate.withModule(PythonServerRustModule.PythonServerApplication) { - PythonApplicationGenerator(context, protocol, operations) - .render(this) - } - } -} 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 8f2d74717524cefd828ff3d6e0fcf4c4c1651f47..b8bce19845c8ddc961d00f04bc4330f0977c5d7d 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 @@ -68,6 +68,8 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilde import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerEnumGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerOperationErrorGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerOperationGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerRootGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerRuntimeTypesReExportsGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerServiceGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerStructureConstrainedTraitImpl import software.amazon.smithy.rust.codegen.server.smithy.generators.UnconstrainedCollectionGenerator @@ -76,6 +78,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.Unconstraine import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolTestGenerator import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader import software.amazon.smithy.rust.codegen.server.smithy.traits.isReachableFromOperationInput import software.amazon.smithy.rust.codegen.server.smithy.transformers.AttachValidationExceptionToConstrainedOperationInputsInAllowList @@ -564,14 +567,33 @@ open class ServerCodegenVisitor( */ override fun serviceShape(shape: ServiceShape) { logger.info("[rust-server-codegen] Generating a service $shape") - ServerServiceGenerator( - rustCrate, - protocolGenerator, - protocolGeneratorFactory.support(), - protocolGeneratorFactory.protocol(codegenContext) as ServerProtocol, - codegenContext, - ) - .render() + val serverProtocol = protocolGeneratorFactory.protocol(codegenContext) as ServerProtocol + + // Generate root + rustCrate.lib { + ServerRootGenerator( + serverProtocol, + codegenContext, + ).render(this) + } + + // Generate server re-exports + rustCrate.withModule(ServerRustModule.Server) { + ServerRuntimeTypesReExportsGenerator(codegenContext).render(this) + } + + // Generate protocol tests + rustCrate.withModule(ServerRustModule.Operation) { + ServerProtocolTestGenerator(codegenContext, protocolGeneratorFactory.support(), protocolGenerator).render(this) + } + + // Generate service module + rustCrate.withModule(ServerRustModule.Service) { + ServerServiceGenerator( + codegenContext, + serverProtocol, + ).render(this) + } } /** diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustModule.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustModule.kt index 59562e88ce021b9958079d36dea73f2d19445d8d..15fde9837f25ed818c16442582908607152dac02 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustModule.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRustModule.kt @@ -42,6 +42,7 @@ object ServerRustModule { val Output = RustModule.public("output") val Types = RustModule.public("types") val Server = RustModule.public("server") + val Service = RustModule.private("service") val UnconstrainedModule = software.amazon.smithy.rust.codegen.core.smithy.UnconstrainedModule diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8221b932778dfac73f956ddd89388a2c206bb05 --- /dev/null +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt @@ -0,0 +1,240 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.smithy.generators + +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.join +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.util.toPascalCase +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol +import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Error as ErrorModule +import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Input as InputModule +import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Output as OutputModule + +/** + * ServerRootGenerator + * + * Generates all code within `lib.rs`, this includes: + * - Crate documentation + * - Re-exports + */ +open class ServerRootGenerator( + val protocol: ServerProtocol, + private val codegenContext: ServerCodegenContext, +) { + private val index = TopDownIndex.of(codegenContext.model) + private val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet( + compareBy { + it.id + }, + ).toList() + private val serviceName = codegenContext.serviceShape.id.name.toPascalCase() + + fun documentation(writer: RustWriter) { + val builderFieldNames = + operations.associateWith { + RustReservedWords.escapeIfNeeded(codegenContext.symbolProvider.toSymbol(it).name.toSnakeCase()) + } + .toSortedMap( + compareBy { it.id }, + ) + val crateName = codegenContext.moduleUseName() + val builderName = "${serviceName}Builder" + val hasErrors = operations.any { it.errors.isNotEmpty() } + val handlers: Writable = operations + .map { operation -> + DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!")::render + } + .join("//!\n") + + writer.rustTemplate( + """ + //! A fast and customizable Rust implementation of the $serviceName Smithy service. + //! + //! ## Using $serviceName + //! + //! The primary entrypoint is [`$serviceName`]: it satisfies the [`Service`](#{Tower}::Service) + //! trait and therefore can be handed to a [`hyper` server](https://github.com/hyperium/hyper) via [`$serviceName::into_make_service`] or used in Lambda via [`LambdaHandler`](#{SmithyHttpServer}::routing::LambdaHandler). + //! The [`crate::${InputModule.name}`], ${if (!hasErrors) "and " else ""}[`crate::${OutputModule.name}`], ${if (hasErrors) "and [`crate::${ErrorModule.name}`]" else "" } + //! modules provide the types used in each operation. + //! + //! ###### Running on Hyper + //! + //! ```rust,no_run + //! ## use std::net::SocketAddr; + //! ## async fn dummy() { + //! use $crateName::$serviceName; + //! + //! ## let app = $serviceName::builder_without_plugins().build_unchecked(); + //! let server = app.into_make_service(); + //! let bind: SocketAddr = "127.0.0.1:6969".parse() + //! .expect("unable to parse the server bind address and port"); + //! #{Hyper}::Server::bind(&bind).serve(server).await.unwrap(); + //! ## } + //! ``` + //! + //! ###### Running on Lambda + //! + //! This requires the `aws-lambda` feature flag to be passed to the [`#{SmithyHttpServer}`] crate. + //! + //! ```rust,ignore + //! use #{SmithyHttpServer}::routing::LambdaHandler; + //! use $crateName::$serviceName; + //! + //! ## async fn dummy() { + //! ## let app = $serviceName::builder_without_plugins().build_unchecked(); + //! let handler = LambdaHandler::new(app); + //! lambda_http::run(handler).await.unwrap(); + //! ## } + //! ``` + //! + //! ## Building the $serviceName + //! + //! To construct [`$serviceName`] we use [`$builderName`] returned by [`$serviceName::builder_without_plugins`] + //! or [`$serviceName::builder_with_plugins`]. + //! + //! #### Plugins + //! + //! The [`$serviceName::builder_with_plugins`] method, returning [`$builderName`], + //! accepts a [`Plugin`](aws_smithy_http_server::plugin::Plugin). + //! Plugins allow you to build middleware which is aware of the operation it is being applied to. + //! + //! ```rust + //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin; + //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin; + //! ## use #{Hyper}::Body; + //! use #{SmithyHttpServer}::plugin::PluginPipeline; + //! use $crateName::{$serviceName, $builderName}; + //! + //! let plugins = PluginPipeline::new() + //! .push(LoggingPlugin) + //! .push(MetricsPlugin); + //! let builder: $builderName = $serviceName::builder_with_plugins(plugins); + //! ``` + //! + //! Check out [`#{SmithyHttpServer}::plugin`] to learn more about plugins. + //! + //! #### Handlers + //! + //! [`$builderName`] provides a setter method for each operation in your Smithy model. The setter methods expect an async function as input, matching the signature for the corresponding operation in your Smithy model. + //! We call these async functions **handlers**. This is where your application business logic lives. + //! + //! Every handler must take an `Input`, and optional [`extractor arguments`](#{SmithyHttpServer}::request), while returning: + //! + //! * A `Result` if your operation has modeled errors, or + //! * An `Output` otherwise. + //! + //! ```rust + //! ## struct Input; + //! ## struct Output; + //! ## struct Error; + //! async fn infallible_handler(input: Input) -> Output { todo!() } + //! + //! async fn fallible_handler(input: Input) -> Result { todo!() } + //! ``` + //! + //! Handlers can accept up to 8 extractors: + //! + //! ```rust + //! ## struct Input; + //! ## struct Output; + //! ## struct Error; + //! ## struct State; + //! ## use std::net::SocketAddr; + //! use #{SmithyHttpServer}::request::{extension::Extension, connect_info::ConnectInfo}; + //! + //! async fn handler_with_no_extensions(input: Input) -> Output { + //! todo!() + //! } + //! + //! async fn handler_with_one_extractor(input: Input, ext: Extension) -> Output { + //! todo!() + //! } + //! + //! async fn handler_with_two_extractors( + //! input: Input, + //! ext0: Extension, + //! ext1: ConnectInfo, + //! ) -> Output { + //! todo!() + //! } + //! ``` + //! + //! See the [`operation module`](#{SmithyHttpServer}::operation) for information on precisely what constitutes a handler. + //! + //! #### Build + //! + //! You can convert [`$builderName`] into [`$serviceName`] using either [`$builderName::build`] or [`$builderName::build_unchecked`]. + //! + //! [`$builderName::build`] requires you to provide a handler for every single operation in your Smithy model. It will return an error if that is not the case. + //! + //! [`$builderName::build_unchecked`], instead, does not require exhaustiveness. The server will automatically return 500 Internal Server Error to all requests for operations that do not have a registered handler. + //! [`$builderName::build_unchecked`] is particularly useful if you are deploying your Smithy service as a collection of Lambda functions, where each Lambda is only responsible for a subset of the operations in the Smithy service (or even a single one!). + //! + //! ## Example + //! + //! ```rust + //! ## use std::net::SocketAddr; + //! use $crateName::$serviceName; + //! + //! ##[#{Tokio}::main] + //! pub async fn main() { + //! let app = $serviceName::builder_without_plugins() + ${builderFieldNames.values.joinToString("\n") { "//! .$it($it)" }} + //! .build() + //! .expect("failed to build an instance of $serviceName"); + //! + //! let bind: SocketAddr = "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()); + //! ## let server = async { Ok::<_, ()>(()) }; + //! + //! // Run your service! + //! if let Err(err) = server.await { + //! eprintln!("server error: {:?}", err); + //! } + //! } + //! + #{HandlerImports:W} + //! + #{Handlers:W} + //! + //! ``` + //! + //! [`serve`]: https://docs.rs/hyper/0.14.16/hyper/server/struct.Builder.html##method.serve + //! [`tower::make::MakeService`]: https://docs.rs/tower/latest/tower/make/trait.MakeService.html + //! [HTTP binding traits]: https://smithy.io/2.0/spec/http-bindings.html + //! [operations]: https://smithy.io/2.0/spec/service-types.html##operation + //! [hyper server]: https://docs.rs/hyper/latest/hyper/server/index.html + //! [Service]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html + """, + "HandlerImports" to handlerImports(crateName, operations, commentToken = "//!"), + "Handlers" to handlers, + "ExampleHandler" to operations.take(1).map { operation -> DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature() }, + "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(), + "Hyper" to ServerCargoDependency.HyperDev.toType(), + "Tokio" to ServerCargoDependency.TokioDev.toType(), + "Tower" to ServerCargoDependency.Tower.toType(), + ) + } + + /** + * Render Service Specific code. Code will end up in different files via [useShapeWriter]. See `SymbolVisitor.kt` + * which assigns a symbol location to each shape. + */ + fun render(rustWriter: RustWriter) { + documentation(rustWriter) + + rustWriter.rust("pub use crate::service::{$serviceName, ${serviceName}Builder, MissingOperationsError};") + } +} 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 e843a3126430a11270688b3adce32c84bb715f0b..160f9fd685ed6218da366cb9082aad4c2fd3a16c 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 @@ -6,265 +6,542 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.neighbor.Walker import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.traits.PatternTrait import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.documentShape import software.amazon.smithy.rust.codegen.core.rustlang.join import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.smithy.RustCrate -import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.hasTrait +import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext -import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol -import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolGenerator -import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolTestGenerator import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Error as ErrorModule import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Input as InputModule import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Output as OutputModule -/** - * ServerServiceGenerator - * - * 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: ServerProtocolGenerator, - private val protocolSupport: ProtocolSupport, - val protocol: ServerProtocol, +class ServerServiceGenerator( private val codegenContext: ServerCodegenContext, + private val protocol: ServerProtocol, ) { + private val runtimeConfig = codegenContext.runtimeConfig + private val smithyHttpServer = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType() + private val codegenScope = + arrayOf( + "Bytes" to RuntimeType.Bytes, + "Http" to RuntimeType.Http, + "SmithyHttp" to RuntimeType.smithyHttp(runtimeConfig), + "HttpBody" to RuntimeType.HttpBody, + "SmithyHttpServer" to smithyHttpServer, + "Tower" to RuntimeType.Tower, + ) + private val model = codegenContext.model + private val symbolProvider = codegenContext.symbolProvider + private val crateName = codegenContext.moduleUseName() + + private val service = codegenContext.serviceShape + private val serviceName = service.id.name.toPascalCase() + private val builderName = "${serviceName}Builder" + private val builderPluginGenericTypeName = "Plugin" + private val builderBodyGenericTypeName = "Body" + + /** Calculate all `operationShape`s contained within the `ServiceShape`. */ private val index = TopDownIndex.of(codegenContext.model) - protected val operations = index.getContainedOperations(codegenContext.serviceShape).sortedBy { it.id } - private val serviceName = codegenContext.serviceShape.id.name.toPascalCase() - - fun documentation(writer: RustWriter) { - val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id }) - val builderFieldNames = - operations.associateWith { - RustReservedWords.escapeIfNeeded(codegenContext.symbolProvider.toSymbol(it).name.toSnakeCase()) + private val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id }) + + /** Associate each operation with the corresponding field names in the builder struct. */ + private val builderFieldNames = + operations.associateWith { RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(it).name.toSnakeCase()) } + .toSortedMap( + compareBy { it.id }, + ) + + /** Associate each operation with the name of the corresponding Zero-Sized Type (ZST) struct name. */ + private val operationStructNames = operations.associateWith { symbolProvider.toSymbol(it).name.toPascalCase() } + + /** A `Writable` block of "field: Type" for the builder. */ + private val builderFields = + builderFieldNames.values.map { name -> "$name: Option<#{SmithyHttpServer}::routing::Route>" } + + /** The name of the local private module containing the functions that return the request for each operation */ + private val requestSpecsModuleName = "request_specs" + + /** Associate each operation with a function that returns its request spec. */ + private val requestSpecMap: Map> = + operations.associateWith { operationShape -> + val operationName = symbolProvider.toSymbol(operationShape).name + val spec = protocol.serverRouterRequestSpec( + operationShape, + operationName, + serviceName, + smithyHttpServer.resolve("routing::request_spec"), + ) + val functionName = RustReservedWords.escapeIfNeeded(operationName.toSnakeCase()) + val functionBody = writable { + rustTemplate( + """ + fn $functionName() -> #{SpecType} { + #{Spec:W} + } + """, + "Spec" to spec, + "SpecType" to protocol.serverRouterRequestSpecType(smithyHttpServer.resolve("routing::request_spec")), + ) } - .toSortedMap( - compareBy { it.id }, + Pair(functionName, functionBody) + } + + /** A `Writable` block containing all the `Handler` and `Operation` setters for the builder. */ + private fun builderSetters(): Writable = writable { + for ((operationShape, structName) in operationStructNames) { + val fieldName = builderFieldNames[operationShape] + rustTemplate( + """ + /// Sets the [`$structName`](crate::operation_shape::$structName) operation. + /// + /// This should be an async function satisfying the [`Handler`](#{SmithyHttpServer}::operation::Handler) trait. + /// See the [operation module documentation](#{SmithyHttpServer}::operation) for more information. + /// + /// ## Example + /// + /// ```no_run + /// use $crateName::$serviceName; + /// + #{HandlerImports:W} + /// + #{Handler:W} + /// + /// let app = $serviceName::builder_without_plugins() + /// .$fieldName(handler) + /// /* Set other handlers */ + /// .build() + /// .unwrap(); + /// ## let app: $serviceName<#{SmithyHttpServer}::routing::Route<#{SmithyHttp}::body::SdkBody>> = app; + /// ``` + /// + pub fn $fieldName(self, handler: HandlerType) -> Self + where + HandlerType: #{SmithyHttpServer}::operation::Handler, + #{SmithyHttpServer}::operation::Operation<#{SmithyHttpServer}::operation::IntoService>: + #{SmithyHttpServer}::operation::Upgradable< + #{Protocol}, + crate::operation_shape::$structName, + ServiceExtractors, + $builderBodyGenericTypeName, + $builderPluginGenericTypeName, + > + { + use #{SmithyHttpServer}::operation::OperationShapeExt; + self.${fieldName}_operation(crate::operation_shape::$structName::from_handler(handler)) + } + + /// Sets the [`$structName`](crate::operation_shape::$structName) operation. + /// + /// This should be an [`Operation`](#{SmithyHttpServer}::operation::Operation) created from + /// [`$structName`](crate::operation_shape::$structName) using either + /// [`OperationShape::from_handler`](#{SmithyHttpServer}::operation::OperationShapeExt::from_handler) or + /// [`OperationShape::from_service`](#{SmithyHttpServer}::operation::OperationShapeExt::from_service). + pub fn ${fieldName}_operation(mut self, operation: Operation) -> Self + where + Operation: #{SmithyHttpServer}::operation::Upgradable< + #{Protocol}, + crate::operation_shape::$structName, + Extractors, + $builderBodyGenericTypeName, + $builderPluginGenericTypeName, + > + { + self.$fieldName = Some(operation.upgrade(&self.plugin)); + self + } + """, + "Protocol" to protocol.markerStruct(), + "Handler" to DocHandlerGenerator(codegenContext, operationShape, "handler", "///")::render, + "HandlerImports" to handlerImports(crateName, operations), + *codegenScope, + ) + + // Adds newline between setters. + rust("") + } + } + + private fun buildMethod(): Writable = writable { + val missingOperationsVariableName = "missing_operation_names" + val expectMessageVariableName = "unexpected_error_msg" + + val nullabilityChecks = writable { + for (operationShape in operations) { + val fieldName = builderFieldNames[operationShape]!! + val operationZstTypeName = operationStructNames[operationShape]!! + rust( + """ + if self.$fieldName.is_none() { + $missingOperationsVariableName.insert(crate::operation_shape::$operationZstTypeName::NAME, ".$fieldName()"); + } + """, + ) + } + } + val routesArrayElements = writable { + for (operationShape in operations) { + val fieldName = builderFieldNames[operationShape]!! + val (specBuilderFunctionName, _) = requestSpecMap.getValue(operationShape) + rust( + """ + ($requestSpecsModuleName::$specBuilderFunctionName(), self.$fieldName.expect($expectMessageVariableName)), + """, ) - val crateName = codegenContext.moduleUseName() - val builderName = "${serviceName}Builder" - val hasErrors = operations.any { it.errors.isNotEmpty() } - val handlers: Writable = operations - .map { operation -> - DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!")::render } - .join("//!\n") + } - writer.rustTemplate( + rustTemplate( """ - //! A fast and customizable Rust implementation of the $serviceName Smithy service. - //! - //! ## Using $serviceName - //! - //! The primary entrypoint is [`$serviceName`]: it satisfies the [`Service`](#{Tower}::Service) - //! trait and therefore can be handed to a [`hyper` server](https://github.com/hyperium/hyper) via [`$serviceName::into_make_service`] or used in Lambda via [`LambdaHandler`](#{SmithyHttpServer}::routing::LambdaHandler). - //! The [`crate::${InputModule.name}`], ${if (!hasErrors) "and " else ""}[`crate::${OutputModule.name}`], ${if (hasErrors) "and [`crate::${ErrorModule.name}`]" else "" } - //! modules provide the types used in each operation. - //! - //! ###### Running on Hyper - //! - //! ```rust,no_run - //! ## use std::net::SocketAddr; - //! ## async fn dummy() { - //! use $crateName::$serviceName; - //! - //! ## let app = $serviceName::builder_without_plugins().build_unchecked(); - //! let server = app.into_make_service(); - //! let bind: SocketAddr = "127.0.0.1:6969".parse() - //! .expect("unable to parse the server bind address and port"); - //! #{Hyper}::Server::bind(&bind).serve(server).await.unwrap(); - //! ## } - //! ``` - //! - //! ###### Running on Lambda - //! - //! This requires the `aws-lambda` feature flag to be passed to the [`#{SmithyHttpServer}`] crate. - //! - //! ```rust,ignore - //! use #{SmithyHttpServer}::routing::LambdaHandler; - //! use $crateName::$serviceName; - //! - //! ## async fn dummy() { - //! ## let app = $serviceName::builder_without_plugins().build_unchecked(); - //! let handler = LambdaHandler::new(app); - //! lambda_http::run(handler).await.unwrap(); - //! ## } - //! ``` - //! - //! ## Building the $serviceName - //! - //! To construct [`$serviceName`] we use [`$builderName`] returned by [`$serviceName::builder_without_plugins`] - //! or [`$serviceName::builder_with_plugins`]. - //! - //! #### Plugins - //! - //! The [`$serviceName::builder_with_plugins`] method, returning [`$builderName`], - //! accepts a [`Plugin`](aws_smithy_http_server::plugin::Plugin). - //! Plugins allow you to build middleware which is aware of the operation it is being applied to. - //! - //! ```rust - //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin; - //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin; - //! ## use #{Hyper}::Body; - //! use #{SmithyHttpServer}::plugin::PluginPipeline; - //! use $crateName::{$serviceName, $builderName}; - //! - //! let plugins = PluginPipeline::new() - //! .push(LoggingPlugin) - //! .push(MetricsPlugin); - //! let builder: $builderName = $serviceName::builder_with_plugins(plugins); - //! ``` - //! - //! Check out [`#{SmithyHttpServer}::plugin`] to learn more about plugins. - //! - //! #### Handlers - //! - //! [`$builderName`] provides a setter method for each operation in your Smithy model. The setter methods expect an async function as input, matching the signature for the corresponding operation in your Smithy model. - //! We call these async functions **handlers**. This is where your application business logic lives. - //! - //! Every handler must take an `Input`, and optional [`extractor arguments`](#{SmithyHttpServer}::request), while returning: - //! - //! * A `Result` if your operation has modeled errors, or - //! * An `Output` otherwise. - //! - //! ```rust - //! ## struct Input; - //! ## struct Output; - //! ## struct Error; - //! async fn infallible_handler(input: Input) -> Output { todo!() } - //! - //! async fn fallible_handler(input: Input) -> Result { todo!() } - //! ``` - //! - //! Handlers can accept up to 8 extractors: - //! - //! ```rust - //! ## struct Input; - //! ## struct Output; - //! ## struct Error; - //! ## struct State; - //! ## use std::net::SocketAddr; - //! use #{SmithyHttpServer}::request::{extension::Extension, connect_info::ConnectInfo}; - //! - //! async fn handler_with_no_extensions(input: Input) -> Output { - //! todo!() - //! } - //! - //! async fn handler_with_one_extractor(input: Input, ext: Extension) -> Output { - //! todo!() - //! } - //! - //! async fn handler_with_two_extractors( - //! input: Input, - //! ext0: Extension, - //! ext1: ConnectInfo, - //! ) -> Output { - //! todo!() - //! } - //! ``` - //! - //! See the [`operation module`](#{SmithyHttpServer}::operation) for information on precisely what constitutes a handler. - //! - //! #### Build - //! - //! You can convert [`$builderName`] into [`$serviceName`] using either [`$builderName::build`] or [`$builderName::build_unchecked`]. - //! - //! [`$builderName::build`] requires you to provide a handler for every single operation in your Smithy model. It will return an error if that is not the case. - //! - //! [`$builderName::build_unchecked`], instead, does not require exhaustiveness. The server will automatically return 500 Internal Server Error to all requests for operations that do not have a registered handler. - //! [`$builderName::build_unchecked`] is particularly useful if you are deploying your Smithy service as a collection of Lambda functions, where each Lambda is only responsible for a subset of the operations in the Smithy service (or even a single one!). - //! - //! ## Example - //! - //! ```rust - //! ## use std::net::SocketAddr; - //! use $crateName::$serviceName; - //! - //! ##[#{Tokio}::main] - //! pub async fn main() { - //! let app = $serviceName::builder_without_plugins() - ${builderFieldNames.values.joinToString("\n") { "//! .$it($it)" }} - //! .build() - //! .expect("failed to build an instance of $serviceName"); - //! - //! let bind: SocketAddr = "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()); - //! ## let server = async { Ok::<_, ()>(()) }; - //! - //! // Run your service! - //! if let Err(err) = server.await { - //! eprintln!("server error: {:?}", err); - //! } - //! } - //! - #{HandlerImports:W} - //! - #{Handlers:W} - //! - //! ``` - //! - //! [`serve`]: https://docs.rs/hyper/0.14.16/hyper/server/struct.Builder.html##method.serve - //! [`tower::make::MakeService`]: https://docs.rs/tower/latest/tower/make/trait.MakeService.html - //! [HTTP binding traits]: https://smithy.io/2.0/spec/http-bindings.html - //! [operations]: https://smithy.io/2.0/spec/service-types.html##operation - //! [hyper server]: https://docs.rs/hyper/latest/hyper/server/index.html - //! [Service]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html + /// Constructs a [`$serviceName`] from the arguments provided to the builder. + /// + /// Forgetting to register a handler for one or more operations will result in an error. + /// + /// Check out [`$builderName::build_unchecked`] if you'd prefer the service to return status code 500 when an + /// unspecified route requested. + pub fn build(self) -> Result<$serviceName<#{SmithyHttpServer}::routing::Route<$builderBodyGenericTypeName>>, MissingOperationsError> + { + let router = { + use #{SmithyHttpServer}::operation::OperationShape; + let mut $missingOperationsVariableName = std::collections::HashMap::new(); + #{NullabilityChecks:W} + if !$missingOperationsVariableName.is_empty() { + return Err(MissingOperationsError { + operation_names2setter_methods: $missingOperationsVariableName, + }); + } + let $expectMessageVariableName = "this should never panic since we are supposed to check beforehand that a handler has been registered for this operation; please file a bug report under https://github.com/awslabs/smithy-rs/issues"; + + #{PatternInitializations:W} + + #{Router}::from_iter([#{RoutesArrayElements:W}]) + }; + Ok($serviceName { + router: #{SmithyHttpServer}::routing::RoutingService::new(router), + }) + } """, - "HandlerImports" to handlerImports(crateName, operations, commentToken = "//!"), - "Handlers" to handlers, - "ExampleHandler" to operations.take(1).map { operation -> DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature() }, - "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(), - "Hyper" to ServerCargoDependency.HyperDev.toType(), - "Tokio" to ServerCargoDependency.TokioDev.toType(), - "Tower" to ServerCargoDependency.Tower.toType(), + "Router" to protocol.routerType(), + "NullabilityChecks" to nullabilityChecks, + "RoutesArrayElements" to routesArrayElements, + "SmithyHttpServer" to smithyHttpServer, + "PatternInitializations" to patternInitializations(), ) } /** - * Render Service Specific code. Code will end up in different files via [useShapeWriter]. See `SymbolVisitor.kt` - * which assigns a symbol location to each shape. + * Renders `PatternString::compile_regex()` function calls for every + * `@pattern`-constrained string shape in the service closure. */ - fun render() { - rustCrate.lib { - documentation(this) + @Suppress("DEPRECATION") + private fun patternInitializations(): Writable { + val patterns = Walker(model).walkShapes(service) + .filter { shape -> shape is StringShape && shape.hasTrait() && !shape.hasTrait() } + .map { shape -> codegenContext.constrainedShapeSymbolProvider.toSymbol(shape) } + .map { symbol -> + writable { + rustTemplate("#{Type}::compile_regex();", "Type" to symbol) + } + } - rust("pub use crate::service::{$serviceName, ${serviceName}Builder, MissingOperationsError};") - } + patterns.letIf(patterns.isNotEmpty()) { + val docs = listOf(writable { rust("// Eagerly initialize regexes for `@pattern` strings.") }) - rustCrate.withModule(ServerRustModule.Operation) { - ServerProtocolTestGenerator(codegenContext, protocolSupport, protocolGenerator).render(this) + docs + patterns } - rustCrate.withModule(RustModule.private("service")) { - ServerServiceGeneratorV2(codegenContext, protocol).render(this) + return patterns.join("") + } + + private fun buildUncheckedMethod(): Writable = writable { + val pairs = writable { + for (operationShape in operations) { + val fieldName = builderFieldNames[operationShape]!! + val (specBuilderFunctionName, _) = requestSpecMap.getValue(operationShape) + val operationZstTypeName = operationStructNames[operationShape]!! + rustTemplate( + """ + ( + $requestSpecsModuleName::$specBuilderFunctionName(), + self.$fieldName.unwrap_or_else(|| { + #{SmithyHttpServer}::routing::Route::new(<#{SmithyHttpServer}::operation::FailOnMissingOperation as #{SmithyHttpServer}::operation::Upgradable< + #{Protocol}, + crate::operation_shape::$operationZstTypeName, + (), + _, + _, + >>::upgrade(#{SmithyHttpServer}::operation::FailOnMissingOperation, &self.plugin)) + }) + ), + """, + "SmithyHttpServer" to smithyHttpServer, + "Protocol" to protocol.markerStruct(), + ) + } } + rustTemplate( + """ + /// Constructs a [`$serviceName`] from the arguments provided to the builder. + /// Operations without a handler default to returning 500 Internal Server Error to the caller. + /// + /// Check out [`$builderName::build`] if you'd prefer the builder to fail if one or more operations do + /// not have a registered handler. + pub fn build_unchecked(self) -> $serviceName<#{SmithyHttpServer}::routing::Route<$builderBodyGenericTypeName>> + where + $builderBodyGenericTypeName: Send + 'static + { + let router = #{Router}::from_iter([#{Pairs:W}]); + $serviceName { + router: #{SmithyHttpServer}::routing::RoutingService::new(router), + } + } + """, + "Router" to protocol.routerType(), + "Pairs" to pairs, + "SmithyHttpServer" to smithyHttpServer, + ) + } + + /** Returns a `Writable` containing the builder struct definition and its implementations. */ + private fun builder(): Writable = writable { + val builderGenerics = listOf(builderBodyGenericTypeName, builderPluginGenericTypeName).joinToString(", ") + rustTemplate( + """ + /// The service builder for [`$serviceName`]. + /// + /// Constructed via [`$serviceName::builder_with_plugins`] or [`$serviceName::builder_without_plugins`]. + pub struct $builderName<$builderGenerics> { + ${builderFields.joinToString(", ")}, + plugin: $builderPluginGenericTypeName, + } + + impl<$builderGenerics> $builderName<$builderGenerics> { + #{Setters:W} + } + + impl<$builderGenerics> $builderName<$builderGenerics> { + #{BuildMethod:W} + + #{BuildUncheckedMethod:W} + } + """, + "Setters" to builderSetters(), + "BuildMethod" to buildMethod(), + "BuildUncheckedMethod" to buildUncheckedMethod(), + *codegenScope, + ) + } - renderExtras(operations) + private fun requestSpecsModule(): Writable = writable { + val functions = writable { + for ((_, function) in requestSpecMap.values) { + rustTemplate( + """ + pub(super) #{Function:W} + """, + "Function" to function, + ) + } + } + rustTemplate( + """ + mod $requestSpecsModuleName { + #{SpecFunctions:W} + } + """, + "SpecFunctions" to functions, + ) + } - rustCrate.withModule(ServerRustModule.Server) { - renderServerReExports(this) + /** Returns a `Writable` comma delimited sequence of `builder_field: None`. */ + private val notSetFields = builderFieldNames.values.map { + writable { + rustTemplate( + "$it: None", + *codegenScope, + ) } } - // Render any extra section needed by subclasses of `ServerServiceGenerator`. - open fun renderExtras(operations: List) { } + /** Returns a `Writable` containing the service struct definition and its implementations. */ + private fun serviceStruct(): Writable = writable { + documentShape(service, model) + + rustTemplate( + """ + /// + /// See the [root](crate) documentation for more information. + ##[derive(Clone)] + pub struct $serviceName { + router: #{SmithyHttpServer}::routing::RoutingService<#{Router}, #{Protocol}>, + } + + impl $serviceName<()> { + /// Constructs a builder for [`$serviceName`]. + /// You must specify what plugins should be applied to the operations in this service. + /// + /// Use [`$serviceName::builder_without_plugins`] if you don't need to apply plugins. + /// + /// Check out [`PluginPipeline`](#{SmithyHttpServer}::plugin::PluginPipeline) if you need to apply + /// multiple plugins. + pub fn builder_with_plugins(plugin: Plugin) -> $builderName { + $builderName { + #{NotSetFields:W}, + plugin + } + } + + /// Constructs a builder for [`$serviceName`]. + /// + /// Use [`$serviceName::builder_with_plugins`] if you need to specify plugins. + pub fn builder_without_plugins() -> $builderName { + Self::builder_with_plugins(#{SmithyHttpServer}::plugin::IdentityPlugin) + } + } + + impl $serviceName { + /// Converts [`$serviceName`] into a [`MakeService`](tower::make::MakeService). + pub fn into_make_service(self) -> #{SmithyHttpServer}::routing::IntoMakeService { + #{SmithyHttpServer}::routing::IntoMakeService::new(self) + } + + + /// Converts [`$serviceName`] into a [`MakeService`](tower::make::MakeService) with [`ConnectInfo`](#{SmithyHttpServer}::request::connect_info::ConnectInfo). + pub fn into_make_service_with_connect_info(self) -> #{SmithyHttpServer}::routing::IntoMakeServiceWithConnectInfo { + #{SmithyHttpServer}::routing::IntoMakeServiceWithConnectInfo::new(self) + } + + /// Applies a [`Layer`](#{Tower}::Layer) uniformly to all routes. + pub fn layer(self, layer: &L) -> $serviceName + where + L: #{Tower}::Layer + { + $serviceName { + router: self.router.map(|s| s.layer(layer)) + } + } + + /// Applies [`Route::new`](#{SmithyHttpServer}::routing::Route::new) to all routes. + /// + /// This has the effect of erasing all types accumulated via [`layer`]($serviceName::layer). + pub fn boxed(self) -> $serviceName<#{SmithyHttpServer}::routing::Route> + where + S: #{Tower}::Service< + #{Http}::Request, + Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, + Error = std::convert::Infallible>, + S: Clone + Send + 'static, + S::Future: Send + 'static, + { + self.layer(&#{Tower}::layer::layer_fn(#{SmithyHttpServer}::routing::Route::new)) + } + } + + impl #{Tower}::Service<#{Http}::Request> for $serviceName + where + S: #{Tower}::Service<#{Http}::Request, Response = #{Http}::Response> + Clone, + RespB: #{HttpBody}::Body + Send + 'static, + RespB::Error: Into> + { + type Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>; + type Error = S::Error; + type Future = #{SmithyHttpServer}::routing::RoutingFuture; + + fn poll_ready(&mut self, cx: &mut std::task::Context) -> std::task::Poll> { + self.router.poll_ready(cx) + } + + fn call(&mut self, request: #{Http}::Request) -> Self::Future { + self.router.call(request) + } + } + """, + "NotSetFields" to notSetFields.join(", "), + "Router" to protocol.routerType(), + "Protocol" to protocol.markerStruct(), + *codegenScope, + ) + } + + private fun missingOperationsError(): Writable = writable { + rust( + """ + /// The error encountered when calling the [`$builderName::build`] method if one or more operation handlers are not + /// specified. + ##[derive(Debug)] + pub struct MissingOperationsError { + operation_names2setter_methods: std::collections::HashMap<&'static str, &'static str>, + } + + impl std::fmt::Display for MissingOperationsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "You must specify a handler for all operations attached to `$serviceName`.\n\ + We are missing handlers for the following operations:\n", + )?; + for operation_name in self.operation_names2setter_methods.keys() { + writeln!(f, "- {}", operation_name)?; + } - // Render `server` crate, re-exporting types. - private fun renderServerReExports(writer: RustWriter) { - ServerRuntimeTypesReExportsGenerator(codegenContext).render(writer) + writeln!(f, "\nUse the dedicated methods on `$builderName` to register the missing handlers:")?; + for setter_name in self.operation_names2setter_methods.values() { + writeln!(f, "- {}", setter_name)?; + } + Ok(()) + } + } + + impl std::error::Error for MissingOperationsError {} + """, + ) + } + + fun render(writer: RustWriter) { + writer.rustTemplate( + """ + #{Builder:W} + + #{MissingOperationsError:W} + + #{RequestSpecs:W} + + #{Struct:W} + """, + "Builder" to builder(), + "MissingOperationsError" to missingOperationsError(), + "RequestSpecs" to requestSpecsModule(), + "Struct" to serviceStruct(), + *codegenScope, + ) + } +} + +/** + * Returns a writable to import the necessary modules used by a handler implementation stub. + * + * ```rust + * use my_service::{input, output, error}; + * ``` + */ +fun handlerImports(crateName: String, operations: Collection, commentToken: String = "///") = writable { + val hasErrors = operations.any { it.errors.isNotEmpty() } + val errorImport = if (hasErrors) ", ${ErrorModule.name}" else "" + if (operations.isNotEmpty()) { + rust("$commentToken use $crateName::{${InputModule.name}, ${OutputModule.name}$errorImport};") } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt deleted file mode 100644 index f5f2cc49db1e9553d715acfee5f2282500a29b4d..0000000000000000000000000000000000000000 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rust.codegen.server.smithy.generators - -import software.amazon.smithy.model.knowledge.TopDownIndex -import software.amazon.smithy.model.neighbor.Walker -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.StringShape -import software.amazon.smithy.model.traits.PatternTrait -import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords -import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Writable -import software.amazon.smithy.rust.codegen.core.rustlang.documentShape -import software.amazon.smithy.rust.codegen.core.rustlang.join -import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.rustlang.writable -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.core.util.hasTrait -import software.amazon.smithy.rust.codegen.core.util.letIf -import software.amazon.smithy.rust.codegen.core.util.toPascalCase -import software.amazon.smithy.rust.codegen.core.util.toSnakeCase -import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency -import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext -import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol -import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Error as ErrorModule -import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Input as InputModule -import software.amazon.smithy.rust.codegen.server.smithy.ServerRustModule.Output as OutputModule - -class ServerServiceGeneratorV2( - private val codegenContext: ServerCodegenContext, - private val protocol: ServerProtocol, -) { - private val runtimeConfig = codegenContext.runtimeConfig - private val smithyHttpServer = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType() - private val codegenScope = - arrayOf( - "Bytes" to RuntimeType.Bytes, - "Http" to RuntimeType.Http, - "SmithyHttp" to RuntimeType.smithyHttp(runtimeConfig), - "HttpBody" to RuntimeType.HttpBody, - "SmithyHttpServer" to smithyHttpServer, - "Tower" to RuntimeType.Tower, - ) - private val model = codegenContext.model - private val symbolProvider = codegenContext.symbolProvider - private val crateName = codegenContext.moduleUseName() - - private val service = codegenContext.serviceShape - private val serviceName = service.id.name.toPascalCase() - private val builderName = "${serviceName}Builder" - private val builderPluginGenericTypeName = "Plugin" - private val builderBodyGenericTypeName = "Body" - - /** Calculate all `operationShape`s contained within the `ServiceShape`. */ - private val index = TopDownIndex.of(codegenContext.model) - private val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id }) - - /** Associate each operation with the corresponding field names in the builder struct. */ - private val builderFieldNames = - operations.associateWith { RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(it).name.toSnakeCase()) } - .toSortedMap( - compareBy { it.id }, - ) - - /** Associate each operation with the name of the corresponding Zero-Sized Type (ZST) struct name. */ - private val operationStructNames = operations.associateWith { symbolProvider.toSymbol(it).name.toPascalCase() } - - /** A `Writable` block of "field: Type" for the builder. */ - private val builderFields = - builderFieldNames.values.map { name -> "$name: Option<#{SmithyHttpServer}::routing::Route>" } - - /** The name of the local private module containing the functions that return the request for each operation */ - private val requestSpecsModuleName = "request_specs" - - /** Associate each operation with a function that returns its request spec. */ - private val requestSpecMap: Map> = - operations.associateWith { operationShape -> - val operationName = symbolProvider.toSymbol(operationShape).name - val spec = protocol.serverRouterRequestSpec( - operationShape, - operationName, - serviceName, - smithyHttpServer.resolve("routing::request_spec"), - ) - val functionName = RustReservedWords.escapeIfNeeded(operationName.toSnakeCase()) - val functionBody = writable { - rustTemplate( - """ - fn $functionName() -> #{SpecType} { - #{Spec:W} - } - """, - "Spec" to spec, - "SpecType" to protocol.serverRouterRequestSpecType(smithyHttpServer.resolve("routing::request_spec")), - ) - } - Pair(functionName, functionBody) - } - - /** A `Writable` block containing all the `Handler` and `Operation` setters for the builder. */ - private fun builderSetters(): Writable = writable { - for ((operationShape, structName) in operationStructNames) { - val fieldName = builderFieldNames[operationShape] - rustTemplate( - """ - /// Sets the [`$structName`](crate::operation_shape::$structName) operation. - /// - /// This should be an async function satisfying the [`Handler`](#{SmithyHttpServer}::operation::Handler) trait. - /// See the [operation module documentation](#{SmithyHttpServer}::operation) for more information. - /// - /// ## Example - /// - /// ```no_run - /// use $crateName::$serviceName; - /// - #{HandlerImports:W} - /// - #{Handler:W} - /// - /// let app = $serviceName::builder_without_plugins() - /// .$fieldName(handler) - /// /* Set other handlers */ - /// .build() - /// .unwrap(); - /// ## let app: $serviceName<#{SmithyHttpServer}::routing::Route<#{SmithyHttp}::body::SdkBody>> = app; - /// ``` - /// - pub fn $fieldName(self, handler: HandlerType) -> Self - where - HandlerType: #{SmithyHttpServer}::operation::Handler, - #{SmithyHttpServer}::operation::Operation<#{SmithyHttpServer}::operation::IntoService>: - #{SmithyHttpServer}::operation::Upgradable< - #{Protocol}, - crate::operation_shape::$structName, - ServiceExtractors, - $builderBodyGenericTypeName, - $builderPluginGenericTypeName, - > - { - use #{SmithyHttpServer}::operation::OperationShapeExt; - self.${fieldName}_operation(crate::operation_shape::$structName::from_handler(handler)) - } - - /// Sets the [`$structName`](crate::operation_shape::$structName) operation. - /// - /// This should be an [`Operation`](#{SmithyHttpServer}::operation::Operation) created from - /// [`$structName`](crate::operation_shape::$structName) using either - /// [`OperationShape::from_handler`](#{SmithyHttpServer}::operation::OperationShapeExt::from_handler) or - /// [`OperationShape::from_service`](#{SmithyHttpServer}::operation::OperationShapeExt::from_service). - pub fn ${fieldName}_operation(mut self, operation: Operation) -> Self - where - Operation: #{SmithyHttpServer}::operation::Upgradable< - #{Protocol}, - crate::operation_shape::$structName, - Extractors, - $builderBodyGenericTypeName, - $builderPluginGenericTypeName, - > - { - self.$fieldName = Some(operation.upgrade(&self.plugin)); - self - } - """, - "Protocol" to protocol.markerStruct(), - "Handler" to DocHandlerGenerator(codegenContext, operationShape, "handler", "///")::render, - "HandlerImports" to handlerImports(crateName, operations), - *codegenScope, - ) - - // Adds newline between setters. - rust("") - } - } - - private fun buildMethod(): Writable = writable { - val missingOperationsVariableName = "missing_operation_names" - val expectMessageVariableName = "unexpected_error_msg" - - val nullabilityChecks = writable { - for (operationShape in operations) { - val fieldName = builderFieldNames[operationShape]!! - val operationZstTypeName = operationStructNames[operationShape]!! - rust( - """ - if self.$fieldName.is_none() { - $missingOperationsVariableName.insert(crate::operation_shape::$operationZstTypeName::NAME, ".$fieldName()"); - } - """, - ) - } - } - val routesArrayElements = writable { - for (operationShape in operations) { - val fieldName = builderFieldNames[operationShape]!! - val (specBuilderFunctionName, _) = requestSpecMap.getValue(operationShape) - rust( - """ - ($requestSpecsModuleName::$specBuilderFunctionName(), self.$fieldName.expect($expectMessageVariableName)), - """, - ) - } - } - - rustTemplate( - """ - /// Constructs a [`$serviceName`] from the arguments provided to the builder. - /// - /// Forgetting to register a handler for one or more operations will result in an error. - /// - /// Check out [`$builderName::build_unchecked`] if you'd prefer the service to return status code 500 when an - /// unspecified route requested. - pub fn build(self) -> Result<$serviceName<#{SmithyHttpServer}::routing::Route<$builderBodyGenericTypeName>>, MissingOperationsError> - { - let router = { - use #{SmithyHttpServer}::operation::OperationShape; - let mut $missingOperationsVariableName = std::collections::HashMap::new(); - #{NullabilityChecks:W} - if !$missingOperationsVariableName.is_empty() { - return Err(MissingOperationsError { - operation_names2setter_methods: $missingOperationsVariableName, - }); - } - let $expectMessageVariableName = "this should never panic since we are supposed to check beforehand that a handler has been registered for this operation; please file a bug report under https://github.com/awslabs/smithy-rs/issues"; - - #{PatternInitializations:W} - - #{Router}::from_iter([#{RoutesArrayElements:W}]) - }; - Ok($serviceName { - router: #{SmithyHttpServer}::routing::RoutingService::new(router), - }) - } - """, - "Router" to protocol.routerType(), - "NullabilityChecks" to nullabilityChecks, - "RoutesArrayElements" to routesArrayElements, - "SmithyHttpServer" to smithyHttpServer, - "PatternInitializations" to patternInitializations(), - ) - } - - /** - * Renders `PatternString::compile_regex()` function calls for every - * `@pattern`-constrained string shape in the service closure. - */ - @Suppress("DEPRECATION") - private fun patternInitializations(): Writable { - val patterns = Walker(model).walkShapes(service) - .filter { shape -> shape is StringShape && shape.hasTrait() && !shape.hasTrait() } - .map { shape -> codegenContext.constrainedShapeSymbolProvider.toSymbol(shape) } - .map { symbol -> - writable { - rustTemplate("#{Type}::compile_regex();", "Type" to symbol) - } - } - - patterns.letIf(patterns.isNotEmpty()) { - val docs = listOf(writable { rust("// Eagerly initialize regexes for `@pattern` strings.") }) - - docs + patterns - } - - return patterns.join("") - } - - private fun buildUncheckedMethod(): Writable = writable { - val pairs = writable { - for (operationShape in operations) { - val fieldName = builderFieldNames[operationShape]!! - val (specBuilderFunctionName, _) = requestSpecMap.getValue(operationShape) - val operationZstTypeName = operationStructNames[operationShape]!! - rustTemplate( - """ - ( - $requestSpecsModuleName::$specBuilderFunctionName(), - self.$fieldName.unwrap_or_else(|| { - #{SmithyHttpServer}::routing::Route::new(<#{SmithyHttpServer}::operation::FailOnMissingOperation as #{SmithyHttpServer}::operation::Upgradable< - #{Protocol}, - crate::operation_shape::$operationZstTypeName, - (), - _, - _, - >>::upgrade(#{SmithyHttpServer}::operation::FailOnMissingOperation, &self.plugin)) - }) - ), - """, - "SmithyHttpServer" to smithyHttpServer, - "Protocol" to protocol.markerStruct(), - ) - } - } - rustTemplate( - """ - /// Constructs a [`$serviceName`] from the arguments provided to the builder. - /// Operations without a handler default to returning 500 Internal Server Error to the caller. - /// - /// Check out [`$builderName::build`] if you'd prefer the builder to fail if one or more operations do - /// not have a registered handler. - pub fn build_unchecked(self) -> $serviceName<#{SmithyHttpServer}::routing::Route<$builderBodyGenericTypeName>> - where - $builderBodyGenericTypeName: Send + 'static - { - let router = #{Router}::from_iter([#{Pairs:W}]); - $serviceName { - router: #{SmithyHttpServer}::routing::RoutingService::new(router), - } - } - """, - "Router" to protocol.routerType(), - "Pairs" to pairs, - "SmithyHttpServer" to smithyHttpServer, - ) - } - - /** Returns a `Writable` containing the builder struct definition and its implementations. */ - private fun builder(): Writable = writable { - val builderGenerics = listOf(builderBodyGenericTypeName, builderPluginGenericTypeName).joinToString(", ") - rustTemplate( - """ - /// The service builder for [`$serviceName`]. - /// - /// Constructed via [`$serviceName::builder_with_plugins`] or [`$serviceName::builder_without_plugins`]. - pub struct $builderName<$builderGenerics> { - ${builderFields.joinToString(", ")}, - plugin: $builderPluginGenericTypeName, - } - - impl<$builderGenerics> $builderName<$builderGenerics> { - #{Setters:W} - } - - impl<$builderGenerics> $builderName<$builderGenerics> { - #{BuildMethod:W} - - #{BuildUncheckedMethod:W} - } - """, - "Setters" to builderSetters(), - "BuildMethod" to buildMethod(), - "BuildUncheckedMethod" to buildUncheckedMethod(), - *codegenScope, - ) - } - - private fun requestSpecsModule(): Writable = writable { - val functions = writable { - for ((_, function) in requestSpecMap.values) { - rustTemplate( - """ - pub(super) #{Function:W} - """, - "Function" to function, - ) - } - } - rustTemplate( - """ - mod $requestSpecsModuleName { - #{SpecFunctions:W} - } - """, - "SpecFunctions" to functions, - ) - } - - /** Returns a `Writable` comma delimited sequence of `builder_field: None`. */ - private val notSetFields = builderFieldNames.values.map { - writable { - rustTemplate( - "$it: None", - *codegenScope, - ) - } - } - - /** Returns a `Writable` containing the service struct definition and its implementations. */ - private fun serviceStruct(): Writable = writable { - documentShape(service, model) - - rustTemplate( - """ - /// - /// See the [root](crate) documentation for more information. - ##[derive(Clone)] - pub struct $serviceName { - router: #{SmithyHttpServer}::routing::RoutingService<#{Router}, #{Protocol}>, - } - - impl $serviceName<()> { - /// Constructs a builder for [`$serviceName`]. - /// You must specify what plugins should be applied to the operations in this service. - /// - /// Use [`$serviceName::builder_without_plugins`] if you don't need to apply plugins. - /// - /// Check out [`PluginPipeline`](#{SmithyHttpServer}::plugin::PluginPipeline) if you need to apply - /// multiple plugins. - pub fn builder_with_plugins(plugin: Plugin) -> $builderName { - $builderName { - #{NotSetFields:W}, - plugin - } - } - - /// Constructs a builder for [`$serviceName`]. - /// - /// Use [`$serviceName::builder_with_plugins`] if you need to specify plugins. - pub fn builder_without_plugins() -> $builderName { - Self::builder_with_plugins(#{SmithyHttpServer}::plugin::IdentityPlugin) - } - } - - impl $serviceName { - /// Converts [`$serviceName`] into a [`MakeService`](tower::make::MakeService). - pub fn into_make_service(self) -> #{SmithyHttpServer}::routing::IntoMakeService { - #{SmithyHttpServer}::routing::IntoMakeService::new(self) - } - - - /// Converts [`$serviceName`] into a [`MakeService`](tower::make::MakeService) with [`ConnectInfo`](#{SmithyHttpServer}::request::connect_info::ConnectInfo). - pub fn into_make_service_with_connect_info(self) -> #{SmithyHttpServer}::routing::IntoMakeServiceWithConnectInfo { - #{SmithyHttpServer}::routing::IntoMakeServiceWithConnectInfo::new(self) - } - - /// Applies a [`Layer`](#{Tower}::Layer) uniformly to all routes. - pub fn layer(self, layer: &L) -> $serviceName - where - L: #{Tower}::Layer - { - $serviceName { - router: self.router.map(|s| s.layer(layer)) - } - } - - /// Applies [`Route::new`](#{SmithyHttpServer}::routing::Route::new) to all routes. - /// - /// This has the effect of erasing all types accumulated via [`layer`]($serviceName::layer). - pub fn boxed(self) -> $serviceName<#{SmithyHttpServer}::routing::Route> - where - S: #{Tower}::Service< - #{Http}::Request, - Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, - Error = std::convert::Infallible>, - S: Clone + Send + 'static, - S::Future: Send + 'static, - { - self.layer(&#{Tower}::layer::layer_fn(#{SmithyHttpServer}::routing::Route::new)) - } - } - - impl #{Tower}::Service<#{Http}::Request> for $serviceName - where - S: #{Tower}::Service<#{Http}::Request, Response = #{Http}::Response> + Clone, - RespB: #{HttpBody}::Body + Send + 'static, - RespB::Error: Into> - { - type Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>; - type Error = S::Error; - type Future = #{SmithyHttpServer}::routing::RoutingFuture; - - fn poll_ready(&mut self, cx: &mut std::task::Context) -> std::task::Poll> { - self.router.poll_ready(cx) - } - - fn call(&mut self, request: #{Http}::Request) -> Self::Future { - self.router.call(request) - } - } - """, - "NotSetFields" to notSetFields.join(", "), - "Router" to protocol.routerType(), - "Protocol" to protocol.markerStruct(), - *codegenScope, - ) - } - - private fun missingOperationsError(): Writable = writable { - rust( - """ - /// The error encountered when calling the [`$builderName::build`] method if one or more operation handlers are not - /// specified. - ##[derive(Debug)] - pub struct MissingOperationsError { - operation_names2setter_methods: std::collections::HashMap<&'static str, &'static str>, - } - - impl std::fmt::Display for MissingOperationsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "You must specify a handler for all operations attached to `$serviceName`.\n\ - We are missing handlers for the following operations:\n", - )?; - for operation_name in self.operation_names2setter_methods.keys() { - writeln!(f, "- {}", operation_name)?; - } - - writeln!(f, "\nUse the dedicated methods on `$builderName` to register the missing handlers:")?; - for setter_name in self.operation_names2setter_methods.values() { - writeln!(f, "- {}", setter_name)?; - } - Ok(()) - } - } - - impl std::error::Error for MissingOperationsError {} - """, - ) - } - - fun render(writer: RustWriter) { - writer.rustTemplate( - """ - #{Builder:W} - - #{MissingOperationsError:W} - - #{RequestSpecs:W} - - #{Struct:W} - """, - "Builder" to builder(), - "MissingOperationsError" to missingOperationsError(), - "RequestSpecs" to requestSpecsModule(), - "Struct" to serviceStruct(), - *codegenScope, - ) - } -} - -/** - * Returns a writable to import the necessary modules used by a handler implementation stub. - * - * ```rust - * use my_service::{input, output, error}; - * ``` - */ -fun handlerImports(crateName: String, operations: Collection, commentToken: String = "///") = writable { - val hasErrors = operations.any { it.errors.isNotEmpty() } - val errorImport = if (hasErrors) ", ${ErrorModule.name}" else "" - if (operations.isNotEmpty()) { - rust("$commentToken use $crateName::{${InputModule.name}, ${OutputModule.name}$errorImport};") - } -}