Skip to content

Commit

Permalink
Fix service builder docs (#2046)
Browse files Browse the repository at this point in the history
* Fix doc prefix in service generator

* Move documentation to root module

* Documentation improvements

* Expose handler imports

Signed-off-by: Daniele Ahmed <ahmeddan@amazon.de>
Co-authored-by: Harry Barber <hlbarber@amazon.co.uk>
  • Loading branch information
82marbag and Harry Barber authored Dec 2, 2022
1 parent 5073a25 commit b7f1a57
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import software.amazon.smithy.model.shapes.OperationShape
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.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.CodegenTarget
Expand All @@ -22,33 +23,48 @@ import software.amazon.smithy.rust.codegen.core.util.outputShape
/**
Generates a stub for use within documentation.
*/
class DocHandlerGenerator(private val operation: OperationShape, private val commentToken: String = "//", private val handlerName: String, codegenContext: CodegenContext) {
class DocHandlerGenerator(
codegenContext: CodegenContext,
private val operation: OperationShape,
private val handlerName: String,
private val commentToken: String = "//",
) {
private val model = codegenContext.model
private val symbolProvider = codegenContext.symbolProvider
private val crateName = codegenContext.moduleUseName()

private val inputSymbol = symbolProvider.toSymbol(operation.inputShape(model))
private val outputSymbol = symbolProvider.toSymbol(operation.outputShape(model))
private val errorSymbol = operation.errorSymbol(model, symbolProvider, CodegenTarget.SERVER)

/**
* Returns the imports required for the function signature
*/
fun docSignatureImports(): Writable = writable {
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};
""".trimIndent(),
)
}

/**
* Returns the function signature for an operation handler implementation. Used in the documentation.
*/
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 (operation.errors.isEmpty()) {
outputSymbol.name
} else {
"Result<${outputSymbol.name}, ${errorSymbol.name}>"
}

return writable {
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 $handlerName(input: ${inputSymbol.name}) -> $outputT {
$commentToken todo!()
$commentToken }
Expand All @@ -58,6 +74,14 @@ class DocHandlerGenerator(private val operation: OperationShape, private val com
}

fun render(writer: RustWriter) {
docSignature()(writer)
writer.rustTemplate(
"""
#{Docs:W}
$commentToken
#{Handler:W}
""",
"Docs" to docSignatureImports(),
"Handler" to docSignature(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ServerOperationShapeGenerator(
"SmithyHttpServer" to
ServerCargoDependency.SmithyHttpServer(codegenContext.runtimeConfig).toType(),
"Tower" to ServerCargoDependency.Tower.toType(),
"Handler" to DocHandlerGenerator(operations[0], "//!", "handler", codegenContext)::render,
"Handler" to DocHandlerGenerator(codegenContext, operations[0], "handler", "//!")::render,
)
for (operation in operations) {
ServerOperationGenerator(codegenContext, operation).render(writer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
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.Visibility
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.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.smithy.RustCrate
import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport
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.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
Expand All @@ -37,12 +46,196 @@ open class ServerServiceGenerator(
protected val operations = index.getContainedOperations(codegenContext.serviceShape).sortedBy { it.id }
private val serviceName = codegenContext.serviceShape.id.name.toString()

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()) }
.toSortedMap(
compareBy { it.id },
)
val crateName = codegenContext.moduleUseName()
val builderName = "${serviceName}Builder"
val service = codegenContext.serviceShape
val hasErrors = service.operations.any { codegenContext.model.expectShape(it).asOperationShape().get().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<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 [`LambdaHandler`](#{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 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<Body, _> = $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
//! ## struct Input;
//! ## struct Output;
//! ## struct Error;
//! async fn infallible_handler(input: Input) -> Output { todo!() }
//!
//! async fn fallible_handler(input: Input) -> Result<Output, Error> { 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<State>) -> Output {
//! todo!()
//! }
//!
//! async fn handler_with_two_extractors(
//! input: Input,
//! ext0: Extension<State>,
//! ext1: ConnectInfo<SocketAddr>,
//! ) -> 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);
//! }
//! }
//!
#{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
""",
"Handlers" to handlers,
"ExampleHandler" to operations.take(1).map { operation -> DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature() },
"SmithyHttpServer" to ServerCargoDependency.SmithyHttpServer(codegenContext.runtimeConfig).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() {
rustCrate.lib {
documentation(this)

rust("##[doc(inline, hidden)]")
rust("pub use crate::service::{$serviceName, ${serviceName}Builder, MissingOperationsError};")
}
Expand Down
Loading

0 comments on commit b7f1a57

Please sign in to comment.