diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 918e5140bc6fb2d7732d93b94e74843c69ec078a..fa8f427f1ba2215efd0c3e7f77c29258e3b3978b 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -141,13 +141,13 @@ message = """`ShapeId` is the new structure used to represent a shape, with its Before you had an operation and an absolute name as its `NAME` member. You could apply a plugin only to some selected operation: ``` -filter_by_operation_name(plugin, |name| name != Op::NAME); +filter_by_operation_name(plugin, |name| name != Op::ID); ``` Your new filter selects on an operation's absolute name, namespace or name. ``` -filter_by_operation_id(plugin, |id| id.name() != Op::NAME.name()); +filter_by_operation_id(plugin, |id| id.name() != Op::ID.name()); ``` The above filter is applied to an operation's name, the one you use to specify the operation in the Smithy model. @@ -168,3 +168,198 @@ message = "The occurrences of `Arc` have now been replaced references = ["smithy-rs#2758"] meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } author = "ysaito1001" + +[[smithy-rs]] +message = """ +The `Plugin` trait has now been simplified and the `Operation` struct has been removed. + +## Simplication of the `Plugin` trait + +Previously, + +```rust +trait Plugin { + type Service; + type Layer; + + fn map(&self, input: Operation) -> Operation; +} +``` + +modified an `Operation`. + +Now, + +```rust +trait Plugin { + type Service; + + fn apply(&self, svc: S) -> Self::Service; +} +``` + +maps a `tower::Service` to a `tower::Service`. This is equivalent to `tower::Layer` with two extra type parameters: `Protocol` and `Operation`. + +The following middleware setup + +```rust +pub struct PrintService { + inner: S, + name: &'static str, +} + +impl Service for PrintService +where + S: Service, +{ + async fn call(&mut self, req: R) -> Self::Future { + println!("Hi {}", self.name); + self.inner.call(req) + } +} + +pub struct PrintLayer { + name: &'static str, +} + +impl Layer for PrintLayer { + type Service = PrintService; + + fn layer(&self, service: S) -> Self::Service { + PrintService { + inner: service, + name: self.name, + } + } +} + +pub struct PrintPlugin; + +impl Plugin for PrintPlugin +where + Op: OperationShape, +{ + type Service = S; + type Layer = Stack; + + fn map(&self, input: Operation) -> Operation { + input.layer(PrintLayer { name: Op::NAME }) + } +} +``` + +now becomes + +```rust +pub struct PrintService { + inner: S, + name: &'static str, +} + +impl Service for PrintService +where + S: Service, +{ + async fn call(&mut self, req: R) -> Self::Future { + println!("Hi {}", self.name); + self.inner.call(req) + } +} + +pub struct PrintPlugin; + +impl Plugin for PrintPlugin +where + Op: OperationShape, +{ + type Service = PrintService; + + fn apply(&self, svc: S) -> Self::Service { + PrintService { inner, name: Op::ID.name() } + } +} +``` + +A single `Plugin` can no longer apply a `tower::Layer` on HTTP requests/responses _and_ modelled structures at the same time (see middleware positions [C](https://awslabs.github.io/smithy-rs/design/server/middleware.html#c-operation-specific-http-middleware) and [D](https://awslabs.github.io/smithy-rs/design/server/middleware.html#d-operation-specific-model-middleware). Instead one `Plugin` must be specified for each and passed to the service builder constructor separately: + +```rust +let app = PokemonService::builder_with_plugins(/* HTTP plugins */, /* model plugins */) + /* setters */ + .build() + .unwrap(); +``` + +The motivation behind this change is to simplify the job of middleware authors, separate concerns, accomodate common cases better, and to improve composition internally. + +Because `Plugin` is now closer to `tower::Layer` we have two canonical converters: + +```rust +use aws_smithy_http_server::plugin::{PluginLayer, LayerPlugin}; + +// Convert from `Layer` to `Plugin` which applies uniformly across all operations +let layer = /* some layer */; +let plugin = PluginLayer(layer); + +// Convert from `Plugin` to `Layer` for some fixed protocol and operation +let plugin = /* some plugin */; +let layer = LayerPlugin::new::(plugin); +``` + +## Remove `Operation` + +The `aws_smithy_http_server::operation::Operation` structure has now been removed. Previously, there existed a `{operation_name}_operation` setter on the service builder, which accepted an `Operation`. This allowed users to + +```rust +let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_service(/* tower::Service */); + +let app = PokemonService::builder_without_plugins() + .get_pokemon_species_operation(operation) + /* other setters */ + .build() + .unwrap(); +``` + +to set an operation with a `tower::Service`, and + +```rust +let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_service(/* tower::Service */).layer(/* layer */); +let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_handler(/* closure */).layer(/* layer */); + +let app = PokemonService::builder_without_plugins() + .get_pokemon_species_operation(operation) + /* other setters */ + .build() + .unwrap(); +``` + +to add a `tower::Layer` (acting on HTTP requests/responses post-routing) to a single operation. + +We have seen little adoption of this API and for this reason we have opted instead to introduce a new setter, accepting a `tower::Service`, on the service builder: + +```rust +let app = PokemonService::builder_without_plugins() + .get_pokemon_species_service(/* tower::Service */) + /* other setters */ + .build() + .unwrap(); +``` + +Applying a `tower::Layer` to a _single_ operation is now done through the `Plugin` API: + +```rust +use aws_smithy_http_server::plugin::{PluginLayer, filter_by_operation_name, IdentityPlugin}; + +let plugin = PluginLayer(/* layer */); +let scoped_plugin = filter_by_operation_name(plugin, |id| id == GetPokemonSpecies::ID); + +let app = PokemonService::builder_with_plugins(scoped_plugin, IdentityPlugin) + .get_pokemon_species(/* handler */) + /* other setters */ + .build() + .unwrap(); +``` + +""" +references = ["smithy-rs#2740"] +meta = { "breaking" = true, "tada" = false, "bug" = false } +author = "hlbarber" 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 15fde9837f25ed818c16442582908607152dac02..2c2ef75f744aafd0192287b050d8903875f323eb 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 @@ -27,7 +27,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticOutputTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait -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.generators.DocHandlerGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.handlerImports @@ -73,49 +72,22 @@ class ServerModuleDocProvider(private val codegenContext: ServerCodegenContext) val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id }) val firstOperation = operations.first() ?: return@writable - val firstOperationSymbol = codegenContext.symbolProvider.toSymbol(firstOperation) - val firstOperationName = firstOperationSymbol.name.toPascalCase() val crateName = codegenContext.settings.moduleName.toSnakeCase() rustTemplate( """ /// A collection of types representing each operation defined in the service closure. /// - /// ## Constructing an [`Operation`](#{SmithyHttpServer}::operation::OperationShapeExt) - /// - /// To apply middleware to specific operations the [`Operation`](#{SmithyHttpServer}::operation::Operation) - /// API must be used. - /// - /// Using the [`OperationShapeExt`](#{SmithyHttpServer}::operation::OperationShapeExt) trait - /// implemented on each ZST we can construct an [`Operation`](#{SmithyHttpServer}::operation::Operation) - /// with appropriate constraints given by Smithy. - /// - /// #### Example - /// - /// ```no_run - /// use $crateName::operation_shape::$firstOperationName; - /// use #{SmithyHttpServer}::operation::OperationShapeExt; - /// - #{HandlerImports:W} - /// - #{Handler:W} - /// - /// let operation = $firstOperationName::from_handler(handler) - /// .layer(todo!("Provide a layer implementation")); - /// ``` - /// - /// ## Use as Marker Structs - /// - /// The [plugin system](#{SmithyHttpServer}::plugin) also makes use of these + /// The [plugin system](#{SmithyHttpServer}::plugin) makes use of these /// [zero-sized types](https://doc.rust-lang.org/nomicon/exotic-sizes.html##zero-sized-types-zsts) (ZSTs) to - /// parameterize [`Plugin`](#{SmithyHttpServer}::plugin::Plugin) implementations. The traits, such as - /// [`OperationShape`](#{SmithyHttpServer}::operation::OperationShape) can be used to provide + /// parameterize [`Plugin`](#{SmithyHttpServer}::plugin::Plugin) implementations. Their traits, such as + /// [`OperationShape`](#{SmithyHttpServer}::operation::OperationShape), can be used to provide /// operation specific information to the [`Layer`](#{Tower}::Layer) being applied. """.trimIndent(), "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(), "Tower" to ServerCargoDependency.Tower.toType(), - "Handler" to DocHandlerGenerator(codegenContext, firstOperation, "handler", commentToken = "///")::render, + "Handler" to DocHandlerGenerator(codegenContext, firstOperation, "handler", commentToken = "///").docSignature(), "HandlerImports" to handlerImports(crateName, operations, commentToken = "///"), ) } 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 a0dcf07ba9e142517e70b583d3e990fbf043dcb7..d67b79b4553920ce32129ca8e6d5784efa338078 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 @@ -6,10 +6,8 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators 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.util.inputShape @@ -55,14 +53,25 @@ class DocHandlerGenerator( } } - fun render(writer: RustWriter) { - // This assumes that the `error` (if applicable) `input`, and `output` modules have been imported by the - // caller and hence are in scope. - writer.rustTemplate( - """ - #{Handler:W} - """, - "Handler" to docSignature(), - ) + /** + * Similarly to `docSignature`, returns the function signature of an operation handler implementation, with the + * difference that we don't ellide the error for use in `tower::service_fn`. + */ + fun docFixedSignature(): Writable { + val errorT = if (operation.errors.isEmpty()) { + "std::convert::Infallible" + } else { + "${ErrorModule.name}::${errorSymbol.name}" + } + + return writable { + rust( + """ + $commentToken async fn $handlerName(input: ${InputModule.name}::${inputSymbol.name}) -> Result<${OutputModule.name}::${outputSymbol.name}, $errorT> { + $commentToken todo!() + $commentToken } + """.trimIndent(), + ) + } } } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationGenerator.kt index a8ce8cbf06052342c84297355e8dfccf057bd57b..3a5af6cb5091fede83158855a054258c19f115d6 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerOperationGenerator.kt @@ -56,7 +56,7 @@ class ServerOperationGenerator( pub struct $operationName; impl #{SmithyHttpServer}::operation::OperationShape for $operationName { - const NAME: #{SmithyHttpServer}::shape_id::ShapeId = #{SmithyHttpServer}::shape_id::ShapeId::new(${operationIdAbsolute.dq()}, ${operationId.namespace.dq()}, ${operationId.name.dq()}); + const ID: #{SmithyHttpServer}::shape_id::ShapeId = #{SmithyHttpServer}::shape_id::ShapeId::new(${operationIdAbsolute.dq()}, ${operationId.namespace.dq()}, ${operationId.name.dq()}); type Input = crate::input::${operationName}Input; type Output = crate::output::${operationName}Output; 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 index a8221b932778dfac73f956ddd89388a2c206bb05..c8c1dd8450aa3f22644f7c1cb21ba4edee426ed6 100644 --- 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 @@ -53,7 +53,7 @@ open class ServerRootGenerator( val hasErrors = operations.any { it.errors.isNotEmpty() } val handlers: Writable = operations .map { operation -> - DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!")::render + DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature() } .join("//!\n") @@ -110,6 +110,7 @@ open class ServerRootGenerator( //! Plugins allow you to build middleware which is aware of the operation it is being applied to. //! //! ```rust + //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin; //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin; //! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin; //! ## use #{Hyper}::Body; @@ -119,7 +120,7 @@ open class ServerRootGenerator( //! let plugins = PluginPipeline::new() //! .push(LoggingPlugin) //! .push(MetricsPlugin); - //! let builder: $builderName = $serviceName::builder_with_plugins(plugins); + //! let builder: $builderName = $serviceName::builder_with_plugins(plugins, IdentityPlugin); //! ``` //! //! Check out [`#{SmithyHttpServer}::plugin`] to learn more about plugins. diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRuntimeTypesReExportsGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRuntimeTypesReExportsGenerator.kt index b184ccbaa43080091f76da94243fa2e5b80eb277..6318c0483ce60cbf4180a032b60fa39219beb0d9 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRuntimeTypesReExportsGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRuntimeTypesReExportsGenerator.kt @@ -28,7 +28,6 @@ class ServerRuntimeTypesReExportsGenerator( } pub mod operation { pub use #{SmithyHttpServer}::operation::OperationShape; - pub use #{SmithyHttpServer}::operation::Operation; } pub mod plugin { pub use #{SmithyHttpServer}::plugin::Plugin; 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 d78a7aa903bf30f5d3779e12b90eedbb6e231ec4..57dd3c82b46a3c57939edcebb552edc9f8e8c035 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 @@ -52,7 +52,6 @@ class ServerServiceGenerator( 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`. */ @@ -105,6 +104,9 @@ class ServerServiceGenerator( private fun builderSetters(): Writable = writable { for ((operationShape, structName) in operationStructNames) { val fieldName = builderFieldNames[operationShape] + val docHandler = DocHandlerGenerator(codegenContext, operationShape, "handler", "///") + val handler = docHandler.docSignature() + val handlerFixed = docHandler.docFixedSignature() rustTemplate( """ /// Sets the [`$structName`](crate::operation_shape::$structName) operation. @@ -129,44 +131,123 @@ class ServerServiceGenerator( /// ## let app: $serviceName<#{SmithyHttpServer}::routing::Route<#{SmithyHttp}::body::SdkBody>> = app; /// ``` /// - pub fn $fieldName(self, handler: HandlerType) -> Self + 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, - > + + ModelPlugin: #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + #{SmithyHttpServer}::operation::IntoService + >, + #{SmithyHttpServer}::operation::UpgradePlugin::: #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + ModelPlugin::Service + >, + HttpPlugin: #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + < + #{SmithyHttpServer}::operation::UpgradePlugin:: + as #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + ModelPlugin::Service + > + >::Service + >, + + HttpPlugin::Service: #{Tower}::Service<#{Http}::Request, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static, + >>::Future: Send + 'static, + { use #{SmithyHttpServer}::operation::OperationShapeExt; - self.${fieldName}_operation(crate::operation_shape::$structName::from_handler(handler)) + use #{SmithyHttpServer}::plugin::Plugin; + let svc = crate::operation_shape::$structName::from_handler(handler); + let svc = self.model_plugin.apply(svc); + let svc = #{SmithyHttpServer}::operation::UpgradePlugin::::new().apply(svc); + let svc = self.http_plugin.apply(svc); + self.${fieldName}_custom(svc) } /// 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 + /// 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} + /// + #{HandlerFixed:W} + /// + /// let svc = #{Tower}::util::service_fn(handler); + /// let app = $serviceName::builder_without_plugins() + /// .${fieldName}_service(svc) + /// /* Set other handlers */ + /// .build() + /// .unwrap(); + /// ## let app: $serviceName<#{SmithyHttpServer}::routing::Route<#{SmithyHttp}::body::SdkBody>> = app; + /// ``` + /// + pub fn ${fieldName}_service(self, service: S) -> Self where - Operation: #{SmithyHttpServer}::operation::Upgradable< + S: #{SmithyHttpServer}::operation::OperationService, + + ModelPlugin: #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + #{SmithyHttpServer}::operation::Normalize + >, + #{SmithyHttpServer}::operation::UpgradePlugin::: #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + ModelPlugin::Service + >, + HttpPlugin: #{SmithyHttpServer}::plugin::Plugin< #{Protocol}, crate::operation_shape::$structName, - Extractors, - $builderBodyGenericTypeName, - $builderPluginGenericTypeName, - > + < + #{SmithyHttpServer}::operation::UpgradePlugin:: + as #{SmithyHttpServer}::plugin::Plugin< + #{Protocol}, + crate::operation_shape::$structName, + ModelPlugin::Service + > + >::Service + >, + + HttpPlugin::Service: #{Tower}::Service<#{Http}::Request, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static, + >>::Future: Send + 'static, + + { + use #{SmithyHttpServer}::operation::OperationShapeExt; + use #{SmithyHttpServer}::plugin::Plugin; + let svc = crate::operation_shape::$structName::from_service(service); + let svc = self.model_plugin.apply(svc); + let svc = #{SmithyHttpServer}::operation::UpgradePlugin::::new().apply(svc); + let svc = self.http_plugin.apply(svc); + self.${fieldName}_custom(svc) + } + + /// Sets the [`$structName`](crate::operation_shape::$structName) to a custom [`Service`](tower::Service). + /// not constrained by the Smithy contract. + fn ${fieldName}_custom(mut self, svc: S) -> Self + where + S: #{Tower}::Service<#{Http}::Request, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static, + S::Future: Send + 'static, { - self.$fieldName = Some(operation.upgrade(&self.plugin)); + self.$fieldName = Some(#{SmithyHttpServer}::routing::Route::new(svc)); self } """, "Protocol" to protocol.markerStruct(), - "Handler" to DocHandlerGenerator(codegenContext, operationShape, "handler", "///")::render, + "Handler" to handler, + "HandlerFixed" to handlerFixed, "HandlerImports" to handlerImports(crateName, operations), *codegenScope, ) @@ -187,7 +268,7 @@ class ServerServiceGenerator( rust( """ if self.$fieldName.is_none() { - $missingOperationsVariableName.insert(crate::operation_shape::$operationZstTypeName::NAME, ".$fieldName()"); + $missingOperationsVariableName.insert(crate::operation_shape::$operationZstTypeName::ID, ".$fieldName()"); } """, ) @@ -278,13 +359,8 @@ class ServerServiceGenerator( ( $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)) + let svc = #{SmithyHttpServer}::operation::MissingFailure::<#{Protocol}>::default(); + #{SmithyHttpServer}::routing::Route::new(svc) }) ), """, @@ -318,7 +394,7 @@ class ServerServiceGenerator( /** Returns a `Writable` containing the builder struct definition and its implementations. */ private fun builder(): Writable = writable { - val builderGenerics = listOf(builderBodyGenericTypeName, builderPluginGenericTypeName).joinToString(", ") + val builderGenerics = listOf(builderBodyGenericTypeName, "HttpPlugin", "ModelPlugin").joinToString(", ") rustTemplate( """ /// The service builder for [`$serviceName`]. @@ -326,7 +402,8 @@ class ServerServiceGenerator( /// Constructed via [`$serviceName::builder_with_plugins`] or [`$serviceName::builder_without_plugins`]. pub struct $builderName<$builderGenerics> { ${builderFields.joinToString(", ")}, - plugin: $builderPluginGenericTypeName, + http_plugin: HttpPlugin, + model_plugin: ModelPlugin } impl<$builderGenerics> $builderName<$builderGenerics> { @@ -398,18 +475,19 @@ class ServerServiceGenerator( /// /// Check out [`PluginPipeline`](#{SmithyHttpServer}::plugin::PluginPipeline) if you need to apply /// multiple plugins. - pub fn builder_with_plugins(plugin: Plugin) -> $builderName { + pub fn builder_with_plugins(http_plugin: HttpPlugin, model_plugin: ModelPlugin) -> $builderName { $builderName { #{NotSetFields:W}, - plugin + http_plugin, + model_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) + pub fn builder_without_plugins() -> $builderName { + Self::builder_with_plugins(#{SmithyHttpServer}::plugin::IdentityPlugin, #{SmithyHttpServer}::plugin::IdentityPlugin) } } diff --git a/design/src/server/anatomy.md b/design/src/server/anatomy.md index 9c65c87babb8384638f7cd7ea80de6e35425f3d6..6ed01d0f50bfe9edf520a509911815d79799a6bb 100644 --- a/design/src/server/anatomy.md +++ b/design/src/server/anatomy.md @@ -41,36 +41,38 @@ service PokemonService { Smithy Rust will use this model to produce the following API: -```rust,ignore +```rust +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +# use pokemon_service_server_sdk::{input::*, output::*, error::*, operation_shape::*, PokemonService}; // A handler for the `GetPokemonSpecies` operation (the `PokemonSpecies` resource). async fn get_pokemon_species(input: GetPokemonSpeciesInput) -> Result { - /* implementation */ + todo!() } -// Apply a `tower::Layer` to a handler. -let get_pokemon_species_op = GetPokemonSpecies::from_handler(get_pokemon_species).layer(/* some `tower::Layer` */); - // Use the service builder to create `PokemonService`. let pokemon_service = PokemonService::builder_without_plugins() // Pass the handler directly to the service builder... .get_pokemon_species(get_pokemon_species) - // ...or pass the layered handler. - .get_pokemon_species_operation(get_pokemon_species_op) /* other operation setters */ .build() + # ; Result::<(), ()>::Ok(()) .expect("failed to create an instance of the Pokémon service"); +# let pokemon_service: Result, _> = pokemon_service; ``` ## Operations A [Smithy Operation](https://awslabs.github.io/smithy/2.0/spec/service-types.html#operation) specifies the input, output, and possible errors of an API operation. One might characterize a Smithy Operation as syntax for specifying a function type. -We represent this in Rust using the [`OperationShape`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/shape.rs#L8-L22) trait: +We represent this in Rust using the [`OperationShape`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/trait.OperationShape.html) trait: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::shape_id::ShapeId; pub trait OperationShape { /// The name of the operation. - const NAME: &'static str; + const ID: ShapeId; /// The operation input. type Input; @@ -80,6 +82,13 @@ pub trait OperationShape { /// exists. type Error; } +# use aws_smithy_http_server::operation::OperationShape as OpS; +# impl OperationShape for T { +# const ID: ShapeId = ::ID; +# type Input = ::Input; +# type Output = ::Output; +# type Error = ::Error; +# } ``` For each Smithy Operation shape, @@ -97,12 +106,16 @@ operation GetPokemonSpecies { the following implementation is generated -```rust,ignore +```rust +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::{operation::OperationShape, shape_id::ShapeId}; +# use pokemon_service_server_sdk::{input::*, output::*, error::*}; /// Retrieve information about a Pokémon species. pub struct GetPokemonSpecies; impl OperationShape for GetPokemonSpecies { - const NAME: &'static str = "com.aws.example#GetPokemonSpecies"; + const ID: ShapeId = ShapeId::new("com.aws.example#GetPokemonSpecies", "com.aws.example", "GetPokemonSpecies"); type Input = GetPokemonSpeciesInput; type Output = GetPokemonSpeciesOutput; @@ -116,119 +129,117 @@ Note that the `GetPokemonSpecies` marker structure is a zero-sized type (ZST), a The following nomenclature will aid us in our survey. We describe a `tower::Service` as a "model service" if its request and response are Smithy structures, as defined by the `OperationShape` trait - the `GetPokemonSpeciesInput`, `GetPokemonSpeciesOutput`, and `GetPokemonSpeciesError` described above. Similarly, we describe a `tower::Service` as a "HTTP service" if its request and response are [`http`](https://github.com/hyperium/http) structures - `http::Request` and `http::Response`. -In contrast to the marker ZSTs above, the [`Operation`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/mod.rs#L192-L198) structure holds the actual runtime behavior of an operation, which is specified, during construction, by the customer. - -```rust,ignore -/// A Smithy operation, represented by a [`Service`](tower::Service) `S` and a [`Layer`](tower::Layer) `L`. -/// -/// The `L` is held and applied lazily during [`Upgradable::upgrade`]. -pub struct Operation { - inner: S, - layer: L, -} -``` - -The `S` here is a model service, this is specified during construction of the `Operation`. The constructors exist on the marker ZSTs as an extension trait to `OperationShape`, namely [`OperationShapeExt`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/shape.rs#L24-L45): +The constructors exist on the marker ZSTs as an extension trait to `OperationShape`, namely [`OperationShapeExt`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/trait.OperationShapeExt.html): -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::operation::*; /// An extension trait over [`OperationShape`]. pub trait OperationShapeExt: OperationShape { - /// Creates a new [`Operation`] for well-formed [`Handler`]s. - fn from_handler(handler: H) -> Operation> + /// Creates a new [`Service`] for well-formed [`Handler`]s. + fn from_handler(handler: H) -> IntoService where - H: Handler, - Self: Sized, - { - Operation::from_handler(handler) - } + H: Handler, + Self: Sized; - /// Creates a new [`Operation`] for well-formed [`Service`](tower::Service)s. - fn from_service(svc: S) -> Operation> + /// Creates a new [`Service`] for well-formed [`Service`](tower::Service)s. + fn from_service(svc: S) -> Normalize where - S: OperationService, - Self: Sized, - { - Operation::from_service(svc) - } + S: OperationService, + Self: Sized; } +# use aws_smithy_http_server::operation::OperationShapeExt as OpS; +# impl OperationShapeExt for T { +# fn from_handler(handler: H) -> IntoService where H: Handler, Self: Sized { ::from_handler(handler) } +# fn from_service(svc: S) -> Normalize where S: OperationService, Self: Sized { ::from_service(svc) } +# } ``` Observe that there are two constructors provided: `from_handler` which takes a `H: Handler` and `from_service` which takes a `S: OperationService`. In both cases `Self` is passed as a parameter to the traits - this constrains `handler: H` and `svc: S` to the signature given by the implementation of `OperationShape` on `Self`. -The [`Handler`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/handler.rs#L21-L29) and [`OperationService`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs#L15-L29) both serve a similar purpose - they provide a common interface for converting to a model service `S`. +The [`Handler`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/trait.Handler.html) and [`OperationService`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/trait.OperationService.html) both serve a similar purpose - they provide a common interface for converting to a model service `S`. - The `Handler` trait covers all async functions taking `GetPokemonSpeciesInput` and asynchronously returning a `Result`. - The `OperationService` trait covers all `tower::Service`s with request `GetPokemonSpeciesInput`, response `GetPokemonSpeciesOutput` and error `GetPokemonSpeciesOutput`. The `from_handler` constructor is used in the following way: -```rust,ignore -async fn get_pokemon_service(input: GetPokemonServiceInput) -> Result { - /* Handler logic */ +```rust +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +use pokemon_service_server_sdk::{ + input::GetPokemonSpeciesInput, + output::GetPokemonSpeciesOutput, + error::GetPokemonSpeciesError, + operation_shape::GetPokemonSpecies +}; +use aws_smithy_http_server::operation::OperationShapeExt; + +async fn get_pokemon_service(input: GetPokemonSpeciesInput) -> Result { + todo!() } -let operation = GetPokemonService::from_handler(get_pokemon_service); +let operation = GetPokemonSpecies::from_handler(get_pokemon_service); ``` Alternatively, `from_service` constructor: -```rust,ignore -struct Svc { - /* ... */ -} - -impl Service for Svc { - type Response = GetPokemonServiceOutput; - type Error = GetPokemonServiceError; +```rust +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +# extern crate tower; +use pokemon_service_server_sdk::{ + input::GetPokemonSpeciesInput, + output::GetPokemonSpeciesOutput, + error::GetPokemonSpeciesError, + operation_shape::GetPokemonSpecies +}; +use aws_smithy_http_server::operation::OperationShapeExt; +use std::task::{Context, Poll}; +use tower::Service; +struct Svc { /* ... */ } -let svc: Svc = /* ... */; -let operation = GetPokemonService::from_service(svc); -``` - -To summarize, the `S`, in `Operation`, is a _model service_ constructed from a `Handler` or a `OperationService` subject to the constraints of an `OperationShape`. More detailed information on these conversions is provided in the [Handler and OperationService section](https://github.com/awslabs/smithy-rs/blob/39c0096c33417d44f125a042c112b3c16918098a/rust-runtime/aws-smithy-http-server/src/operation/mod.rs#L50-L100) Rust docs. +impl Service for Svc { + type Response = GetPokemonSpeciesOutput; + type Error = GetPokemonSpeciesError; + type Future = /* Future> */ + # std::future::Ready>; -Now, what about the `L` in `Operation`? The `L` is a [`tower::Layer`](https://docs.rs/tower/latest/tower/layer/trait.Layer.html), or colloquially "middleware", that is applied to a _HTTP service_. Note that this means that `L` is _not_ applied directly to `S`. We can append to `L` using the `Operation::layer` method: + fn poll_ready(&mut self, ctx: &mut Context<'_>) -> Poll> { + todo!() + } -```rust,ignore -impl Operation { - /// Applies a [`Layer`] to the operation _after_ it has been upgraded via [`Operation::upgrade`]. - pub fn layer(self, layer: NewL) -> Operation> { - Operation { - inner: self.inner, - layer: Stack::new(self.layer, layer), - } + fn call(&mut self, input: GetPokemonSpeciesInput) -> Self::Future { + todo!() } } -``` - -where [`tower::layer::util::Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html) is used to chain layers together. -A typical use of this might be: - -```rust,ignore -let operation = GetPokemonSpecies::from_handler(handler).layer(RequestBodyLimitLayer::new(500)); +let svc: Svc = Svc { /* ... */ }; +let operation = GetPokemonSpecies::from_service(svc); ``` -where [`RequestBodyLimitLayer`](https://docs.rs/tower-http/latest/tower_http/limit/struct.RequestBodyLimitLayer.html) limits the size of the HTTP request body to the `GetPokemonSpecies` operation. - -As mentioned, `L` is applied _after_ the `Operation` has been "upgraded" to a HTTP service. The procedure of upgrading a model service to a HTTP service is described in the [Upgrading a Model Service](#upgrading-a-model-service) section below. +To summarize a _model service_ constructed can be constructed from a `Handler` or a `OperationService` subject to the constraints of an `OperationShape`. More detailed information on these conversions is provided in the [Handler and OperationService section](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/index.html) Rust docs. ## Serialization and Deserialization -A [Smithy protocol](https://awslabs.github.io/smithy/2.0/spec/protocol-traits.html#serialization-and-protocol-traits) specifies the serialization/deserialization scheme - how a HTTP request is transformed into a modelled input and a modelled output to a HTTP response. The is formalized using the [`FromRequest`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/request.rs#L156-L164) and [`IntoResponse`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/response.rs#L40-L44) traits: +A [Smithy protocol](https://awslabs.github.io/smithy/2.0/spec/protocol-traits.html#serialization-and-protocol-traits) specifies the serialization/deserialization scheme - how a HTTP request is transformed into a modelled input and a modelled output to a HTTP response. The is formalized using the [`FromRequest`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/request/trait.FromRequest.html) and [`IntoResponse`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/response.rs#L40-L44) traits: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# extern crate http; +# use aws_smithy_http_server::body::BoxBody; +# use std::future::Future; /// Provides a protocol aware extraction from a [`Request`]. This consumes the /// [`Request`], in contrast to [`FromParts`]. -pub trait FromRequest: Sized { +pub trait FromRequest: Sized { type Rejection: IntoResponse; type Future: Future>; /// Extracts `self` from a [`Request`] asynchronously. - fn from_request(request: http::Request) -> Self::Future; + fn from_request(request: http::Request) -> Self::Future; } /// A protocol aware function taking `self` to [`http::Response`]. @@ -236,11 +247,30 @@ pub trait IntoResponse { /// Performs a conversion into a [`http::Response`]. fn into_response(self) -> http::Response; } +# use aws_smithy_http_server::request::FromRequest as FR; +# impl> FromRequest for T { +# type Rejection = >::Rejection; +# type Future = >::Future; +# fn from_request(request: http::Request) -> Self::Future { +# >::from_request(request) +# } +# } +# use aws_smithy_http_server::response::IntoResponse as IR; +# impl> IntoResponse

