diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt index ce69131578b16377683e409f9249e55b1e82cd19..c4420fced545ec0786845853ebaa3982fa7a3022 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGeneratorV2.kt @@ -332,6 +332,9 @@ class ServerServiceGeneratorV2( /// You must specify what plugins should be applied to the operations in this service. /// /// Use [`$serviceName::builder_without_plugins`] if you don't need to apply plugins. + /// + /// Check out [`PluginPipeline`](#{SmithyHttpServer}::plugin::PluginPipeline) if you need to apply + /// multiple plugins. pub fn builder_with_plugins(plugin: Plugin) -> $builderName { $builderName { #{NotSetFields:W}, diff --git a/design/src/server/anatomy.md b/design/src/server/anatomy.md index 3f90aec96e40747f02baabc26a8366e2e94ee418..75b577d944947d404ee139c723ee82af0cc8d8ef 100644 --- a/design/src/server/anatomy.md +++ b/design/src/server/anatomy.md @@ -589,12 +589,9 @@ The central trait is [`Plugin`](https://github.com/awslabs/smithy-rs/blob/4c5cbc ```rust /// A mapping from one [`Operation`] to another. Used to modify the behavior of -/// [`Upgradable`](crate::operation::Upgradable) and therefore the resulting service builder, +/// [`Upgradable`](crate::operation::Upgradable) and therefore the resulting service builder. /// /// The generics `Protocol` and `Op` allow the behavior to be parameterized. -/// -/// Every service builder enjoys [`Pluggable`] and therefore can be provided with a [`Plugin`] using -/// [`Pluggable::apply`]. pub trait Plugin { type Service; type Layer; @@ -611,7 +608,7 @@ The `Upgradable::upgrade` method on `Operation`, previously presented in [ /// 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) -> Self::Service { + 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)) @@ -656,132 +653,43 @@ An example `Plugin` implementation can be found in [aws-smithy-http-server/examp 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. -[//]: # (The section below is no longer accurate, it'll have to be updated once we add the `PluginBuilder`) - -[//]: # (The service builder implements the [`Pluggable`](https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/plugin.rs#L8-L29) trait, which allows them to apply plugins to service builders:) - -[//]: # () -[//]: # (```rust) - -[//]: # (/// Provides a standard interface for applying [`Plugin`]s to a service builder. This is implemented automatically for) - -[//]: # (/// all builders.) - -[//]: # (///) - -[//]: # (/// As [`Plugin`]s modify the way in which [`Operation`]s are [`upgraded`](crate::operation::Upgradable) we can use) - -[//]: # (/// [`Pluggable`] as a foundation to write extension traits which are implemented for all service builders.) - -[//]: # (///) - -[//]: # (/// # Example) - -[//]: # (///) - -[//]: # (/// ```) - -[//]: # (/// # struct PrintPlugin;) - -[//]: # (/// # use aws_smithy_http_server::plugin::Pluggable;) - -[//]: # (/// trait PrintExt: Pluggable {) - -[//]: # (/// fn print(self) -> Self::Output where Self: Sized {) - -[//]: # (/// self.apply(PrintPlugin)) - -[//]: # (/// }) - -[//]: # (/// }) - -[//]: # (///) - -[//]: # (/// impl PrintExt for Builder where Builder: Pluggable {}) - -[//]: # (/// ```) - -[//]: # (pub trait Pluggable {) - -[//]: # ( type Output;) - -[//]: # () -[//]: # ( /// Applies a [`Plugin`] to the service builder.) - -[//]: # ( fn apply(self, plugin: NewPlugin) -> Self::Output;) - -[//]: # (}) - -[//]: # (```) - -[//]: # () -[//]: # (As seen in the `Pluggable` documentation, third-parties can use extension traits over `Pluggable` to extend the API of builders. In addition to all the `Op{N}` the service builder also holds a `Pl`:) +You might find yourself wanting to apply _multiple_ plugins to your service. +This can be accommodated via [`PluginPipeline`]. -[//]: # () -[//]: # (```rust) - -[//]: # (/// The service builder for [`PokemonService`].) - -[//]: # (///) - -[//]: # (/// Constructed via [`PokemonService::builder`].) - -[//]: # (pub struct PokemonServiceBuilder {) - -[//]: # ( capture_pokemon_operation: Op1,) - -[//]: # ( empty_operation: Op2,) - -[//]: # ( get_pokemon_species: Op3,) - -[//]: # ( get_server_statistics: Op4,) - -[//]: # ( get_storage: Op5,) - -[//]: # ( health_check_operation: Op6,) - -[//]: # () -[//]: # ( plugin: Pl) - -[//]: # (}) - -[//]: # (```) - -[//]: # () -[//]: # (which allows the following `Pluggable` implementation to be generated:) - -[//]: # () -[//]: # (```rust) - -[//]: # (impl Pluggable for PokemonServiceBuilder) - -[//]: # ({) - -[//]: # ( type Output = PokemonServiceBuilder>;) - -[//]: # ( fn apply(self, plugin: NewPl) -> Self::Output {) - -[//]: # ( PokemonServiceBuilder {) - -[//]: # ( capture_pokemon_operation: self.capture_pokemon_operation,) - -[//]: # ( empty_operation: self.empty_operation,) +```rust +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); +``` -[//]: # () -[//]: # ( plugin: PluginStack::new(self.plugin, plugin),) +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 +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) + } +} -[//]: # () -[//]: # (Here `PluginStack` works in a similar way to [`tower::layer::util::Stack`](https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html) - allowing users to stack a new plugin rather than replacing the currently set one.) +let pipeline = PluginPipeline::new() + .push(LoggingPlugin) + // Our custom method! + .with_auth(); +``` ## Accessing Unmodelled Data diff --git a/design/src/server/middleware.md b/design/src/server/middleware.md index 8d9130286adb5050f2bef345e0b7bffbebfbe812..45334adf9eb215a5f6a503a0f690aaeecb04b464 100644 --- a/design/src/server/middleware.md +++ b/design/src/server/middleware.md @@ -135,7 +135,7 @@ The output of the Smithy service builder provides the user with a `Service> */ = PokemonService::builder() +let app /* : PokemonService> */ = PokemonService::builder_without_plugins() .get_pokemon_species(/* handler */) /* ... */ .build(); @@ -155,7 +155,7 @@ A _single_ layer can be applied to _all_ routes inside the `Router`. This exists // Construct `TraceLayer`. let trace_layer = TraceLayer::new_for_http(Duration::from_secs(3)); -let app /* : PokemonService> */ = PokemonService::builder() +let app /* : PokemonService> */ = PokemonService::builder_without_plugins() .get_pokemon_species(/* handler */) /* ... */ .build() @@ -176,7 +176,7 @@ 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() +let app /* : PokemonService> */ = PokemonService::builder_without_plugins() .get_pokemon_species_operation(layered_handler) /* ... */ .build(); @@ -200,7 +200,7 @@ let handler_svc = buffer_layer.layer(handler_svc); let layered_handler = GetPokemonSpecies::from_service(handler_svc); -let app /* : PokemonService> */ = PokemonService::builder() +let app /* : PokemonService> */ = PokemonService::builder_without_plugins() .get_pokemon_species_operation(layered_handler) /* ... */ .build(); @@ -307,44 +307,36 @@ impl Plugin for PrintPlugin } ``` -A `Plugin` can then be applied to all operations using the `Pluggable::apply` method +You can provide a custom method to add your plugin to a `PluginPipeline` via an extension trait: ```rust -pub trait Pluggable { - type Output; - - /// Applies a [`Plugin`] to the service builder. - fn apply(self, plugin: NewPlugin) -> Self::Output; -} -``` - -which is implemented on every service builder. - -The plugin system is designed to hide the details of the `Plugin` and `Pluggable` trait from the average consumer. Such customers should instead interact with utility methods on the service builder which are vended by extension traits and enjoy self contained documentation. - -```rust -/// An extension trait of [`Pluggable`]. -/// -/// This provides a [`print`](PrintExt::print) method to all service builders. -pub trait PrintExt: Pluggable { +/// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`]. +pub trait PrintExt { /// Causes all operations to print the operation name when called. /// /// This works by applying the [`PrintPlugin`]. - fn print(self) -> Self::Output - where - Self: Sized, - { - self.apply(PrintPlugin) + fn print(self) -> PluginPipeline>; +} + +impl PrintExt for PluginPipeline { + fn print(self) -> PluginPipeline> { + self.push(PrintPlugin) } } ``` -which allows for +This allows for: ```rust -let app /* : PokemonService> */ = PokemonService::builder() +let plugin_pipeline = PluginPipeline::new() + // [..other plugins..] + // The custom method! + .print(); +let app /* : PokemonService> */ = PokemonService::builder_with_plugins(plugin_pipeline) .get_pokemon_species_operation(layered_handler) /* ... */ - .print() .build(); ``` + +The custom `print` method hides the details of the `Plugin` trait from the average consumer. +They interact with the utility methods on `PluginPipeline` and enjoy the self-contained documentation. diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs index baf9a69b11ab78b8585bd3a04f899f43463dc0bd..a5a5612a40b0790a02239c184dd3c0b3573aef4a 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs @@ -5,7 +5,7 @@ use aws_smithy_http_server::{ operation::{Operation, OperationShape}, - plugin::{Pluggable, Plugin}, + plugin::{Plugin, PluginPipeline, PluginStack}, }; use tower::{layer::util::Stack, Layer, Service}; @@ -68,19 +68,16 @@ where } } -/// An extension trait of [`Pluggable`]. -/// -/// This provides a [`print`](PrintExt::print) method to all service builders. -pub trait PrintExt: Pluggable { +/// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`]. +pub trait PrintExt { /// Causes all operations to print the operation name when called. /// /// This works by applying the [`PrintPlugin`]. - fn print(self) -> Self::Output - where - Self: Sized, - { - self.apply(PrintPlugin) - } + fn print(self) -> PluginPipeline>; } -impl PrintExt for Builder where Builder: Pluggable {} +impl PrintExt for PluginPipeline { + fn print(self) -> PluginPipeline> { + self.push(PrintPlugin) + } +} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1852ca11fb91c65a51e064fa154e38e020c8dd1 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +use aws_smithy_http::body::SdkBody; +use aws_smithy_http_server::operation::Operation; +use aws_smithy_http_server::plugin::{Plugin, PluginPipeline}; +use hyper::http; +use pokemon_service::do_nothing; +use pokemon_service_client::input::DoNothingInput; +use pokemon_service_client::Config; +use std::ops::Deref; +use std::sync::Arc; +use std::sync::Mutex; +use std::task::{Context, Poll}; +use tower::layer::util::Stack; +use tower::{Layer, Service}; + +trait OperationExt { + /// Convert an SDK operation into an `http::Request`. + fn into_http(self) -> http::Request; +} + +impl OperationExt for aws_smithy_http::operation::Operation { + fn into_http(self) -> http::Request { + self.into_request_response().0.into_parts().0 + } +} + +#[tokio::test] +async fn plugin_layers_are_executed_in_registration_order() { + // Each plugin layer will push its name into this vector when it gets invoked. + // We can then check the vector content to verify the invocation order + let output = Arc::new(Mutex::new(Vec::new())); + + let pipeline = PluginPipeline::new() + .push(SentinelPlugin::new("first", output.clone())) + .push(SentinelPlugin::new("second", output.clone())); + let mut app = pokemon_service_server_sdk::service::PokemonService::builder_with_plugins(pipeline) + .do_nothing(do_nothing) + .build_unchecked(); + let request = DoNothingInput::builder() + .build() + .unwrap() + .make_operation(&Config::builder().build()) + .await + .unwrap() + .into_http(); + app.call(request).await.unwrap(); + + let output_guard = output.lock().unwrap(); + assert_eq!(output_guard.deref(), &vec!["first", "second"]); +} + +struct SentinelPlugin { + name: &'static str, + output: Arc>>, +} + +impl SentinelPlugin { + pub fn new(name: &'static str, output: Arc>>) -> Self { + Self { name, output: output } + } +} + +impl Plugin for SentinelPlugin { + type Service = S; + type Layer = Stack; + + fn map(&self, input: Operation) -> Operation { + input.layer(SentinelLayer { + name: self.name, + output: self.output.clone(), + }) + } +} + +/// A [`Service`] that adds a print log. +#[derive(Clone, Debug)] +pub struct SentinelService { + inner: S, + output: Arc>>, + name: &'static str, +} + +impl Service for SentinelService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: R) -> Self::Future { + self.output.lock().unwrap().push(self.name); + self.inner.call(req) + } +} + +/// A [`Layer`] which constructs the [`PrintService`]. +#[derive(Debug)] +pub struct SentinelLayer { + name: &'static str, + output: Arc>>, +} + +impl Layer for SentinelLayer { + type Service = SentinelService; + + fn layer(&self, service: S) -> Self::Service { + SentinelService { + inner: service, + output: self.output.clone(), + name: self.name, + } + } +} 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 6edbea980de52ad75310da24dbe60b3cef06994c..ce772186030e45366f63873507aa44305a80e71f 100644 --- a/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs +++ b/rust-runtime/aws-smithy-http-server/src/instrumentation/plugin.rs @@ -5,14 +5,15 @@ use tower::layer::util::Stack; +use crate::plugin::{PluginPipeline, PluginStack}; use crate::{ operation::{Operation, OperationShape}, - plugin::{Pluggable, Plugin}, + plugin::Plugin, }; use super::{layer::InstrumentLayer, sensitivity::Sensitivity}; -/// An [`Plugin`] which applies [`InstrumentLayer`] to all operations in the builder. +/// A [`Plugin`] which applies [`InstrumentLayer`] to all operations in the builder. #[derive(Debug)] pub struct InstrumentPlugin; @@ -32,18 +33,17 @@ where } } -/// An extension trait for applying [`InstrumentLayer`] to all operations. -pub trait InstrumentExt: Pluggable { +/// 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 /// 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) -> Self::Output - where - Self: Sized, - { - self.apply(InstrumentPlugin) - } + fn instrument(self) -> PluginPipeline>; } -impl InstrumentExt for Builder where Builder: Pluggable {} +impl InstrumentExt for PluginPipeline { + fn instrument(self) -> PluginPipeline> { + self.push(InstrumentPlugin) + } +} 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 5a00b0d285b49d21d5926cc36e0b14d951cad489..a9804169b8f652ac1c854eca1c2b42b6b612015f 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/filter.rs @@ -9,17 +9,43 @@ use crate::operation::{Operation, OperationShape}; use super::Plugin; -/// A [`Plugin`] used to filter [`Plugin::map`] application using a predicate over the [`OperationShape::NAME`]. +/// Filters the application of an inner [`Plugin`] using a predicate over the +/// [`OperationShape::NAME`](crate::operation::OperationShape). /// -/// See [`PluginExt::filter_by_operation_name`](super::PluginExt::filter_by_operation_name) for more information. +/// See [`filter_by_operation_name`] for more details. pub struct FilterByOperationName { inner: Inner, predicate: F, } +/// Filters the application of an inner [`Plugin`] using a predicate over the +/// [`OperationShape::NAME`](crate::operation::OperationShape). +/// +/// # Example +/// +/// ```rust +/// use aws_smithy_http_server::plugin::filter_by_operation_name; +/// # use aws_smithy_http_server::{plugin::Plugin, operation::{Operation, OperationShape}}; +/// # struct Pl; +/// # struct CheckHealth; +/// # impl OperationShape for CheckHealth { const NAME: &'static str = ""; type Input = (); type Output = (); type Error = (); } +/// # impl Plugin<(), CheckHealth, (), ()> for Pl { type Service = (); type Layer = (); fn map(&self, input: Operation<(), ()>) -> Operation<(), ()> { input }} +/// # let plugin = Pl; +/// # let operation = Operation { inner: (), layer: () }; +/// // Prevents `plugin` from being applied to the `CheckHealth` operation. +/// let filtered_plugin = filter_by_operation_name(plugin, |name| name != CheckHealth::NAME); +/// let new_operation = filtered_plugin.map(operation); +/// ``` +pub fn filter_by_operation_name(plugins: Inner, predicate: F) -> FilterByOperationName +where + F: Fn(&str) -> bool, +{ + FilterByOperationName::new(plugins, predicate) +} + impl FilterByOperationName { /// Creates a new [`FilterByOperationName`]. - pub(crate) fn new(inner: Inner, predicate: F) -> Self { + fn new(inner: Inner, predicate: F) -> Self { Self { inner, predicate } } } 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 3317337bd71de457ed80433c9311879e60ba866b..8ee496774fe76c7ea197f8d893faac24f4945d56 100644 --- a/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/plugin/mod.rs @@ -5,47 +5,20 @@ mod filter; mod identity; +mod pipeline; mod stack; use crate::operation::Operation; -pub use filter::*; -pub use identity::*; -pub use stack::*; - -/// Provides a standard interface for applying [`Plugin`]s to a service builder. This is implemented automatically for -/// all builders. -/// -/// As [`Plugin`]s modify the way in which [`Operation`]s are [`upgraded`](crate::operation::Upgradable) we can use -/// [`Pluggable`] as a foundation to write extension traits which are implemented for all service builders. -/// -/// # Example -/// -/// ``` -/// # struct PrintPlugin; -/// # use aws_smithy_http_server::plugin::Pluggable; -/// trait PrintExt: Pluggable { -/// fn print(self) -> Self::Output where Self: Sized { -/// self.apply(PrintPlugin) -/// } -/// } -/// -/// impl PrintExt for Builder where Builder: Pluggable {} -/// ``` -pub trait Pluggable { - type Output; - - /// Applies a [`Plugin`] to the service builder. - fn apply(self, plugin: NewPlugin) -> Self::Output; -} +pub use filter::{filter_by_operation_name, FilterByOperationName}; +pub use identity::IdentityPlugin; +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, +/// [`Upgradable`](crate::operation::Upgradable) and therefore the resulting service builder. /// /// The generics `Protocol` and `Op` allow the behavior to be parameterized. -/// -/// Every service builder enjoys [`Pluggable`] and therefore can be provided with a [`Plugin`] using -/// [`Pluggable::apply`]. pub trait Plugin { type Service; type Layer; @@ -53,41 +26,3 @@ pub trait Plugin { /// Maps an [`Operation`] to another. fn map(&self, input: Operation) -> Operation; } - -/// An extension trait for [`Plugin`]. -pub trait PluginExt: Plugin { - /// Stacks another [`Plugin`], running them sequentially. - fn stack(self, other: Other) -> PluginStack - where - Self: Sized, - { - PluginStack::new(self, other) - } - - /// Filters the application of the [`Plugin`] using a predicate over the - /// [`OperationShape::NAME`](crate::operation::OperationShape). - /// - /// # Example - /// - /// ```rust - /// # use aws_smithy_http_server::{plugin::{Plugin, PluginExt}, operation::{Operation, OperationShape}}; - /// # struct Pl; - /// # struct CheckHealth; - /// # impl OperationShape for CheckHealth { const NAME: &'static str = ""; type Input = (); type Output = (); type Error = (); } - /// # impl Plugin<(), CheckHealth, (), ()> for Pl { type Service = (); type Layer = (); fn map(&self, input: Operation<(), ()>) -> Operation<(), ()> { input }} - /// # let plugin = Pl; - /// # let operation = Operation { inner: (), layer: () }; - /// // Prevents `plugin` from being applied to the `CheckHealth` operation. - /// let filtered_plugin = plugin.filter_by_operation_name(|name| name != CheckHealth::NAME); - /// let new_operation = filtered_plugin.map(operation); - /// ``` - fn filter_by_operation_name(self, predicate: F) -> FilterByOperationName - where - Self: Sized, - F: Fn(&str) -> bool, - { - FilterByOperationName::new(self, predicate) - } -} - -impl PluginExt for Pl where Pl: Plugin {} diff --git a/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs b/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b390fc903ad0907efa493a8874fb102e2d6d265 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/plugin/pipeline.rs @@ -0,0 +1,183 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::operation::Operation; +use crate::plugin::{IdentityPlugin, Plugin, PluginStack}; + +/// A wrapper struct for composing [`Plugin`]s. +/// It is used as input for the `builder_with_plugins` method on the generate service struct +/// (e.g. `PokemonService::builder_with_plugins`). +/// +/// ## Applying plugins in a sequence +/// +/// You can use the [`push`](PluginPipeline::push) method to apply a new plugin after the ones that +/// have already been registered. +/// +/// ```rust +/// 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 our example above, `LoggingPlugin` would run first, while `MetricsPlugin` is executed last. +/// +/// ## Wrapping the current plugin pipeline +/// +/// From time to time, you might have a need to transform the entire pipeline that has been built +/// so far - e.g. you only want to apply those plugins for a specific operation. +/// +/// `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_name) to limit the scope of +/// the logging and metrics plugins to the `CheckHealth` operation: +/// +/// ```rust +/// use aws_smithy_http_server::plugin::{filter_by_operation_name, PluginPipeline}; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as MetricsPlugin; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin; +/// # struct CheckHealth; +/// # impl CheckHealth { const NAME: &'static str = "MyName"; } +/// +/// // The logging and metrics plugins will only be applied to the `CheckHealth` operation. +/// let operation_specific_pipeline = filter_by_operation_name( +/// PluginPipeline::new() +/// .push(LoggingPlugin) +/// .push(MetricsPlugin), +/// |name| name == CheckHealth::NAME +/// ); +/// let pipeline = PluginPipeline::new() +/// .push(operation_specific_pipeline) +/// // The auth plugin will be applied to all operations +/// .push(AuthPlugin); +/// ``` +/// +/// ## Concatenating two plugin pipelines +/// +/// `PluginPipeline` is a good way to bundle together multiple plugins, ensuring they are all +/// registered in the correct order. +/// +/// Since `PluginPipeline` is itself a [`Plugin`], you can use the [`push`](PluginPipeline::push) to +/// append, at once, all the plugins in another pipeline to the current pipeline: +/// +/// ```rust +/// use aws_smithy_http_server::plugin::{IdentityPlugin, PluginPipeline, PluginStack}; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as MetricsPlugin; +/// # use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin; +/// +/// pub fn get_bundled_pipeline() -> PluginPipeline>> { +/// PluginPipeline::new().push(LoggingPlugin).push(MetricsPlugin) +/// } +/// +/// let pipeline = PluginPipeline::new() +/// .push(AuthPlugin) +/// .push(get_bundled_pipeline()); +/// ``` +/// +/// ## Providing custom methods on `PluginPipeline` +/// +/// You use an **extension trait** to add custom methods on `PluginPipeline`. +/// +/// This is a simple example using `AuthPlugin`: +/// +/// ```rust +/// 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(); +/// ``` +pub struct PluginPipeline

(P); + +impl Default for PluginPipeline { + fn default() -> Self { + Self(IdentityPlugin) + } +} + +impl PluginPipeline { + /// Create an empty [`PluginPipeline`]. + /// + /// You can use [`PluginPipeline::push`] to add plugins to it. + pub fn new() -> Self { + Self::default() + } +} + +impl

PluginPipeline

{ + /// Apply a new plugin after the ones that have already been registered. + /// + /// ```rust + /// 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 our example above, `LoggingPlugin` would run first, while `MetricsPlugin` is executed last. + /// + /// ## Implementation notes + /// + /// Plugins are applied to the underlying [`Operation`] 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 + /// // [...] + /// { + /// // [...] + /// fn map(&self, input: Operation) -> Operation { + /// input.layer(PrintLayer { name: Op::NAME }) + /// } + /// } + /// ``` + /// + /// 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)) + } +} + +impl Plugin for PluginPipeline +where + InnerPlugin: Plugin, +{ + type Service = InnerPlugin::Service; + type Layer = InnerPlugin::Layer; + + fn map(&self, input: Operation) -> Operation { + self.0.map(input) + } +}