Unverified Commit 51070219 authored by 82marbag's avatar 82marbag Committed by GitHub
Browse files

Document the new service builder (#2021)



* Document the new service builder

Signed-off-by: default avatarDaniele Ahmed <ahmeddan@amazon.de>
parent 9057bd1f
Loading
Loading
Loading
Loading
+10 −11
Original line number Diff line number Diff line
@@ -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)
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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)
+144 −1
Original line number Diff line number Diff line
@@ -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<http::Request, Response = http::Response>`]
            /// 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<Output, Error>` if your operation has modeled errors, or
            /// * An `Output` otherwise.
            ///
            /// ```rust,ignore
            /// async fn fallible_handler(input: Input, extensions: #{SmithyHttpServer}::Extension<T>) -> Result<Output, Error> { todo!() }
            /// async fn infallible_handler(input: Input, extensions: #{SmithyHttpServer}::Extension<T>) -> 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<T>) -> ... { todo!() }
            /// async fn handler_with_two_extensions(input: Input, ext0: #{SmithyHttpServer}::Extension<T>, ext1: #{SmithyHttpServer}::Extension<T>) -> ... { 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,
        )
    }
}