diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt index d9a052b280..d4e4affccd 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/DocHandlerGenerator.kt @@ -18,39 +18,38 @@ import software.amazon.smithy.rust.codegen.core.smithy.OutputsModule import software.amazon.smithy.rust.codegen.core.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.core.util.inputShape import software.amazon.smithy.rust.codegen.core.util.outputShape -import software.amazon.smithy.rust.codegen.core.util.toSnakeCase /** Generates a stub for use within documentation. */ -class DocHandlerGenerator(private val operation: OperationShape, private val commentToken: String = "//", codegenContext: CodegenContext) { +class DocHandlerGenerator(private val operation: OperationShape, private val commentToken: String = "//", private val handlerName: String, codegenContext: CodegenContext) { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider - private val crateName = codegenContext.settings.moduleName.toSnakeCase() + private val crateName = codegenContext.moduleUseName() /** * Returns the function signature for an operation handler implementation. Used in the documentation. */ - private fun OperationShape.docSignature(): Writable { - val inputSymbol = symbolProvider.toSymbol(inputShape(model)) - val outputSymbol = symbolProvider.toSymbol(outputShape(model)) - val errorSymbol = errorSymbol(model, symbolProvider, CodegenTarget.SERVER) + fun docSignature(): Writable { + val inputSymbol = symbolProvider.toSymbol(operation.inputShape(model)) + val outputSymbol = symbolProvider.toSymbol(operation.outputShape(model)) + val errorSymbol = operation.errorSymbol(model, symbolProvider, CodegenTarget.SERVER) - val outputT = if (errors.isEmpty()) { + val outputT = if (operation.errors.isEmpty()) { outputSymbol.name } else { "Result<${outputSymbol.name}, ${errorSymbol.name}>" } return writable { - if (!errors.isEmpty()) { + if (operation.errors.isNotEmpty()) { rust("$commentToken ## use $crateName::${ErrorsModule.name}::${errorSymbol.name};") } rust( """ $commentToken ## use $crateName::${InputsModule.name}::${inputSymbol.name}; $commentToken ## use $crateName::${OutputsModule.name}::${outputSymbol.name}; - $commentToken async fn handler(input: ${inputSymbol.name}) -> $outputT { + $commentToken async fn $handlerName(input: ${inputSymbol.name}) -> $outputT { $commentToken todo!() $commentToken } """.trimIndent(), @@ -59,6 +58,6 @@ class DocHandlerGenerator(private val operation: OperationShape, private val com } fun render(writer: RustWriter) { - operation.docSignature()(writer) + docSignature()(writer) } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationShapeGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationShapeGenerator.kt index 8ae62beca1..4f650f1999 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationShapeGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationShapeGenerator.kt @@ -62,7 +62,7 @@ class ServerOperationShapeGenerator( "SmithyHttpServer" to ServerCargoDependency.SmithyHttpServer(codegenContext.runtimeConfig).toType(), "Tower" to ServerCargoDependency.Tower.toType(), - "Handler" to DocHandlerGenerator(operations[0], "//!", codegenContext)::render, + "Handler" to DocHandlerGenerator(operations[0], "//!", "handler", codegenContext)::render, ) for (operation in operations) { ServerOperationGenerator(codegenContext, operation).render(writer) 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 index 738c14d8ce..ff1a76ce14 100644 --- 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 @@ -17,6 +17,9 @@ 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.CodegenContext +import software.amazon.smithy.rust.codegen.core.smithy.ErrorsModule +import software.amazon.smithy.rust.codegen.core.smithy.InputsModule +import software.amazon.smithy.rust.codegen.core.smithy.OutputsModule 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 @@ -156,7 +159,7 @@ class ServerServiceGeneratorV2( } """, "Protocol" to protocol.markerStruct(), - "Handler" to DocHandlerGenerator(operationShape, "///", codegenContext)::render, + "Handler" to DocHandlerGenerator(operationShape, "///", "handler", codegenContext)::render, *codegenScope, ) @@ -469,8 +472,145 @@ class ServerServiceGeneratorV2( } fun render(writer: RustWriter) { + val crateName = codegenContext.moduleUseName() + val handlers: Writable = operations + .map { operation -> + DocHandlerGenerator(operation, "///", builderFieldNames[operation]!!, codegenContext).docSignature() + } + .reduce { acc, wt -> + writable { + rustTemplate("#{acc:W} \n#{wt:W}", "acc" to acc, "wt" to wt) + } + } + + val hasErrors = service.operations.any { model.expectShape(it).asOperationShape().get().errors.isNotEmpty() } + writer.rustTemplate( """ + /// A fast and customizable Rust implementation of the $serviceName Smithy service. + /// + /// ## Using $serviceName + /// + /// The primary entrypoint is [`$serviceName`]: it satisfies the [`Service`] + /// trait and therefore can be handed to a [`hyper` server] via [`$serviceName::into_make_service`] or used in Lambda via [`#{SmithyHttpServer}::routing::LambdaHandler`]. + /// The [`crate::${InputsModule.name}`], ${if (!hasErrors) "and " else ""}[`crate::${OutputsModule.name}`], ${if (hasErrors) "and [`crate::${ErrorsModule.name}`]" else "" } + /// modules provide the types used in each operation. + /// + /// ###### Running on Hyper + /// ```rust,no_run + /// ## use $crateName::$serviceName; + /// ## use std::net::SocketAddr; + /// ## ##[tokio::main] + /// ## pub async fn main() { + /// ## 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 + /// ```rust,ignore + /// ## use $crateName::$serviceName; + /// ## ##[tokio::main] + /// ## pub async fn main() { + /// ## let app = $serviceName::builder_without_plugins().build_unchecked(); + /// let handler = #{SmithyHttpServer}::routing::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,ignore + /// ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin; + /// ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin; + /// ## use #{SmithyHttpServer}::plugin::PluginPipeline; + /// let plugins = PluginPipeline::new() + /// .push(LoggingPlugin) + /// .push(MetricsPlugin); + /// let builder = $crateName::$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,ignore + /// async fn fallible_handler(input: Input, extensions: #{SmithyHttpServer}::Extension) -> Result { todo!() } + /// async fn infallible_handler(input: Input, extensions: #{SmithyHttpServer}::Extension) -> Output { todo!() } + /// ``` + /// + /// Handlers can accept up to 8 extractors: + /// + /// ```rust,ignore + /// async fn handler_with_no_extensions(input: Input) -> ... { todo!() } + /// async fn handler_with_one_extension(input: Input, ext: #{SmithyHttpServer}::Extension) -> ... { todo!() } + /// async fn handler_with_two_extensions(input: Input, ext0: #{SmithyHttpServer}::Extension, ext1: #{SmithyHttpServer}::Extension) -> ... { todo!() } + /// ... + /// ``` + /// + /// #### 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 500s 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); + /// } + /// } + /// + #{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 + #{Builder:W} #{MissingOperationsError:W} @@ -483,6 +623,9 @@ class ServerServiceGeneratorV2( "MissingOperationsError" to missingOperationsError(), "RequestSpecs" to requestSpecsModule(), "Struct" to serviceStruct(), + "Handlers" to handlers, + "ExampleHandler" to operations.take(1).map { operation -> DocHandlerGenerator(operation, "///", builderFieldNames[operation]!!, codegenContext).docSignature() }, + *codegenScope, ) } }