for T { +# fn into_response(self) -> http::Response { >::into_response(self) } +# } ``` Note that both traits are parameterized by `Protocol`. These [protocols](https://awslabs.github.io/smithy/2.0/aws/protocols/index.html) exist as ZST marker structs: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::proto::{ +# aws_json_10::AwsJson1_0 as _, +# aws_json_11::AwsJson1_1 as _, +# rest_json_1::RestJson1 as _, +# rest_xml::RestXml as _, +# }; /// [AWS REST JSON 1.0 Protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-restjson1-protocol.html). pub struct RestJson1; @@ -272,23 +302,16 @@ stateDiagram-v2 into_response --> [*]: HTTP Response ``` -This is formalized by the [`Upgrade`](https://github.com/awslabs/smithy-rs/blob/9a6de1f533f8743dbbc3fa6ad974d104c8b841f4/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs#L74-L82) HTTP service. The `tower::Service` implementation is approximately: +This is formalized by the [`Upgrade`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/operation/struct.Upgrade.html) HTTP service. The `tower::Service` implementation is approximately: ```rust,ignore impl Service for Upgrade where - // `Op` is used to specify the operation shape - Op: OperationShape, - // Smithy input must convert from a HTTP request - Op::Input: FromRequest

, - // Smithy output must convert into a HTTP response - Op::Output: IntoResponse

, - // Smithy error must convert into a HTTP response - OpError: IntoResponse

, - - // The signature of the inner service is correct - S: Service, - + Input: FromRequest, + S: Service, + S::Response: IntoResponse

, + S::Error: IntoResponse

, +{ async fn call(&mut self, request: http::Request) -> http::Response { let model_request = match ::from_request(request).await { Ok(ok) => ok, @@ -297,75 +320,50 @@ where let model_response = self.model_service.call(model_request).await; model_response.into_response() } +} ``` -When we `GetPokemonService::from_handler` or `GetPokemonService::from_service`, the model service produced, `S`, will meet the constraints above. +When we `GetPokemonSpecies::from_handler` or `GetPokemonSpecies::from_service`, the model service produced, `S`, will meet the constraints above. -There is an associated `Layer`, `UpgradeLayer` which constructs `Upgrade` from a service. +There is an associated `Plugin`, `UpgradePlugin` which constructs `Upgrade` from a service. The upgrade procedure is finalized by the application of the `Layer` `L`, referenced in `Operation`. In this way the entire upgrade procedure takes an `Operation` and returns a HTTP service. ```mermaid stateDiagram-v2 direction LR - [*] --> S: HTTP Request - state L { - state Upgrade { - S + [*] --> UpgradePlugin: HTTP Request + state HttpPlugin { + state UpgradePlugin { + direction LR + [*] --> S: Model Input + S --> [*] : Model Output + state ModelPlugin { + S + } } } - S --> [*]: HTTP Response -``` - -Note that the `S` and `L` are specified by logic written, in Rust, by the customer, whereas `Upgrade`/`UpgradeLayer` is specified entirely by Smithy model via the protocol, [HTTP bindings](https://awslabs.github.io/smithy/2.0/spec/http-bindings.html), etc. - -The procedure of taking a struct and transforming it into a HTTP service is formalized by the [`Upgradable`](https://github.com/awslabs/smithy-rs/blob/9a6de1f533f8743dbbc3fa6ad974d104c8b841f4/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs#L220-L225) trait: - -```rust,ignore -/// An interface to convert a representation of a Smithy operation into a [`Route`]. -pub trait Upgradable { - /// Upgrade the representation of a Smithy operation to a [`Route`]. - fn upgrade(self, plugin: &Plugin) -> Route; -} + UpgradePlugin --> [*]: HTTP Response ``` -Why do we need a trait for this? Why not simply write an `upgrade` method on `Operation`? The reason is that we might _not_ want to supply an `Operation` to the service builder, instead we might want to supply something that overrides the typical upgrade procedure. - -Below we give an example of a ZST which can be provided to the builder, which also satisfies `Upgradable` and returns a `MissingFailure` `tower::Service`. This `MissingFailure` service simply returns a status code 500. - -```rust,ignore -/// A marker struct indicating an [`Operation`] has not been set in a builder. -/// -/// This _does_ implement [`Upgradable`] but produces a [`Service`] which always returns an internal failure message. -pub struct FailOnMissingOperation; - -impl Upgradable for FailOnMissingOperation -where - InternalFailureException: IntoResponse, - Protocol: 'static, -{ - fn upgrade(self, _plugin: &Plugin) -> Route { - Route::new(MissingFailure { _protocol: PhantomData }) - } -} -``` - -We go into more detail on how the `Upgradable` trait is used in conjunction with builders in the [Builders](#builders) section below. +Note that the `S` is specified by logic written, in Rust, by the customer, whereas `UpgradePlugin` is specified entirely by Smithy model via the protocol, [HTTP bindings](https://awslabs.github.io/smithy/2.0/spec/http-bindings.html), etc. ## Routers Different protocols supported by Smithy enjoy different routing mechanisms, for example, [AWS JSON 1.0](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-json-1_0-protocol.html#protocol-behaviors) uses the `X-Amz-Target` header to select an operation, whereas [AWS REST XML](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-restxml-protocol.html) uses the [HTTP label trait](https://awslabs.github.io/smithy/2.0/spec/http-bindings.html#httplabel-trait). -Despite their differences, all routing mechanisms satisfy a common interface. This is formalized using the `Router` trait: +Despite their differences, all routing mechanisms satisfy a common interface. This is formalized using the [Router](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/routing/trait.Router.html) trait: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# extern crate http; /// An interface for retrieving an inner [`Service`] given a [`http::Request`]. -pub trait Router { +pub trait Router { type Service; type Error; /// Matches a [`http::Request`] to a target [`Service`]. - fn match_route(&self, request: &http::Request) -> Result; + fn match_route(&self, request: &http::Request) -> Result; } ``` @@ -423,6 +421,90 @@ state in <> ServiceC --> [*] ``` +## Plugins + + +A [`Plugin`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/plugin/trait.Plugin.html) is a +[`tower::Layer`] with two extra type parameters, `Protocol` and `Operation`. This allows the middleware to be +parameterized them and change behavior depending on the context in which it's applied. + +```rust +# extern crate aws_smithy_http_server; +pub trait Plugin { + type Service; + + fn apply(&self, svc: S) -> Self::Service; +} +# use aws_smithy_http_server::plugin::Plugin as Pl; +# impl> Plugin for T { +# type Service = >::Service; +# fn apply(&self, svc: S) -> Self::Service { >::apply(self, svc) } +# } +``` + +An example `Plugin` implementation can be found in [/examples/pokemon-service/src/plugin.rs](https://github.com/awslabs/smithy-rs/blob/main/examples/pokemon-service/src/plugin.rs). + +Plugins can be applied in two places: + +- HTTP plugins, which are applied pre-deserialization/post-serialization, acting on HTTP requests/responses. +- Model plugins, which are applied post-deserialization/pre-serialization, acting on model inputs/outputs/errors. + +```mermaid +stateDiagram-v2 + direction LR + [*] --> S: HTTP Request + state HttpPlugin { + state UpgradePlugin { + state ModelPlugin { + S + } + } + } + S --> [*]: HTTP Response +``` + +The service builder API requires plugins to be specified upfront - they must be passed as an argument to `builder_with_plugins` and cannot be modified afterwards. + +You might find yourself wanting to apply _multiple_ plugins to your service. +This can be accommodated via [`PluginPipeline`]. + +```rust +# extern crate aws_smithy_http_server; +use aws_smithy_http_server::plugin::PluginPipeline; +# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; +# use aws_smithy_http_server::plugin::IdentityPlugin as MetricsPlugin; + +let pipeline = PluginPipeline::new().push(LoggingPlugin).push(MetricsPlugin); +``` + +The plugins' runtime logic is executed in registration order. +In the example above, `LoggingPlugin` would run first, while `MetricsPlugin` is executed last. + +If you are vending a plugin, you can leverage `PluginPipeline` as an extension point: you can add custom methods to it using an extension trait. +For example: + +```rust +# extern crate aws_smithy_http_server; +use aws_smithy_http_server::plugin::{PluginPipeline, PluginStack}; +# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; +# use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin; + +pub trait AuthPluginExt { + fn with_auth(self) -> PluginPipeline>; +} + +impl AuthPluginExt for PluginPipeline { + fn with_auth(self) -> PluginPipeline> { + self.push(AuthPlugin) + } +} + +let pipeline = PluginPipeline::new() + .push(LoggingPlugin) + // Our custom method! + .with_auth(); +``` + ## Builders The service builder is the primary public API, generated for every [Smithy Service](https://awslabs.github.io/smithy/2.0/spec/service-types.html). @@ -430,56 +512,88 @@ At a high-level, the service builder takes as input a function for each Smithy O You can create an instance of a service builder by calling either `builder_without_plugins` or `builder_with_plugins` on the corresponding service struct. -> Plugins? What plugins? Don't worry, they'll be covered in a [dedicated section](#plugins) later on! - -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::routing::Route; /// The service builder for [`PokemonService`]. /// /// Constructed via [`PokemonService::builder`]. -pub struct PokemonServiceBuilder { +pub struct PokemonServiceBuilder { capture_pokemon_operation: Option>, empty_operation: Option>, get_pokemon_species: Option>, get_server_statistics: Option>, get_storage: Option>, health_check_operation: Option>, - plugin: Plugin + http_plugin: HttpPlugin, + model_plugin: ModelPlugin } ``` The builder has two setter methods for each [Smithy Operation](https://awslabs.github.io/smithy/2.0/spec/service-types.html#operation) in the [Smithy Service](https://awslabs.github.io/smithy/2.0/spec/service-types.html#service): ```rust,ignore - /// Sets the [`GetPokemonSpecies`](crate::operation_shape::GetPokemonSpecies) operation. - /// - /// This should be an async function satisfying the [`Handler`](aws_smithy_http_server::operation::Handler) trait. - /// See the [operation module documentation](aws_smithy_http_server::operation) for more information. - pub fn get_pokemon_species( - self, - handler: HandlerType, - ) -> Self + pub fn get_pokemon_species(self, handler: HandlerType) -> Self + where + HandlerType:Handler, + + ModelPlugin: Plugin< + RestJson1, + GetPokemonSpecies, + IntoService + >, + UpgradePlugin::: Plugin< + RestJson1, + GetPokemonSpecies, + ModelPlugin::Service + >, + HttpPlugin: Plugin< + RestJson1, + GetPokemonSpecies, + UpgradePlugin::::Service + >, + { + let svc = GetPokemonSpecies::from_handler(handler); + let svc = self.model_plugin.apply(svc); + let svc = UpgradePlugin::::new() + .apply(svc); + let svc = self.http_plugin.apply(svc); + self.get_pokemon_species_custom(svc) + } + + pub fn get_pokemon_species_service(self, service: S) -> Self where - HandlerType: Handler, - Operation>: - Upgradable, + S: OperationService, + + ModelPlugin: Plugin< + RestJson1, + GetPokemonSpecies, + Normalize + >, + UpgradePlugin::: Plugin< + RestJson1, + GetPokemonSpecies, + ModelPlugin::Service + >, + HttpPlugin: Plugin< + RestJson1, + GetPokemonSpecies, + UpgradePlugin::::Service + >, { - self.get_pokemon_species_operation(GetPokemonSpecies::from_handler(handler)) + let svc = GetPokemonSpecies::from_service(service); + let svc = self.model_plugin.apply(svc); + let svc = UpgradePlugin::::new().apply(svc); + let svc = self.http_plugin.apply(svc); + self.get_pokemon_species_custom(svc) } - /// Sets the [`GetPokemonSpecies`](crate::operation_shape::GetPokemonSpecies) operation. - /// - /// This should be an [`Operation`](aws_smithy_http_server::operation::Operation) created from - /// [`GetPokemonSpecies`](crate::operation_shape::GetPokemonSpecies) using either - /// [`OperationShape::from_handler`](aws_smithy_http_server::operation::OperationShapeExt::from_handler) or - /// [`OperationShape::from_service`](aws_smithy_http_server::operation::OperationShapeExt::from_service). - pub fn get_pokemon_species_operation( - self, - operation: Operation, - ) -> Self + pub fn get_pokemon_species_custom(mut self, svc: S) -> Self where - Operation: Upgradable, + S: Service, Response = Response, Error = Infallible>, { - self.get_pokemon_species = Some(operation.upgrade(&self.plugin)) + self.get_pokemon_species = Some(Route::new(svc)); + self } ``` @@ -499,7 +613,9 @@ Both builder methods take care of: The final outcome, an instance of `PokemonService`, looks roughly like this: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# use aws_smithy_http_server::{routing::RoutingService, proto::rest_json_1::{router::RestRouter, RestJson1}}; /// The Pokémon Service allows you to retrieve information about Pokémon species. #[derive(Clone)] pub struct PokemonService { @@ -518,19 +634,22 @@ stateDiagram-v2 state "..." as C4 direction LR [*] --> in : HTTP Request - UpgradeLayer --> [*]: HTTP Response + UpgradePlugin --> [*]: HTTP Response state PokemonService { state RoutingService { - in --> UpgradeLayer: HTTP Request + in --> UpgradePlugin: HTTP Request in --> C2: HTTP Request in --> C3: HTTP Request in --> C4: HTTP Request state C1 { - state L { - state UpgradeLayer { + state HttpPlugin { + state UpgradePlugin { direction LR [*] --> S: Model Input S --> [*] : Model Output + state ModelPlugin { + S + } } } } @@ -545,159 +664,15 @@ stateDiagram-v2 C4 --> [*]: HTTP Response ``` -## Plugins - - -There are a variety of places in which the customer can apply middleware. During the build: - -- For a specific operation, for example `GetPokemonSpecies`, the model service can be wrapped by a `Layer` before passing it to `GetPokemonSpecies::from_service` constructor. -- The `Operation::layer` method can be used to apply a `Layer` to a specific operation _after_ it's been upgraded. - -After the build is finalized: - -- The entire `PokemonService` HTTP service can be wrapped by a `Layer`. -- Every `Route` in the `Router` can be wrapped by a `Layer` using `PokemonService::layer`. - -Although this provides a reasonably "complete" API, it can be cumbersome in some use cases. Suppose a customer wants to log the operation name when a request is routed to said operation. Writing a `Layer`, `NameLogger`, to log an operation name is simple, however with the current API the customer is forced to do the following - -```rust,ignore -let get_pokemon_species = GetPokemonSpecies::from_handler(/* handler */).layer(NameLogger::new("GetPokemonSpecies")); -let get_storage = GetStorage::from_handler(/* handler */).layer(NameLogger::new("GetStorage")); -let do_nothing = DoNothing::from_handler(/* handler */).layer(NameLogger::new("DoNothing")); -/* Repeat for every route... */ -``` - -Note that `PokemonService::layer` cannot be used here because it applies a _single_ layer uniformly across all `Route`s stored in the `Router`. - -```rust,ignore -impl PokemonService { - /// Applies a [`Layer`](tower::Layer) uniformly to all routes. - pub fn layer(self, layer: &L) -> PokemonService - where - L: Layer, - { - PokemonService { - router: self.router.map(|s| s.layer(layer)), - } - } -} -``` - -The plugin system solves the general problem of modifying `Operation` prior to the upgrade procedure in a way parameterized by the protocol and operation marker structures. This parameterization removes the excessive boilerplate above. - -The central trait is [`Plugin`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/plugin.rs#L31-L41): - -```rust,ignore -/// A mapping from one [`Operation`] to another. Used to modify the behavior of -/// [`Upgradable`](crate::operation::Upgradable) and therefore the resulting service builder. -/// -/// The generics `Protocol` and `Op` allow the behavior to be parameterized. -pub trait Plugin { - type Service; - type Layer; - - /// Maps an [`Operation`] to another. - fn map(&self, input: Operation) -> Operation; -} -``` - -The `Upgradable::upgrade` method on `Operation`, previously presented in [Upgrading a Model Service](#upgrading-a-model-service), is more accurately: - -```rust,ignore - /// Takes the [`Operation`](Operation), applies [`Plugin`], then applies [`UpgradeLayer`] to - /// the modified `S`, then finally applies the modified `L`. - /// - /// The composition is made explicit in the method constraints and return type. - fn upgrade(self, plugin: &Pl) -> Route { - let mapped = plugin.map(self); - let layer = Stack::new(UpgradeLayer::new(), mapped.layer); - Route::new(layer.layer(mapped.inner)) - } -``` - -```mermaid -stateDiagram-v2 - direction TB - Op1: Operation#60;S1, L1#62; - state Op1 { - direction LR - [*] --> S1 : HTTP Request - S1 --> [*]: HTTP Response - state L1 { - Upgrade1 : Upgrade - state Upgrade1 { - S1 - } - } - - } - - Op2: Operation#60;S2, L2#62; - state Op2 { - direction LR - [*] --> S2: HTTP Request - S2 --> [*]: HTTP Response - state L2 { - Upgrade2 : Upgrade - state Upgrade2 { - S2 - } - } - } - - Op1 --> Op2 : Plugin#colon;#colon;map -``` - -An example `Plugin` implementation can be found in [/examples/pokemon-service/src/plugin.rs](https://github.com/awslabs/smithy-rs/blob/main/examples/pokemon-service/src/plugin.rs). - -The service builder API requires plugins to be specified upfront - they must be passed as an argument to `builder_with_plugins` and cannot be modified afterwards. -This constraint is in place to ensure that all handlers are upgraded using the same set of plugins. - -You might find yourself wanting to apply _multiple_ plugins to your service. -This can be accommodated via [`PluginPipeline`]. - -```rust,ignore -use aws_smithy_http_server::plugin::PluginPipeline; -# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; -# use aws_smithy_http_server::plugin::IdentityPlugin as MetricsPlugin; - -let pipeline = PluginPipeline::new().push(LoggingPlugin).push(MetricsPlugin); -``` - -The plugins' runtime logic is executed in registration order. -In the example above, `LoggingPlugin` would run first, while `MetricsPlugin` is executed last. - -If you are vending a plugin, you can leverage `PluginPipeline` as an extension point: you can add custom methods to it using an extension trait. -For example: - -```rust,ignore -use aws_smithy_http_server::plugin::{PluginPipeline, PluginStack}; -# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; -# use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin; - -pub trait AuthPluginExt { - fn with_auth(self) -> PluginPipeline>; -} - -impl AuthPluginExt for PluginPipeline { - fn with_auth(self) -> PluginPipeline> { - self.push(AuthPlugin) - } -} - -let pipeline = PluginPipeline::new() - .push(LoggingPlugin) - // Our custom method! - .with_auth(); -``` - ## Accessing Unmodelled Data -An additional omitted detail is that we provide an "escape hatch" allowing `Handler`s and `OperationService`s to accept data that isn't modelled. In addition to accepting `Op::Input` they can accept additional arguments which implement the [`FromParts`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/request.rs#L114-L121) trait: - -```rust,ignore -use http::request::Parts; +An additional omitted detail is that we provide an "escape hatch" allowing `Handler`s and `OperationService`s to accept data that isn't modelled. In addition to accepting `Op::Input` they can accept additional arguments which implement the [`FromParts`](https://docs.rs/aws-smithy-http-server/latest/aws_smithy_http_server/request/trait.FromParts.html) trait: +```rust +# extern crate aws_smithy_http_server; +# extern crate http; +# use http::request::Parts; +# use aws_smithy_http_server::response::IntoResponse; /// Provides a protocol aware extraction from a [`Request`]. This borrows the /// [`Parts`], in contrast to [`FromRequest`]. pub trait FromParts: Sized { @@ -707,9 +682,14 @@ pub trait FromParts: Sized { /// Extracts `self` from a [`Parts`] synchronously. fn from_parts(parts: &mut Parts) -> Result; } +# use aws_smithy_http_server::request::FromParts as FP; +# impl> FromParts

for T { +# type Rejection = >::Rejection; +# fn from_parts(parts: &mut Parts) -> Result { >::from_parts(parts) } +# } ``` -This differs from `FromRequest` trait, introduced in [Serialization and Deserialization](#serialization-and-deserialization), as it's synchronous and has non-consuming access to [`Parts`](https://docs.rs/http/0.2.8/http/request/struct.Parts.html), rather than the entire [Request](https://docs.rs/http/0.2.8/http/request/struct.Request.html). +This differs from `FromRequest` trait, introduced in [Serialization and Deserialization](#serialization-and-deserialization), as it's synchronous and has non-consuming access to [`Parts`](https://docs.rs/http/latest/http/request/struct.Parts.html), rather than the entire [Request](https://docs.rs/http/latest/http/request/struct.Request.html). ```rust,ignore pub struct Parts { @@ -724,7 +704,14 @@ pub struct Parts { This is commonly used to access types stored within [`Extensions`](https://docs.rs/http/0.2.8/http/struct.Extensions.html) which have been inserted by a middleware. An `Extension` struct implements `FromParts` to support this use case: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# extern crate http; +# extern crate thiserror; +# use aws_smithy_http_server::{body::BoxBody, request::FromParts, response::IntoResponse}; +# use http::status::StatusCode; +# use thiserror::Error; +# fn empty() -> BoxBody { todo!() } /// Generic extension type stored in and extracted from [request extensions]. /// /// This is commonly used to share state across handlers. diff --git a/design/src/server/instrumentation.md b/design/src/server/instrumentation.md index b24ba6775cdfba25ebe87d3bf77e00b2e1d955f8..dfb5ee52520faefb45daa66452ca14cc11544230 100644 --- a/design/src/server/instrumentation.md +++ b/design/src/server/instrumentation.md @@ -19,7 +19,11 @@ RUST_LOG=aws_smithy_http_server=warn,aws_smithy_http_server_python=error and -```rust,ignore,ignore +```rust +# extern crate tracing_subscriber; +# extern crate tracing; +# use tracing_subscriber::filter; +# use tracing::Level; let filter = filter::Targets::new().with_target("aws_smithy_http_server", Level::DEBUG); ``` @@ -55,14 +59,24 @@ Smithy provides an out-the-box middleware which: This is enabled via the `instrument` method provided by the `aws_smithy_http_server::instrumentation::InstrumentExt` trait. -```rust,ignore -use aws_smithy_http_server::instrumentation::InstrumentExt; - -let plugins = PluginPipeline::new().instrument(); -let app = PokemonService::builder_with_plugins(plugins) - .get_pokemon_species(/* handler */) +```rust,no_run +# extern crate aws_smithy_http_server; +# extern crate pokemon_service_server_sdk; +# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, input::*, output::*, error::*}; +# let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +use aws_smithy_http_server::{ + instrumentation::InstrumentExt, + plugin::{IdentityPlugin, PluginPipeline} +}; +use pokemon_service_server_sdk::PokemonService; + +let http_plugins = PluginPipeline::new().instrument(); +let app = PokemonService::builder_with_plugins(http_plugins, IdentityPlugin) + .get_pokemon_species(handler) /* ... */ - .build(); + .build() + .unwrap(); +# let app: PokemonService = app; ``` @@ -71,7 +85,10 @@ let app = PokemonService::builder_with_plugins(plugins) The Pokémon service example, located at `/examples/pokemon-service`, sets up a `tracing` `Subscriber` as follows: -```rust,ignore,ignore +```rust +# extern crate tracing_subscriber; +use tracing_subscriber::{prelude::*, EnvFilter}; + /// Setup `tracing::subscriber` to read the log level from RUST_LOG environment variable. pub fn setup_tracing() { let format = tracing_subscriber::fmt::layer().pretty(); diff --git a/design/src/server/middleware.md b/design/src/server/middleware.md index eb8d46c9ae287f85bb33fdb4b5d6b5204abd8d22..a04ae3abec1f3adc16c180a441543c26bc32759c 100644 --- a/design/src/server/middleware.md +++ b/design/src/server/middleware.md @@ -49,7 +49,7 @@ The `Service` trait can be thought of as an asynchronous function from a request Middleware in `tower` typically conforms to the following pattern, a `Service` implementation of the form -```rust,ignore +```rust pub struct NewService { inner: S, /* auxillary data */ @@ -58,7 +58,11 @@ pub struct NewService { and a complementary -```rust,ignore +```rust +# extern crate tower; +# pub struct NewService { inner: S } +use tower::{Layer, Service}; + pub struct NewLayer { /* auxiliary data */ } @@ -129,16 +133,30 @@ stateDiagram-v2 where `UpgradeLayer` is the `Layer` converting Smithy model structures to HTTP structures and the `RoutingService` is responsible for routing requests to the appropriate operation. -### A) Outer Middleware +### A. Outer Middleware The output of the Smithy service builder provides the user with a `Service` implementation. A `Layer` can be applied around the entire `Service`. -```rust,ignore +```rust,no_run +# extern crate aws_smithy_http_server; +# extern crate pokemon_service_server_sdk; +# extern crate tower; +# use std::time::Duration; +# struct TimeoutLayer; +# impl TimeoutLayer { fn new(t: Duration) -> Self { Self }} +# impl Layer for TimeoutLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } } +# use pokemon_service_server_sdk::{input::*, output::*, error::*}; +# let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +use pokemon_service_server_sdk::PokemonService; +use tower::Layer; + // This is a HTTP `Service`. let app /* : PokemonService> */ = PokemonService::builder_without_plugins() - .get_pokemon_species(/* handler */) + .get_pokemon_species(handler) /* ... */ - .build(); + .build() + .unwrap(); +# let app: PokemonService = app; // Construct `TimeoutLayer`. let timeout_layer = TimeoutLayer::new(Duration::from_secs(3)); @@ -147,76 +165,98 @@ let timeout_layer = TimeoutLayer::new(Duration::from_secs(3)); let app = timeout_layer.layer(app); ``` -### B) Route Middleware +### B. Route Middleware A _single_ layer can be applied to _all_ routes inside the `Router`. This exists as a method on the output of the service builder. -```rust,ignore -// Construct `TraceLayer`. -let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); +```rust,no_run +# extern crate tower; +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +# use tower::{util::service_fn, Layer}; +# use std::time::Duration; +# use pokemon_service_server_sdk::{input::*, output::*, error::*}; +# let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +# struct MetricsLayer; +# impl MetricsLayer { pub fn new() -> Self { Self } } +# impl Layer for MetricsLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } } +use pokemon_service_server_sdk::PokemonService; + +// Construct `MetricsLayer`. +let metrics_layer = MetricsLayer::new(); let app /* : PokemonService> */ = PokemonService::builder_without_plugins() - .get_pokemon_species(/* handler */) + .get_pokemon_species(handler) /* ... */ .build() + .unwrap() + # ; let app: PokemonService = app; + # app // Apply HTTP logging after routing. - .layer(&trace_layer); + .layer(&metrics_layer); ``` Note that requests pass through this middleware immediately _after_ routing succeeds and therefore will _not_ be encountered if routing fails. This means that the [TraceLayer](https://docs.rs/tower-http/latest/tower_http/trace/struct.TraceLayer.html) in the example above does _not_ provide logs unless routing has completed. This contrasts to [middleware A](#a-outer-middleware), which _all_ requests/responses pass through when entering/leaving the service. -### C) Operation Specific HTTP Middleware +### C. Operation Specific HTTP Middleware A "HTTP layer" can be applied to specific operations. -```rust,ignore -// Construct `TraceLayer`. -let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); - -// Apply HTTP logging to only the `GetPokemonSpecies` operation. -let layered_handler = GetPokemonSpecies::from_handler(/* handler */).layer(trace_layer); - -let app /* : PokemonService> */ = PokemonService::builder_without_plugins() - .get_pokemon_species_operation(layered_handler) +```rust,no_run +# extern crate tower; +# extern crate pokemon_service_server_sdk; +# extern crate aws_smithy_http_server; +# use tower::{util::service_fn, Layer}; +# use std::time::Duration; +# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, PokemonService, input::*, output::*, error::*}; +# use aws_smithy_http_server::{operation::OperationShapeExt, plugin::*, operation::*}; +# let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +# struct LoggingLayer; +# impl LoggingLayer { pub fn new() -> Self { Self } } +# impl Layer for LoggingLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } } +// Construct `LoggingLayer`. +let logging_plugin = LayerPlugin(LoggingLayer::new()); +let logging_plugin = filter_by_operation_id(logging_plugin, |name| name == GetPokemonSpecies::ID); +let http_plugins = PluginPipeline::new().push(logging_plugin); + +let app /* : PokemonService> */ = PokemonService::builder_with_plugins(http_plugins, IdentityPlugin) + .get_pokemon_species(handler) /* ... */ - .build(); + .build() + .unwrap(); +# let app: PokemonService = app; ``` This middleware transforms the operations HTTP requests and responses. -### D) Operation Specific Model Middleware +### D. Operation Specific Model Middleware A "model layer" can be applied to specific operations. -```rust +```rust,no_run # extern crate tower; # extern crate pokemon_service_server_sdk; # extern crate aws_smithy_http_server; # use tower::{util::service_fn, Layer}; -# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, PokemonService, input::*, output::*, error::*}; -# use aws_smithy_http_server::operation::OperationShapeExt; +# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, input::*, output::*, error::*}; # let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +# use aws_smithy_http_server::{operation::*, plugin::*}; # struct BufferLayer; # impl BufferLayer { pub fn new(size: usize) -> Self { Self } } # impl Layer for BufferLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } } -// A handler `Service`. -let handler_svc = service_fn(handler); +use pokemon_service_server_sdk::PokemonService; // Construct `BufferLayer`. -let buffer_layer = BufferLayer::new(3); - -// Apply a 3 item buffer to `handler_svc`. -let handler_svc = buffer_layer.layer(handler_svc); - -let layered_handler = GetPokemonSpecies::from_service(handler_svc); +let buffer_plugin = LayerPlugin(BufferLayer::new(3)); +let buffer_plugin = filter_by_operation_id(buffer_plugin, |name| name != GetPokemonSpecies::ID); +let model_plugins = PluginPipeline::new().push(buffer_plugin); -let app /* : PokemonService> */ = PokemonService::builder_without_plugins() - .get_pokemon_species_operation(layered_handler) +let app /* : PokemonService> */ = PokemonService::builder_with_plugins(IdentityPlugin, model_plugins) + .get_pokemon_species(handler) /* ... */ .build() - # ;Result::<(), ()>::Ok(()) .unwrap(); -# let app: Result, _> = app; +# let app: PokemonService = app; ``` In contrast to [position C](#c-operation-specific-http-middleware), this middleware transforms the operations modelled inputs to modelled outputs. @@ -227,9 +267,12 @@ Suppose we want to apply a different `Layer` to every operation. In this case, p Consider the following middleware: -```rust,ignore +```rust +# extern crate tower; +use std::task::{Context, Poll}; +use tower::Service; + /// A [`Service`] that adds a print log. -#[derive(Clone, Debug)] pub struct PrintService { inner: S, name: &'static str, @@ -252,77 +295,76 @@ where self.inner.call(req) } } - -/// A [`Layer`] which constructs the [`PrintService`]. -#[derive(Debug)] -pub struct PrintLayer { - name: &'static str, -} -impl Layer for PrintLayer { - type Service = PrintService; - - fn layer(&self, service: S) -> Self::Service { - PrintService { - inner: service, - name: self.name, - } - } -} ``` The plugin system provides a way to construct then apply `Layer`s in position [C](#c-operation-specific-http-middleware) and [D](#d-operation-specific-model-middleware), using the [protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/index.html) and [operation shape](https://awslabs.github.io/smithy/2.0/spec/service-types.html#service-operations) as parameters. -An example of a `PrintPlugin` which applies a layer printing the operation name: +An example of a `PrintPlugin` which prints the operation name: + +```rust +# extern crate aws_smithy_http_server; +# pub struct PrintService { inner: S, name: &'static str } +use aws_smithy_http_server::{plugin::Plugin, operation::OperationShape}; -```rust,ignore -/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. +/// A [`Plugin`] for a service builder to add a [`PrintService`] over operations. #[derive(Debug)] pub struct PrintPlugin; -impl Plugin for PrintPlugin +impl Plugin for PrintPlugin where Op: OperationShape, { - type Service = S; - type Layer = Stack; + type Service = PrintService; - fn map(&self, input: Operation) -> Operation { - input.layer(PrintLayer { name: Op::NAME }) + fn apply(&self, inner: S) -> Self::Service { + PrintService { name: Op::ID.name(), inner } } } ``` -An alternative example which applies a layer for a given protocol: +An alternative example which prints the protocol name: + +```rust +# extern crate aws_smithy_http_server; +# pub struct PrintService { name: &'static str, inner: S} +use aws_smithy_http_server::{ + plugin::Plugin, + proto::{ + aws_json_10::AwsJson1_0, + rest_xml::RestXml, + } +}; -```rust,ignore -/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. +/// A [`Plugin`] for a service builder to add a [`PrintService`] over operations. #[derive(Debug)] pub struct PrintPlugin; -impl Plugin for PrintPlugin +impl Plugin for PrintPlugin { - type Service = S; - type Layer = Stack; + type Service = PrintService; - fn map(&self, input: Operation) -> Operation { - input.layer(PrintLayer { name: "AWS REST JSON v1" }) + fn apply(&self, inner: S) -> Self::Service { + PrintService { name: "AWS JSON 1.0", inner } } } -impl Plugin for PrintPlugin +impl Plugin for PrintPlugin { - type Service = S; - type Layer = Stack; + type Service = PrintService; - fn map(&self, input: Operation) -> Operation { - input.layer(PrintLayer { name: "AWS REST XML" }) + fn apply(&self, inner: S) -> Self::Service { + PrintService { name: "AWS REST XML", inner } } } ``` You can provide a custom method to add your plugin to a `PluginPipeline` via an extension trait: -```rust,ignore +```rust +# extern crate aws_smithy_http_server; +# pub struct PrintPlugin; +use aws_smithy_http_server::plugin::{PluginPipeline, PluginStack}; + /// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`]. pub trait PrintExt { /// Causes all operations to print the operation name when called. @@ -340,15 +382,29 @@ impl PrintExt for PluginPipeline Plugin for PrintPlugin { type Service = S; fn apply(&self, svc: S) -> Self::Service { svc }} +# trait PrintExt { fn print(self) -> PluginPipeline>; } +# impl PrintExt for PluginPipeline { fn print(self) -> PluginPipeline> { self.push(PrintPlugin) }} +# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, input::*, output::*, error::*}; +# let handler = |req: GetPokemonSpeciesInput| async { Result::::Ok(todo!()) }; +use aws_smithy_http_server::plugin::{IdentityPlugin, PluginPipeline}; +use pokemon_service_server_sdk::PokemonService; + +let http_plugins = PluginPipeline::new() // [..other plugins..] // The custom method! .print(); -let app /* : PokemonService> */ = PokemonService::builder_with_plugins(plugin_pipeline) - .get_pokemon_species_operation(layered_handler) +let app /* : PokemonService> */ = PokemonService::builder_with_plugins(http_plugins, IdentityPlugin) + .get_pokemon_species(handler) /* ... */ - .build(); + .build() + .unwrap(); +# let app: PokemonService = app; ``` The custom `print` method hides the details of the `Plugin` trait from the average consumer. diff --git a/examples/pokemon-service-common/tests/plugins_execution_order.rs b/examples/pokemon-service-common/tests/plugins_execution_order.rs index 4965bbf9d83a7c17c18d2f9112b63dbeb7897c17..901ed89fc1d67ceb9e162e555004c93d8450f8aa 100644 --- a/examples/pokemon-service-common/tests/plugins_execution_order.rs +++ b/examples/pokemon-service-common/tests/plugins_execution_order.rs @@ -11,11 +11,7 @@ use std::{ }; use aws_smithy_http::body::SdkBody; -use aws_smithy_http_server::{ - operation::Operation, - plugin::{Plugin, PluginPipeline}, -}; -use tower::layer::util::Stack; +use aws_smithy_http_server::plugin::{IdentityPlugin, Plugin, PluginPipeline}; use tower::{Layer, Service}; use pokemon_service_client::{operation::do_nothing::DoNothingInput, Config}; @@ -41,9 +37,10 @@ async fn plugin_layers_are_executed_in_registration_order() { let pipeline = PluginPipeline::new() .push(SentinelPlugin::new("first", output.clone())) .push(SentinelPlugin::new("second", output.clone())); - let mut app = pokemon_service_server_sdk::PokemonService::builder_with_plugins(pipeline) - .do_nothing(do_nothing) - .build_unchecked(); + let mut app = + pokemon_service_server_sdk::PokemonService::builder_with_plugins(pipeline, IdentityPlugin) + .do_nothing(do_nothing) + .build_unchecked(); let request = DoNothingInput::builder() .build() .unwrap() @@ -68,15 +65,15 @@ impl SentinelPlugin { } } -impl Plugin for SentinelPlugin { - type Service = S; - type Layer = Stack; +impl Plugin for SentinelPlugin { + type Service = SentinelService; - fn map(&self, input: Operation) -> Operation { - input.layer(SentinelLayer { + fn apply(&self, inner: S) -> Self::Service { + SentinelService { + inner, name: self.name, output: self.output.clone(), - }) + } } } diff --git a/examples/pokemon-service/src/main.rs b/examples/pokemon-service/src/main.rs index db7878995b53176e6927ba755fd4e233d05d605b..76a8b8e594cb4ebbec93f4ace2807533a9da89d0 100644 --- a/examples/pokemon-service/src/main.rs +++ b/examples/pokemon-service/src/main.rs @@ -10,7 +10,7 @@ use std::{net::SocketAddr, sync::Arc}; use aws_smithy_http_server::{ extension::OperationExtensionExt, instrumentation::InstrumentExt, - plugin::{alb_health_check::AlbHealthCheckLayer, PluginPipeline}, + plugin::{alb_health_check::AlbHealthCheckLayer, IdentityPlugin, PluginPipeline}, request::request_id::ServerRequestIdProviderLayer, AddExtensionLayer, }; @@ -54,11 +54,11 @@ pub async fn main() { // Adds `tracing` spans and events to the request lifecycle. .instrument() // Handle `/ping` health check requests. - .http_layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { + .layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { StatusCode::OK })); - let app = PokemonService::builder_with_plugins(plugins) + let app = PokemonService::builder_with_plugins(plugins, IdentityPlugin) // Build a registry containing implementations to all the operations in the service. These // are async functions or async closures that take as input the operation's input and // return the operation's output. diff --git a/examples/pokemon-service/src/plugin.rs b/examples/pokemon-service/src/plugin.rs index ea6ee09d914e0997f56ed8829ab34a20e00e8bc0..e4236631649526a97aac6e67473beec469eaa179 100644 --- a/examples/pokemon-service/src/plugin.rs +++ b/examples/pokemon-service/src/plugin.rs @@ -6,11 +6,11 @@ //! Provides an example [`Plugin`] implementation - [`PrintPlugin`]. use aws_smithy_http_server::{ - operation::{Operation, OperationShape}, + operation::OperationShape, plugin::{Plugin, PluginPipeline, PluginStack}, shape_id::ShapeId, }; -use tower::{layer::util::Stack, Layer, Service}; +use tower::Service; use std::task::{Context, Poll}; @@ -38,49 +38,30 @@ where self.inner.call(req) } } - -/// A [`Layer`] which constructs the [`PrintService`]. -#[derive(Debug)] -pub struct PrintLayer { - id: ShapeId, -} -impl Layer for PrintLayer { - type Service = PrintService; - - fn layer(&self, service: S) -> Self::Service { - PrintService { - inner: service, - id: self.id.clone(), - } - } -} - /// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. #[derive(Debug)] pub struct PrintPlugin; -impl Plugin for PrintPlugin +impl Plugin for PrintPlugin where Op: OperationShape, { - type Service = S; - type Layer = Stack; + type Service = PrintService; - fn map(&self, input: Operation) -> Operation { - input.layer(PrintLayer { id: Op::NAME }) + fn apply(&self, inner: S) -> Self::Service { + PrintService { inner, id: Op::ID } } } - /// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`]. -pub trait PrintExt { +pub trait PrintExt { /// Causes all operations to print the operation name when called. /// /// This works by applying the [`PrintPlugin`]. - fn print(self) -> PluginPipeline>; + fn print(self) -> PluginPipeline>; } -impl PrintExt for PluginPipeline { - fn print(self) -> PluginPipeline> { +impl PrintExt for PluginPipeline { + fn print(self) -> PluginPipeline> { self.push(PrintPlugin) } } diff --git a/rust-runtime/aws-smithy-http-server/src/extension.rs b/rust-runtime/aws-smithy-http-server/src/extension.rs index 371df0152993a2fa117ea010923305718c1333c1..bd25a3f5f90b4562a24b96511674dd89492710c8 100644 --- a/rust-runtime/aws-smithy-http-server/src/extension.rs +++ b/rust-runtime/aws-smithy-http-server/src/extension.rs @@ -22,14 +22,13 @@ use std::hash::Hash; use std::{fmt, fmt::Debug, future::Future, ops::Deref, pin::Pin, task::Context, task::Poll}; -use crate::extension; use futures_util::ready; use futures_util::TryFuture; use thiserror::Error; -use tower::{layer::util::Stack, Layer, Service}; +use tower::Service; -use crate::operation::{Operation, OperationShape}; -use crate::plugin::{plugin_from_operation_id_fn, OperationIdFn, Plugin, PluginPipeline, PluginStack}; +use crate::operation::OperationShape; +use crate::plugin::{Plugin, PluginPipeline, PluginStack}; use crate::shape_id::ShapeId; pub use crate::request::extension::{Extension, MissingExtension}; @@ -106,23 +105,8 @@ where } } -/// A [`Layer`] applying the [`OperationExtensionService`] to an inner [`Service`]. -#[derive(Debug, Clone)] -pub struct OperationExtensionLayer(OperationExtension); - -impl Layer for OperationExtensionLayer { - type Service = OperationExtensionService; - - fn layer(&self, inner: S) -> Self::Service { - OperationExtensionService { - inner, - operation_extension: self.0.clone(), - } - } -} - -/// A [`Plugin`] which applies [`OperationExtensionLayer`] to every operation. -pub struct OperationExtensionPlugin(OperationIdFn OperationExtensionLayer>); +/// A [`Plugin`] which applies [`OperationExtensionService`] to every operation. +pub struct OperationExtensionPlugin; impl fmt::Debug for OperationExtensionPlugin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -130,32 +114,31 @@ impl fmt::Debug for OperationExtensionPlugin { } } -impl Plugin for OperationExtensionPlugin +impl Plugin for OperationExtensionPlugin where Op: OperationShape, { - type Service = S; - type Layer = Stack; + type Service = OperationExtensionService; - fn map(&self, input: Operation) -> Operation { - OperationExtensionLayer> as Plugin>::map(&self.0, input) + fn apply(&self, inner: S) -> Self::Service { + OperationExtensionService { + inner, + operation_extension: OperationExtension(Op::ID), + } } } /// An extension trait on [`PluginPipeline`] allowing the application of [`OperationExtensionPlugin`]. /// /// See [`module`](crate::extension) documentation for more info. -pub trait OperationExtensionExt

{ +pub trait OperationExtensionExt { /// Apply the [`OperationExtensionPlugin`], which inserts the [`OperationExtension`] into every [`http::Response`]. - fn insert_operation_extension(self) -> PluginPipeline>; + fn insert_operation_extension(self) -> PluginPipeline>; } -impl

OperationExtensionExt

for PluginPipeline

{ - fn insert_operation_extension(self) -> PluginPipeline> { - let plugin = OperationExtensionPlugin(plugin_from_operation_id_fn(|shape_id| { - OperationExtensionLayer(extension::OperationExtension(shape_id)) - })); - self.push(plugin) +impl OperationExtensionExt for PluginPipeline { + fn insert_operation_extension(self) -> PluginPipeline> { + self.push(OperationExtensionPlugin) } } @@ -201,9 +184,9 @@ impl Deref for RuntimeErrorExtension { #[cfg(test)] mod tests { - use tower::{service_fn, ServiceExt}; + use tower::{service_fn, Layer, ServiceExt}; - use crate::{operation::OperationShapeExt, proto::rest_json_1::RestJson1}; + use crate::{plugin::PluginLayer, proto::rest_json_1::RestJson1}; use super::*; @@ -226,7 +209,7 @@ mod tests { struct DummyOp; impl OperationShape for DummyOp { - const NAME: ShapeId = ShapeId::new( + const ID: ShapeId = ShapeId::new( "com.amazonaws.ebs#CompleteSnapshot", "com.amazonaws.ebs", "CompleteSnapshot", @@ -238,18 +221,16 @@ mod tests { } // Apply `Plugin`. - let operation = DummyOp::from_handler(|_| async { Ok(()) }); let plugins = PluginPipeline::new().insert_operation_extension(); - let op = Plugin::::map(&plugins, operation); // Apply `Plugin`s `Layer`. - let layer = op.layer; + let layer = PluginLayer::new::(plugins); let svc = service_fn(|_: http::Request<()>| async { Ok::<_, ()>(http::Response::new(())) }); let svc = layer.layer(svc); // Check for `OperationExtension`. let response = svc.oneshot(http::Request::new(())).await.unwrap(); - let expected = DummyOp::NAME; + let expected = DummyOp::ID; let actual = response.extensions().get::().unwrap(); assert_eq!(actual.0, expected); } diff --git a/rust-runtime/aws-smithy-http-server/src/instrumentation/layer.rs b/rust-runtime/aws-smithy-http-server/src/instrumentation/layer.rs deleted file mode 100644 index c070d1297e08b1e12b9e65c0a4aec8944a7dcaf6..0000000000000000000000000000000000000000 --- a/rust-runtime/aws-smithy-http-server/src/instrumentation/layer.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use tower::Layer; - -use crate::shape_id::ShapeId; - -use super::{InstrumentOperation, MakeIdentity}; - -/// A [`Layer`] used to apply [`InstrumentOperation`]. -#[derive(Debug)] -pub struct InstrumentLayer { - operation_id: ShapeId, - make_request: RequestMakeFmt, - make_response: ResponseMakeFmt, -} - -impl InstrumentLayer { - /// Constructs a new [`InstrumentLayer`] with no data redacted. - pub fn new(operation_id: ShapeId) -> Self { - Self { - operation_id, - make_request: MakeIdentity, - make_response: MakeIdentity, - } - } -} - -impl InstrumentLayer { - /// Configures the request format. - /// - /// The argument is typically [`RequestFmt`](super::sensitivity::RequestFmt). - pub fn request_fmt(self, make_request: R) -> InstrumentLayer { - InstrumentLayer { - operation_id: self.operation_id, - make_request, - make_response: self.make_response, - } - } - - /// Configures the response format. - /// - /// The argument is typically [`ResponseFmt`](super::sensitivity::ResponseFmt). - pub fn response_fmt(self, make_response: R) -> InstrumentLayer { - InstrumentLayer { - operation_id: self.operation_id, - make_request: self.make_request, - make_response, - } - } -} - -impl Layer for InstrumentLayer -where - RequestMakeFmt: Clone, - ResponseMakeFmt: Clone, -{ - type Service = InstrumentOperation; - - fn layer(&self, service: S) -> Self::Service { - InstrumentOperation::new(service, self.operation_id.clone()) - .request_fmt(self.make_request.clone()) - .response_fmt(self.make_response.clone()) - } -} diff --git a/rust-runtime/aws-smithy-http-server/src/instrumentation/mod.rs b/rust-runtime/aws-smithy-http-server/src/instrumentation/mod.rs index 2519e10137831d2bf84fbb508dc81813e3a0ffee..bad93a9ae140d9605374646c42ba2f500456200e 100644 --- a/rust-runtime/aws-smithy-http-server/src/instrumentation/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/instrumentation/mod.rs @@ -21,7 +21,7 @@ //! # } //! # async fn example() { //! # let service = service_fn(service); -//! # const NAME: ShapeId = ShapeId::new("namespace#foo-operation", "namespace", "foo-operation"); +//! # const ID: ShapeId = ShapeId::new("namespace#foo-operation", "namespace", "foo-operation"); //! let request = Request::get("http://localhost/a/b/c/d?bar=hidden") //! .header("header-name-a", "hidden") //! .body(()) @@ -49,7 +49,7 @@ //! } //! }) //! .status_code(); -//! let mut service = InstrumentOperation::new(service, NAME) +//! let mut service = InstrumentOperation::new(service, ID) //! .request_fmt(request_fmt) //! .response_fmt(response_fmt); //! @@ -59,14 +59,12 @@ //! //! [sensitive trait]: https://awslabs.github.io/smithy/1.0/spec/core/documentation-traits.html?highlight=sensitive%20trait#sensitive-trait -mod layer; mod plugin; pub mod sensitivity; mod service; use std::fmt::{Debug, Display}; -pub use layer::*; pub use plugin::*; pub use service::*; diff --git a/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs b/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs index 7da5e2fbeb8d8a0ca6d610bfdbecbe693e7c95c4..ba7104801d92bfdfeafd5d1b8e763c99090d2b2a 100644 --- a/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs +++ b/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs @@ -3,48 +3,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -use tower::layer::util::Stack; - use crate::plugin::{PluginPipeline, PluginStack}; -use crate::{ - operation::{Operation, OperationShape}, - plugin::Plugin, -}; +use crate::{operation::OperationShape, plugin::Plugin}; -use super::{layer::InstrumentLayer, sensitivity::Sensitivity}; +use super::sensitivity::Sensitivity; +use super::InstrumentOperation; -/// A [`Plugin`] which applies [`InstrumentLayer`] to all operations in the builder. +/// A [`Plugin`] which applies [`InstrumentOperation`] to every operation. #[derive(Debug)] pub struct InstrumentPlugin; -impl Plugin for InstrumentPlugin +impl Plugin for InstrumentPlugin where Op: OperationShape, Op: Sensitivity, { - type Service = S; - type Layer = Stack>; + type Service = InstrumentOperation; - fn map(&self, operation: Operation) -> Operation { - let operation_id = Op::NAME; - let layer = InstrumentLayer::new(operation_id) + fn apply(&self, svc: S) -> Self::Service { + InstrumentOperation::new(svc, Op::ID) .request_fmt(Op::request_fmt()) - .response_fmt(Op::response_fmt()); - operation.layer(layer) + .response_fmt(Op::response_fmt()) } } -/// An extension trait for applying [`InstrumentLayer`] to all operations in a service. -pub trait InstrumentExt { - /// Applies an [`InstrumentLayer`] to all operations which respects the [@sensitive] trait given on the input and +/// An extension trait for applying [`InstrumentPlugin`]. +pub trait InstrumentExt { + /// Applies an [`InstrumentOperation`] to every operation, respecting the [@sensitive] trait given on the input and /// output models. See [`InstrumentOperation`](super::InstrumentOperation) for more information. /// /// [@sensitive]: https://awslabs.github.io/smithy/2.0/spec/documentation-traits.html#sensitive-trait - fn instrument(self) -> PluginPipeline>; + fn instrument(self) -> PluginPipeline>; } -impl InstrumentExt for PluginPipeline { - fn instrument(self) -> PluginPipeline> { +impl InstrumentExt for PluginPipeline { + fn instrument(self) -> PluginPipeline> { self.push(InstrumentPlugin) } } diff --git a/rust-runtime/aws-smithy-http-server/src/instrumentation/service.rs b/rust-runtime/aws-smithy-http-server/src/instrumentation/service.rs index 76410cfa65fe2deda590632ce82a8bc9574349a3..21f211bc06c9152783a5fde901829d75b70389b4 100644 --- a/rust-runtime/aws-smithy-http-server/src/instrumentation/service.rs +++ b/rust-runtime/aws-smithy-http-server/src/instrumentation/service.rs @@ -94,12 +94,12 @@ where /// # use http::{Request, Response}; /// # async fn f(request: Request<()>) -> Result, ()> { Ok(Response::new(())) } /// # let mut svc = service_fn(f); -/// # const NAME: ShapeId = ShapeId::new("namespace#foo-operation", "namespace", "foo-operation"); +/// # const ID: ShapeId = ShapeId::new("namespace#foo-operation", "namespace", "foo-operation"); /// let request_fmt = RequestFmt::new() /// .label(|index| index == 1, None) /// .query(|_| QueryMarker { key: false, value: true }); /// let response_fmt = ResponseFmt::new().status_code(); -/// let mut svc = InstrumentOperation::new(svc, NAME) +/// let mut svc = InstrumentOperation::new(svc, ID) /// .request_fmt(request_fmt) /// .response_fmt(response_fmt); /// # svc.call(Request::new(())); diff --git a/rust-runtime/aws-smithy-http-server/src/operation/handler.rs b/rust-runtime/aws-smithy-http-server/src/operation/handler.rs index c40c0f77424d1f7622cd2495bf9c387e14b29229..28ad3fd7437f1dc4a70d0eed8a60c630adb91d1e 100644 --- a/rust-runtime/aws-smithy-http-server/src/operation/handler.rs +++ b/rust-runtime/aws-smithy-http-server/src/operation/handler.rs @@ -118,8 +118,8 @@ where /// A [`Service`] provided for every [`Handler`]. pub struct IntoService { - handler: H, - _operation: PhantomData, + pub(crate) handler: H, + pub(crate) _operation: PhantomData, } impl Clone for IntoService diff --git a/rust-runtime/aws-smithy-http-server/src/operation/mod.rs b/rust-runtime/aws-smithy-http-server/src/operation/mod.rs index b9935b86d5703dfcf0607823f495010e5f4f8010..8450da8366554b5fc088bf805f20e75c844f9f08 100644 --- a/rust-runtime/aws-smithy-http-server/src/operation/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/operation/mod.rs @@ -32,7 +32,7 @@ //! pub struct GetShopping; //! //! impl OperationShape for GetShopping { -//! const NAME: ShapeId = ShapeId::new("namespace#GetShopping", "namespace", "GetShopping"); +//! const ID: ShapeId = ShapeId::new("namespace#GetShopping", "namespace", "GetShopping"); //! //! type Input = CartIdentifier; //! type Output = ShoppingCart; @@ -40,11 +40,11 @@ //! } //! ``` //! -//! The behavior of a Smithy operation is encoded by an [`Operation`]. The [`OperationShape`] types can be used to -//! construct specific operations using [`OperationShapeExt::from_handler`] and [`OperationShapeExt::from_service`]. -//! The [from_handler](OperationShapeExt::from_handler) constructor takes a [`Handler`] whereas the -//! [from_service](OperationShapeExt::from_service) takes a [`OperationService`]. Both traits serve a similar purpose - -//! they provide a common interface over a class of structures. +//! The behavior of a Smithy operation is encoded by a [`Service`](tower::Service). The [`OperationShape`] types can +//! be used to construct specific operations using [`OperationShapeExt::from_handler`] and +//! [`OperationShapeExt::from_service`] methods. The [from_handler](OperationShapeExt::from_handler) constructor takes +//! a [`Handler`] whereas the [from_service](OperationShapeExt::from_service) takes a [`OperationService`]. Both traits +//! serve a similar purpose - they provide a common interface over a class of structures. //! //! ## [`Handler`] //! @@ -95,7 +95,7 @@ //! //! Notice the parallels between [`OperationService`] and [`Handler`]. //! -//! ## Constructing an [`Operation`] +//! ## Constructing an Operation //! //! The following is an example of using both construction approaches: //! @@ -109,7 +109,7 @@ //! # pub enum GetShoppingError {} //! # pub struct GetShopping; //! # impl OperationShape for GetShopping { -//! # const NAME: ShapeId = ShapeId::new("namespace#GetShopping", "namespace", "GetShopping"); +//! # const ID: ShapeId = ShapeId::new("namespace#GetShopping", "namespace", "GetShopping"); //! # //! # type Input = CartIdentifier; //! # type Output = ShoppingCart; @@ -134,12 +134,10 @@ //! type Future = OpFuture; //! //! fn poll_ready(&mut self, cx: &mut Context) -> Poll> { -//! // NOTE: This MUST NOT return `Err(OperationError::Model(_))`. //! todo!() //! } //! //! fn call(&mut self, request: CartIdentifier) -> Self::Future { -//! // NOTE: This MUST NOT return `Err(OperationError::Poll(_))`. //! todo!() //! } //! } @@ -150,19 +148,15 @@ //! //! ## Upgrading Smithy services to HTTP services //! -//! Both [`Handler`] and [`OperationService`] accept and return Smithy model structures. After an [`Operation`] is -//! constructed they are converted to a canonical form -//! `Service<(Op::Input, Exts), Response = Op::Output, Error = Op::Error>`. The -//! [`UpgradeLayer`] acts upon such services by converting them to -//! `Service`. +//! Both [`Handler`] and [`OperationService`] accept and return Smithy model structures. They are converted to a +//! canonical form `Service<(Op::Input, Exts), Response = Op::Output, Error = Op::Error>`. The +//! [`UpgradePlugin`] acts upon such services by converting them to +//! `Service` by applying serialization/deserialization +//! and validation specified by the Smithy contract. //! //! -//! The [`UpgradeLayer`] and it's [`Layer::Service`](tower::Layer::Service) [`Upgrade`] are both parameterized by a -//! protocol. This allows for upgrading to `Service` to be -//! protocol dependent. -//! -//! The [`Operation::upgrade`] will apply [`UpgradeLayer`] to `S` then apply the [`Layer`](tower::Layer) `L`. The -//! service builder provided to the user will perform this composition on `build`. +//! The [`UpgradePlugin`], being a [`Plugin`](crate::plugin::Plugin), is parameterized by a protocol. This allows for +//! upgrading to `Service` to be protocol dependent. //! //! [Smithy operation]: https://awslabs.github.io/smithy/2.0/spec/service-types.html#operation @@ -171,58 +165,7 @@ mod operation_service; mod shape; mod upgrade; -use tower::layer::util::{Identity, Stack}; - pub use handler::*; pub use operation_service::*; pub use shape::*; pub use upgrade::*; - -/// A Smithy operation, represented by a [`Service`](tower::Service) `S` and a [`Layer`](tower::Layer) `L`. -/// -/// The `L` is held and applied lazily during [`Upgradable::upgrade`]. -pub struct Operation { - /// The inner [`Service`](tower::Service) representing the logic of the operation. - pub inner: S, - /// The [`Layer`](tower::Layer) applied to the HTTP [`Service`](tower::Service) after `S` has been wrapped in - /// [`Upgrade`]. - pub layer: L, -} - -impl Operation { - /// Applies a [`Layer`](tower::Layer) to the operation _after_ it has been upgraded via [`Operation::upgrade`]. - pub fn layer(self, layer: NewL) -> Operation> { - Operation { - inner: self.inner, - layer: Stack::new(self.layer, layer), - } - } -} - -impl Operation> { - /// Creates an [`Operation`] from a [`Service`](tower::Service). - pub fn from_service(inner: S) -> Self - where - Op: OperationShape, - S: OperationService, - { - Self { - inner: inner.normalize(), - layer: Identity::new(), - } - } -} - -impl Operation> { - /// Creates an [`Operation`] from a [`Handler`]. - pub fn from_handler(handler: H) -> Self - where - Op: OperationShape, - H: Handler, - { - Self { - inner: handler.into_service(), - layer: Identity::new(), - } - } -} diff --git a/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs b/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs index f759f297a24113f54a3a95a59741022fc6316fd3..b9a87aa6e9e86fb0b910e4786998f43300ff8b0b 100644 --- a/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs +++ b/rust-runtime/aws-smithy-http-server/src/operation/operation_service.rs @@ -95,8 +95,8 @@ where /// A [`Service`] normalizing the request type of a [`OperationService`]. #[derive(Debug)] pub struct Normalize { - inner: S, - _operation: PhantomData, + pub(crate) inner: S, + pub(crate) _operation: PhantomData, } impl Clone for Normalize diff --git a/rust-runtime/aws-smithy-http-server/src/operation/shape.rs b/rust-runtime/aws-smithy-http-server/src/operation/shape.rs index 72f712f2c01d1d79be3e1b70508fd310264238a2..6d3d7748689e99317425ae00f9676868b84d92f0 100644 --- a/rust-runtime/aws-smithy-http-server/src/operation/shape.rs +++ b/rust-runtime/aws-smithy-http-server/src/operation/shape.rs @@ -3,15 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -use super::{Handler, IntoService, Normalize, Operation, OperationService}; +use std::marker::PhantomData; + +use super::{Handler, IntoService, Normalize, OperationService}; use crate::shape_id::ShapeId; /// Models the [Smithy Operation shape]. /// /// [Smithy Operation shape]: https://awslabs.github.io/smithy/1.0/spec/core/model.html#operation pub trait OperationShape { - /// The name of the operation. - const NAME: ShapeId; + /// The ID of the operation. + const ID: ShapeId; /// The operation input. type Input; @@ -24,22 +26,29 @@ pub trait OperationShape { /// An extension trait over [`OperationShape`]. pub trait OperationShapeExt: OperationShape { - /// Creates a new [`Operation`] for well-formed [`Handler`]s. - fn from_handler(handler: H) -> Operation> + /// Creates a new [`Service`](tower::Service), [`IntoService`], for well-formed [`Handler`]s. + fn from_handler(handler: H) -> IntoService where H: Handler, Self: Sized, { - Operation::from_handler(handler) + IntoService { + handler, + _operation: PhantomData, + } } - /// Creates a new [`Operation`] for well-formed [`Service`](tower::Service)s. - fn from_service(svc: S) -> Operation> + /// Creates a new normalized [`Service`](tower::Service), [`Normalize`], for well-formed + /// [`Service`](tower::Service)s. + fn from_service(svc: S) -> Normalize where S: OperationService, Self: Sized, { - Operation::from_service(svc) + Normalize { + inner: svc, + _operation: PhantomData, + } } } diff --git a/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs b/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs index d1c95672e4121dcdeaa93414bb7511042531a9b9..f0ed2a0604d9a040c2afc92e92cb874beee31aff 100644 --- a/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs +++ b/rust-runtime/aws-smithy-http-server/src/operation/upgrade.rs @@ -13,56 +13,50 @@ use std::{ use futures_util::ready; use pin_project_lite::pin_project; -use tower::{layer::util::Stack, util::Oneshot, Layer, Service, ServiceExt}; +use tower::{util::Oneshot, Service, ServiceExt}; use tracing::error; use crate::{ - body::BoxBody, - plugin::Plugin, - request::{FromParts, FromRequest}, - response::IntoResponse, - routing::Route, + body::BoxBody, plugin::Plugin, request::FromRequest, response::IntoResponse, runtime_error::InternalFailureException, }; -use super::{Operation, OperationShape}; +use super::OperationShape; -/// A [`Layer`] responsible for taking an operation [`Service`], accepting and returning Smithy +/// A [`Plugin`] responsible for taking an operation [`Service`], accepting and returning Smithy /// types and converting it into a [`Service`] taking and returning [`http`] types. /// /// See [`Upgrade`]. #[derive(Debug, Clone)] -pub struct UpgradeLayer { - _protocol: PhantomData, - _operation: PhantomData, - _exts: PhantomData, +pub struct UpgradePlugin { + _extractors: PhantomData, } -impl Default for UpgradeLayer { +impl Default for UpgradePlugin { fn default() -> Self { Self { - _protocol: PhantomData, - _operation: PhantomData, - _exts: PhantomData, + _extractors: PhantomData, } } } -impl UpgradeLayer { - /// Creates a new [`UpgradeLayer`]. +impl UpgradePlugin { + /// Creates a new [`UpgradePlugin`]. pub fn new() -> Self { Self::default() } } -impl Layer for UpgradeLayer { - type Service = Upgrade; +impl Plugin for UpgradePlugin +where + Op: OperationShape, +{ + type Service = Upgrade; - fn layer(&self, inner: S) -> Self::Service { + fn apply(&self, inner: S) -> Self::Service { Upgrade { _protocol: PhantomData, - _operation: PhantomData, - _exts: PhantomData, + _input: PhantomData, inner, } } @@ -70,22 +64,20 @@ impl Layer for UpgradeLayer { /// A [`Service`] responsible for wrapping an operation [`Service`] accepting and returning Smithy /// types, and converting it into a [`Service`] accepting and returning [`http`] types. -pub struct Upgrade { +pub struct Upgrade { _protocol: PhantomData, - _operation: PhantomData, - _exts: PhantomData, + _input: PhantomData, inner: S, } -impl Clone for Upgrade +impl Clone for Upgrade where S: Clone, { fn clone(&self) -> Self { Self { _protocol: PhantomData, - _operation: PhantomData, - _exts: PhantomData, + _input: PhantomData, inner: self.inner.clone(), } } @@ -106,39 +98,27 @@ pin_project! { } } -type InnerAlias = - Inner<<(Input, Exts) as FromRequest>::Future, Oneshot>; +type InnerAlias = Inner<>::Future, Oneshot>; pin_project! { /// The [`Service::Future`] of [`Upgrade`]. - pub struct UpgradeFuture + pub struct UpgradeFuture where - Operation: OperationShape, - (Operation::Input, Exts): FromRequest, - S: Service<(Operation::Input, Exts)>, + Input: FromRequest, + S: Service, { - service: S, + service: Option, #[pin] - inner: InnerAlias + inner: InnerAlias } } -impl Future for UpgradeFuture +impl Future for UpgradeFuture where - // `Op` is used to specify the operation shape - Op: OperationShape, - // Smithy input must convert from a HTTP request - Op::Input: FromRequest, - // Smithy output must convert into a HTTP response - Op::Output: IntoResponse

, - // Smithy error must convert into a HTTP response - Op::Error: IntoResponse

, - - // Must be able to convert extensions - Exts: FromParts

, - - // The signature of the inner service is correct - S: Service<(Op::Input, Exts), Response = Op::Output, Error = Op::Error> + Clone, + Input: FromRequest, + S: Service, + S::Response: IntoResponse

, + S::Error: IntoResponse

, { type Output = Result, Infallible>; @@ -151,7 +131,11 @@ where InnerProj::FromRequest { inner } => { let result = ready!(inner.poll(cx)); match result { - Ok(ok) => this.service.clone().oneshot(ok), + Ok(ok) => this + .service + .take() + .expect("futures cannot be polled after completion") + .oneshot(ok), Err(err) => return Poll::Ready(Ok(err.into_response())), } } @@ -170,26 +154,16 @@ where } } -impl Service> for Upgrade +impl Service> for Upgrade where - // `Op` is used to specify the operation shape - Op: OperationShape, - // Smithy input must convert from a HTTP request - Op::Input: FromRequest, - // Smithy output must convert into a HTTP response - Op::Output: IntoResponse

, - // Smithy error must convert into a HTTP response - Op::Error: IntoResponse

, - - // Must be able to convert extensions - Exts: FromParts

, - - // The signature of the inner service is correct - S: Service<(Op::Input, Exts), Response = Op::Output, Error = Op::Error> + Clone, + Input: FromRequest, + S: Service + Clone, + S::Response: IntoResponse

, + S::Error: IntoResponse

, { type Response = http::Response; type Error = Infallible; - type Future = UpgradeFuture; + type Future = UpgradeFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -199,87 +173,26 @@ where let clone = self.inner.clone(); let service = std::mem::replace(&mut self.inner, clone); UpgradeFuture { - service, + service: Some(service), inner: Inner::FromRequest { - inner: <(Op::Input, Exts) as FromRequest>::from_request(req), + inner: >::from_request(req), }, } } } -/// An interface to convert a representation of a Smithy operation into a [`Route`]. -/// -/// See the [module](crate::operation) documentation for more information. -pub trait Upgradable { - /// Upgrade the representation of a Smithy operation to a [`Route`]. - fn upgrade(self, plugin: &Plugin) -> Route; -} - -type UpgradedService = - <>::Layer as Layer>::Service>>>::Service; - -impl Upgradable for Operation -where - // `Op` is used to specify the operation shape - Op: OperationShape, - - // Smithy input must convert from a HTTP request - Op::Input: FromRequest, - // Smithy output must convert into a HTTP response - Op::Output: IntoResponse

, - // Smithy error must convert into a HTTP response - Op::Error: IntoResponse

, - - // Must be able to convert extensions - Exts: FromParts

, - - // The signature of the inner service is correct - Pl::Service: Service<(Op::Input, Exts), Response = Op::Output, Error = Op::Error> + Clone, - - // The plugin takes this operation as input - Pl: Plugin, - - // The modified Layer applies correctly to `Upgrade` - Pl::Layer: Layer>, - - // For `Route::new` for the resulting service - UpgradedService: - Service, Response = http::Response, Error = Infallible> + Clone + Send + 'static, - as Service>>::Future: Send + 'static, -{ - /// Takes the [`Operation`](Operation), applies [`Plugin`], then applies [`UpgradeLayer`] to - /// the modified `S`, then finally applies the modified `L`. - /// - /// The composition is made explicit in the method constraints and return type. - fn upgrade(self, plugin: &Pl) -> Route { - let mapped = plugin.map(self); - let layer = Stack::new(UpgradeLayer::new(), mapped.layer); - let svc = layer.layer(mapped.inner); - Route::new(svc) - } -} - -/// A marker struct indicating an [`Operation`] has not been set in a builder. -/// -/// This _does_ implement [`Upgradable`] but produces a [`Service`] which always returns an internal failure message. -pub struct FailOnMissingOperation; - -impl Upgradable for FailOnMissingOperation -where - InternalFailureException: IntoResponse

, - P: 'static, -{ - fn upgrade(self, _plugin: &Pl) -> Route { - Route::new(MissingFailure { _protocol: PhantomData }) - } -} - /// A [`Service`] which always returns an internal failure message and logs an error. #[derive(Copy)] pub struct MissingFailure

{ _protocol: PhantomData, } +impl

Default for MissingFailure

{ + fn default() -> Self { + Self { _protocol: PhantomData } + } +} + impl

Clone for MissingFailure

{ fn clone(&self) -> Self { MissingFailure { _protocol: PhantomData } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/alb_health_check.rs b/rust-runtime/aws-smithy-http-server/src/plugin/alb_health_check.rs index 33932ddd7dbbe5c8c5b73d668cd740a7655a1394..c87e86619cdad0d4235c0f2928c8225a659a9559 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/alb_health_check.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/alb_health_check.rs @@ -13,7 +13,7 @@ //! # use hyper::{Body, Response, StatusCode}; //! let plugins = PluginPipeline::new() //! // Handle all `/ping` health check requests by returning a `200 OK`. -//! .http_layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { +//! .layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { //! StatusCode::OK //! })); //! @@ -92,7 +92,7 @@ pub struct AlbHealthCheckService { impl Service> for AlbHealthCheckService where S: Service, Response = Response> + Clone, - S::Future: std::marker::Send + 'static, + S::Future: Send + 'static, H: Service, Response = StatusCode, Error = Infallible> + Clone, { type Response = S::Response; diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/closure.rs b/rust-runtime/aws-smithy-http-server/src/plugin/closure.rs index f1f5951e8a028cc6993e6581290428e276c20a0d..2429cc25f1fedb4197e1fc5f84be1c4fcebdc8c2 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/closure.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/closure.rs @@ -3,9 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use tower::layer::util::Stack; - -use crate::operation::{Operation, OperationShape}; +use crate::operation::OperationShape; use crate::shape_id::ShapeId; use super::Plugin; @@ -15,17 +13,15 @@ pub struct OperationIdFn { f: F, } -impl Plugin for OperationIdFn +impl Plugin for OperationIdFn where - F: Fn(ShapeId) -> NewLayer, + F: Fn(ShapeId, S) -> NewService, Op: OperationShape, { - type Service = S; - type Layer = Stack; + type Service = NewService; - fn map(&self, input: Operation) -> Operation { - let operation_id = Op::NAME; - input.layer((self.f)(operation_id)) + fn apply(&self, svc: S) -> Self::Service { + (self.f)(Op::ID, svc) } } @@ -56,9 +52,9 @@ where /// // This plugin applies the `PrintService` middleware around every operation. /// let plugin = plugin_from_operation_id_fn(f); /// ``` -pub fn plugin_from_operation_id_fn(f: F) -> OperationIdFn +pub fn plugin_from_operation_id_fn(f: F) -> OperationIdFn where - F: Fn(ShapeId) -> L, + F: Fn(ShapeId) -> NewService, { OperationIdFn { f } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/either.rs b/rust-runtime/aws-smithy-http-server/src/plugin/either.rs index bf33546ca183cfe0678007f7f2a96861100e9ae2..2054e906e83d94a269d1d7247df693645e7062a4 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/either.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/either.rs @@ -13,18 +13,17 @@ use std::{ }; use tower::{Layer, Service}; -use crate::operation::Operation; - use super::Plugin; pin_project! { - /// Combine two different [`Future`]/[`Service`]/[`Layer`]/[`Plugin`] types into a single type. + /// Combine two different [`Futures`](std::future::Future)/[`Services`](tower::Service)/ + /// [`Layers`](tower::Layer)/[`Plugins`](super::Plugin) into a single type. /// - /// # Notes on [`Future`] + /// # Notes on [`Future`](std::future::Future) /// /// The [`Future::Output`] must be identical. /// - /// # Notes on [`Service`] + /// # Notes on [`Service`](tower::Service) /// /// The [`Service::Response`] and [`Service::Error`] must be identical. #[derive(Clone, Debug)] @@ -103,30 +102,21 @@ where } } -impl Plugin for Either +impl Plugin for Either where - Le: Plugin, - Ri: Plugin, + Le: Plugin, + Ri: Plugin, { type Service = Either; - type Layer = Either; - fn map(&self, input: Operation) -> Operation { + fn apply(&self, svc: S) -> Self::Service { match self { - Either::Left { value } => { - let Operation { inner, layer } = value.map(input); - Operation { - inner: Either::Left { value: inner }, - layer: Either::Left { value: layer }, - } - } - Either::Right { value } => { - let Operation { inner, layer } = value.map(input); - Operation { - inner: Either::Right { value: inner }, - layer: Either::Right { value: layer }, - } - } + Either::Left { value } => Either::Left { + value: value.apply(svc), + }, + Either::Right { value } => Either::Right { + value: value.apply(svc), + }, } } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs b/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs index a9108a074721ac641e2079e86986efe37e0032bc..46608483c03052bc0459c1d98427ea3dfca005f8 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs @@ -5,13 +5,13 @@ use super::{either::Either, IdentityPlugin}; -use crate::operation::{Operation, OperationShape}; +use crate::operation::OperationShape; use crate::shape_id::ShapeId; use super::Plugin; /// Filters the application of an inner [`Plugin`] using a predicate over the -/// [`OperationShape::NAME`](crate::operation::OperationShape). +/// [`OperationShape::ID`](crate::operation::OperationShape). /// /// See [`filter_by_operation_id`] for more details. pub struct FilterByOperationId { @@ -20,23 +20,22 @@ pub struct FilterByOperationId { } /// Filters the application of an inner [`Plugin`] using a predicate over the -/// [`OperationShape::NAME`](crate::operation::OperationShape). +/// [`OperationShape::ID`](crate::operation::OperationShape). /// /// # Example /// /// ```rust /// use aws_smithy_http_server::plugin::filter_by_operation_id; -/// use aws_smithy_http_server::shape_id::ShapeId; -/// # use aws_smithy_http_server::{plugin::Plugin, operation::{Operation, OperationShape}}; +/// # use aws_smithy_http_server::{plugin::Plugin, operation::OperationShape, shape_id::ShapeId}; /// # struct Pl; /// # struct CheckHealth; -/// # impl OperationShape for CheckHealth { const NAME: ShapeId = ShapeId::new("ns#CheckHealth", "ns", "CheckHealth"); type Input = (); type Output = (); type Error = (); } -/// # impl Plugin<(), CheckHealth, (), ()> for Pl { type Service = (); type Layer = (); fn map(&self, input: Operation<(), ()>) -> Operation<(), ()> { input }} +/// # impl OperationShape for CheckHealth { const ID: ShapeId = ShapeId::new("", "", ""); type Input = (); type Output = (); type Error = (); } +/// # impl Plugin<(), CheckHealth, ()> for Pl { type Service = (); fn apply(&self, input: ()) -> Self::Service { input }} /// # let plugin = Pl; -/// # let operation = Operation { inner: (), layer: () }; +/// # let svc = (); /// // Prevents `plugin` from being applied to the `CheckHealth` operation. -/// let filtered_plugin = filter_by_operation_id(plugin, |id| id.name() != CheckHealth::NAME.name()); -/// let new_operation = filtered_plugin.map(operation); +/// let filtered_plugin = filter_by_operation_id(plugin, |name| name != CheckHealth::ID); +/// let new_operation = filtered_plugin.apply(svc); /// ``` pub fn filter_by_operation_id(plugins: Inner, predicate: F) -> FilterByOperationId where @@ -52,21 +51,20 @@ impl FilterByOperationId { } } -impl Plugin for FilterByOperationId +impl Plugin for FilterByOperationId where F: Fn(ShapeId) -> bool, - Inner: Plugin, + Inner: Plugin, Op: OperationShape, { type Service = Either; - type Layer = Either; - fn map(&self, input: Operation) -> Operation { - let either_plugin = if (self.predicate)(Op::NAME) { + fn apply(&self, svc: S) -> Self::Service { + let either_plugin = if (self.predicate)(Op::ID) { Either::Left { value: &self.inner } } else { Either::Right { value: IdentityPlugin } }; - either_plugin.map(input) + either_plugin.apply(svc) } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/identity.rs b/rust-runtime/aws-smithy-http-server/src/plugin/identity.rs index d0a6d1390bef94d623806ab4b05acaf615160ccb..52cd7e6965a035eb8c3d816d809d1d54fdbe0015 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/identity.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/identity.rs @@ -3,18 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::operation::Operation; - use super::Plugin; -/// A [`Plugin`] that maps an `input` [`Operation`] to itself. +/// A [`Plugin`] that maps a service to itself. pub struct IdentityPlugin; -impl Plugin for IdentityPlugin { +impl Plugin for IdentityPlugin { type Service = S; - type Layer = L; - fn map(&self, input: Operation) -> Operation { - input + fn apply(&self, svc: S) -> S { + svc } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/layer.rs b/rust-runtime/aws-smithy-http-server/src/plugin/layer.rs index d7a055f291862fd94fd57e5c687627cf033eb0bc..6d5e9188f64eeee3513729d33fec710b55abb35a 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/layer.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/layer.rs @@ -3,23 +3,50 @@ * SPDX-License-Identifier: Apache-2.0 */ -use tower::layer::util::Stack; +use std::marker::PhantomData; -use crate::operation::Operation; +use tower::Layer; use super::Plugin; -/// A [`Plugin`] which appends a HTTP [`Layer`](tower::Layer) `L` to the existing `Layer` in [`Operation`](Operation). -pub struct HttpLayer(pub L); +/// A [`Plugin`] which acts as a [`Layer`] `L`. +pub struct LayerPlugin(pub L); -impl Plugin for HttpLayer +impl Plugin for LayerPlugin where - NewLayer: Clone, + L: Layer, { - type Service = S; - type Layer = Stack; + type Service = L::Service; - fn map(&self, input: Operation) -> Operation { - input.layer(self.0.clone()) + fn apply(&self, svc: S) -> Self::Service { + self.0.layer(svc) + } +} + +/// A [`Layer`] which acts as a [`Plugin`] `Pl` for specific protocol `P` and operation `Op`. +pub struct PluginLayer { + plugin: Pl, + _protocol: PhantomData

, + _op: PhantomData, +} + +impl Layer for PluginLayer +where + Pl: Plugin, +{ + type Service = Pl::Service; + + fn layer(&self, inner: S) -> Self::Service { + self.plugin.apply(inner) + } +} + +impl PluginLayer<(), (), Pl> { + pub fn new(plugin: Pl) -> PluginLayer { + PluginLayer { + plugin, + _protocol: PhantomData, + _op: PhantomData, + } } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs b/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs index b6acaa038ab3ed45ce9208773260f857dde81e89..cda732e6996e7acd2fb1d6a7ee601a9ed6088993 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs @@ -15,12 +15,12 @@ //! # use aws_smithy_http_server::shape_id::ShapeId; //! # let layer = (); //! # struct GetPokemonSpecies; -//! # impl GetPokemonSpecies { const NAME: ShapeId = ShapeId::new("namespace#name", "namespace", "name"); }; +//! # impl GetPokemonSpecies { const ID: ShapeId = ShapeId::new("namespace#name", "namespace", "name"); }; //! // Create a `Plugin` from a HTTP `Layer` -//! let plugin = HttpLayer(layer); +//! let plugin = LayerPlugin(layer); //! //! // Only apply the layer to operations with name "GetPokemonSpecies" -//! let plugin = filter_by_operation_id(plugin, |id| id.name() == GetPokemonSpecies::NAME.name()); +//! let plugin = filter_by_operation_id(plugin, |id| id.name() == GetPokemonSpecies::ID.name()); //! ``` //! //! # Construct a [`Plugin`] from a closure that takes as input the operation name @@ -55,7 +55,7 @@ //! //! ```rust //! use aws_smithy_http_server::{ -//! operation::{Operation, OperationShape}, +//! operation::{OperationShape}, //! plugin::{Plugin, PluginPipeline, PluginStack}, //! shape_id::ShapeId, //! }; @@ -87,35 +87,18 @@ //! } //! } //! -//! /// A [`Layer`] which constructs the [`PrintService`]. -//! #[derive(Debug)] -//! pub struct PrintLayer { -//! id: ShapeId, -//! } -//! impl Layer for PrintLayer { -//! type Service = PrintService; -//! -//! fn layer(&self, service: S) -> Self::Service { -//! PrintService { -//! inner: service, -//! id: self.id.clone(), -//! } -//! } -//! } -//! //! /// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. //! #[derive(Debug)] //! pub struct PrintPlugin; //! -//! impl Plugin for PrintPlugin +//! impl Plugin for PrintPlugin //! where //! Op: OperationShape, //! { -//! type Service = S; -//! type Layer = Stack; +//! type Service = PrintService; //! -//! fn map(&self, input: Operation) -> Operation { -//! input.layer(PrintLayer { id: Op::NAME }) +//! fn apply(&self, inner: S) -> Self::Service { +//! PrintService { inner, id: Op::ID } //! } //! } //! ``` @@ -130,40 +113,35 @@ mod layer; mod pipeline; mod stack; -use crate::operation::Operation; - pub use closure::{plugin_from_operation_id_fn, OperationIdFn}; pub use either::Either; pub use filter::{filter_by_operation_id, FilterByOperationId}; pub use identity::IdentityPlugin; -pub use layer::HttpLayer; +pub use layer::{LayerPlugin, PluginLayer}; pub use pipeline::PluginPipeline; pub use stack::PluginStack; -/// A mapping from one [`Operation`] to another. Used to modify the behavior of -/// [`Upgradable`](crate::operation::Upgradable) and therefore the resulting service builder. +/// A mapping from one [`Service`](tower::Service) to another. This should be viewed as a +/// [`Layer`](tower::Layer) parameterized by the protocol and operation. /// /// The generics `Protocol` and `Op` allow the behavior to be parameterized. /// /// See [module](crate::plugin) documentation for more information. -pub trait Plugin { +pub trait Plugin { /// The type of the new [`Service`](tower::Service). type Service; - /// The type of the new [`Layer`](tower::Layer). - type Layer; - /// Maps an [`Operation`] to another. - fn map(&self, input: Operation) -> Operation; + /// Maps a [`Service`](tower::Service) to another. + fn apply(&self, svc: S) -> Self::Service; } -impl<'a, P, Op, S, L, Pl> Plugin for &'a Pl +impl<'a, P, Op, S, Pl> Plugin for &'a Pl where - Pl: Plugin, + Pl: Plugin, { type Service = Pl::Service; - type Layer = Pl::Layer; - fn map(&self, input: Operation) -> Operation { - >::map(*self, input) + fn apply(&self, inner: S) -> Self::Service { + >::apply(self, inner) } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs b/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs index d38ecfd782738cccb4738ebbac38db8fa986ae43..2fde128511b6b265fc412b0c1d970edae826172d 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::operation::Operation; use crate::plugin::{IdentityPlugin, Plugin, PluginStack}; -use super::HttpLayer; +use super::LayerPlugin; /// A wrapper struct for composing [`Plugin`]s. /// It is used as input for the `builder_with_plugins` method on the generate service struct @@ -35,7 +34,7 @@ use super::HttpLayer; /// /// `PluginPipeline` is itself a [`Plugin`]: you can apply any transformation that expects a /// [`Plugin`] to an entire pipeline. In this case, we want to use -/// [`filter_by_operation_name`](crate::plugin::filter_by_operation_id) to limit the scope of +/// [`filter_by_operation_id`](crate::plugin::filter_by_operation_id) to limit the scope of /// the logging and metrics plugins to the `CheckHealth` operation: /// /// ```rust @@ -45,14 +44,14 @@ use super::HttpLayer; /// # use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin; /// use aws_smithy_http_server::shape_id::ShapeId; /// # struct CheckHealth; -/// # impl CheckHealth { const NAME: ShapeId = ShapeId::new("namespace#MyName", "namespace", "MyName"); } +/// # impl CheckHealth { const ID: ShapeId = ShapeId::new("namespace#MyName", "namespace", "MyName"); } /// /// // The logging and metrics plugins will only be applied to the `CheckHealth` operation. /// let operation_specific_pipeline = filter_by_operation_id( /// PluginPipeline::new() /// .push(LoggingPlugin) /// .push(MetricsPlugin), -/// |name| name == CheckHealth::NAME +/// |name| name == CheckHealth::ID /// ); /// let pipeline = PluginPipeline::new() /// .push(operation_specific_pipeline) @@ -109,7 +108,7 @@ use super::HttpLayer; /// // Our custom method! /// .with_auth(); /// ``` -pub struct PluginPipeline

(P); +pub struct PluginPipeline

(pub(crate) P); impl Default for PluginPipeline { fn default() -> Self { @@ -142,50 +141,42 @@ impl

PluginPipeline

{ /// /// ## Implementation notes /// - /// Plugins are applied to the underlying [`Operation`] in opposite order compared + /// Plugins are applied to the underlying [`Service`](tower::Service) in opposite order compared /// to their registration order. - /// But most [`Plugin::map`] implementations desugar to appending a layer to [`Operation`], - /// usually via [`Operation::layer`]. + /// /// As an example: /// /// ```rust,compile_fail /// #[derive(Debug)] /// pub struct PrintPlugin; /// - /// impl Plugin for PrintPlugin + /// impl Plugin for PrintPlugin /// // [...] /// { /// // [...] - /// fn map(&self, input: Operation) -> Operation { - /// input.layer(PrintLayer { id: Op::NAME }) + /// fn apply(&self, inner: S) -> Self::Service { + /// PrintService { inner, name: Op::ID } /// } /// } /// ``` /// - /// The layer that is registered **last** via [`Operation::layer`] is the one that gets executed - /// **first** at runtime when a new request comes in, since it _wraps_ the underlying service. - /// - /// This is why plugins in [`PluginPipeline`] are applied in opposite order compared to their - /// registration order: this ensures that, _at runtime_, their logic is executed - /// in registration order. pub fn push(self, new_plugin: NewPlugin) -> PluginPipeline> { PluginPipeline(PluginStack::new(new_plugin, self.0)) } /// Applies a single [`tower::Layer`] to all operations _before_ they are deserialized. - pub fn http_layer(self, layer: L) -> PluginPipeline, P>> { - PluginPipeline(PluginStack::new(HttpLayer(layer), self.0)) + pub fn layer(self, layer: L) -> PluginPipeline, P>> { + PluginPipeline(PluginStack::new(LayerPlugin(layer), self.0)) } } -impl Plugin for PluginPipeline +impl Plugin for PluginPipeline where - InnerPlugin: Plugin, + InnerPlugin: Plugin, { type Service = InnerPlugin::Service; - type Layer = InnerPlugin::Layer; - fn map(&self, input: Operation) -> Operation { - self.0.map(input) + fn apply(&self, svc: S) -> Self::Service { + self.0.apply(svc) } } diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/stack.rs b/rust-runtime/aws-smithy-http-server/src/plugin/stack.rs index a68e3086d36fed133649db20bc1c21a67f873516..e5aa54d219b4700f4e06f5773f1ac3c11da0402f 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/stack.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/stack.rs @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::operation::Operation; - use super::Plugin; /// A wrapper struct which composes an `Inner` and an `Outer` [`Plugin`]. @@ -24,16 +22,15 @@ impl PluginStack { } } -impl Plugin for PluginStack +impl Plugin for PluginStack where - Inner: Plugin, - Outer: Plugin, + Inner: Plugin, + Outer: Plugin, { type Service = Outer::Service; - type Layer = Outer::Layer; - fn map(&self, input: Operation) -> Operation { - let inner = self.inner.map(input); - self.outer.map(inner) + fn apply(&self, svc: S) -> Self::Service { + let svc = self.inner.apply(svc); + self.outer.apply(svc) } }