Unverified Commit 044ebeac authored by Luca Palmieri's avatar Luca Palmieri Committed by GitHub
Browse files

Implement RFC 23, Part 2 - Plugin pipeline (#1971)



* Implement RFC 23, with the exception of PluginBuilder

* Update documentation.

* Avoid star re-exports.

* Elide implementation details in `Upgradable`.

* Update wording in docs to avoid personal pronouns.

* Update `Upgradable`'s documentation.

* Template the service builder name.

* Template MissingOperationsError directly.

* Code-generate an array directly.

* Sketch out the implementation of `PluginPipeline`.
Remove `PluginExt`.
Add a public constructor for `FilterByOperationName`.

* Ask for a `PluginPipeline` as input for the generated builder.

* Rename `add` to `push`.

* Remove Pluggable.
Rename `composer` module to `pipeline`.

* Remove all mentions of `Pluggable` from docs and examples.

* Fix punctuation.

* Rename variable from `composer` to `pipeline` in doc examples.

* Add a free-standing function for filtering.

* Rename.

* Rename.

* Update design/src/server/anatomy.md

Co-authored-by: default avatardavid-perez <d@vidp.dev>

* Use `rust` where we do not need templating.

* Remove unused variable.

* Add `expect` message to point users at our issue board in case a panic slips through.

* Typo.

* Update `expect` error message.

* Refactor the `for` loop in ``requestSpecMap` into an `associateWith` call.

* Remove unnecessary bound - since `new` is private, the condition is already enforced via `filter_by_operation_name`.

* Use `Self` in `new`.

* Rename `inner` to `into_inner`

* Rename `concat` to `append` to correctly mirror Vec's API terminology.

* Fix codegen to use renamed method.

* Cut down the public API surface to key methods for a sequence-like interface.

* Add a note about ordering.

* Add method docs.

* Add Default implementation.

* Fix new pokemon bin example.

* Fix new pokemon bin example.

* Fix code-generated builder.

* Fix unresolved symbolProvider.

* Assign the `expect` error message to a variable.

* Do not require a PluginPipeline as input to `builder_with_plugins`.

* Reverse plugin application order.

* Upgrade documentation.

* Add a test to verify that plugin layers are executed in registration order.

* Add license header.

* Update middleware.md

* Typo.

* Fix more builder() calls.

Co-authored-by: default avatardavid-perez <d@vidp.dev>
parent 7ab8f56d
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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<Body, Plugin>(plugin: Plugin) -> $builderName<Body, Plugin> {
                    $builderName {
                        #{NotSetFields:W},
+31 −123
Original line number Diff line number Diff line
@@ -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<Protocol, Op, S, L> {
    type Service;
    type Layer;
@@ -611,7 +608,7 @@ The `Upgradable::upgrade` method on `Operation<S, L>`, 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<B> {
        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`]&#40;https://github.com/awslabs/smithy-rs/blob/4c5cbc39384f0d949d7693eb87b5853fe72629cd/rust-runtime/aws-smithy-http-server/src/plugin.rs#L8-L29&#41; 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`]&#40;crate::operation::Upgradable&#41; 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<PrintPlugin> {)

[//]: # (///     fn print&#40;self&#41; -> Self::Output where Self: Sized {)

[//]: # (///         self.apply&#40;PrintPlugin&#41;)

[//]: # (///     })

[//]: # (/// })

[//]: # (///)

[//]: # (/// impl<Builder> PrintExt for Builder where Builder: Pluggable<PrintPlugin> {})

[//]: # (/// ```)

[//]: # (pub trait Pluggable<NewPlugin> {)

[//]: # (    type Output;)

[//]: # ()
[//]: # (    /// Applies a [`Plugin`] to the service builder.)

[//]: # (    fn apply&#40;self, plugin: NewPlugin&#41; -> 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<Op1, Op2, Op3, Op4, Op5, Op6, Pl> {)

[//]: # (    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<Op1, Op2, /* ... */, Pl, NewPl> Pluggable<NewPl> for PokemonServiceBuilder<Op1, Op2, /* ... */, Pl>)

[//]: # ({)

[//]: # (    type Output = PokemonServiceBuilder<Op1, Exts1, PluginStack<Pl, NewPl>>;)

[//]: # (    fn apply&#40;self, plugin: NewPl&#41; -> 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&#40;self.plugin, plugin&#41;,)
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<CurrentPlugins> {
    fn with_auth(self) -> PluginPipeline<PluginStack<AuthPlugin, CurrentPlugins>>;
}

[//]: # (```)
impl<CurrentPlugins> AuthPluginExt<CurrentPlugins> for PluginPipeline<CurrentPlugins> {
    fn with_auth(self) -> PluginPipeline<PluginStack<AuthPlugin, CurrentPlugins>> {
        self.push(AuthPlugin)
    }
}

[//]: # ()
[//]: # (Here `PluginStack` works in a similar way to [`tower::layer::util::Stack`]&#40;https://docs.rs/tower/latest/tower/layer/util/struct.Stack.html&#41; - 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

+22 −30
Original line number Diff line number Diff line
@@ -135,7 +135,7 @@ The output of the Smithy service builder provides the user with a `Service<http:

```rust
// This is a HTTP `Service`.
let app /* : PokemonService<Route<B>> */ = PokemonService::builder()
let app /* : PokemonService<Route<B>> */ = 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<Route<B>> */ = PokemonService::builder()
let app /* : PokemonService<Route<B>> */ = 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<Route<B>> */ = PokemonService::builder()
let app /* : PokemonService<Route<B>> */ = 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<Route<B>> */ = PokemonService::builder()
let app /* : PokemonService<Route<B>> */ = PokemonService::builder_without_plugins()
    .get_pokemon_species_operation(layered_handler)
    /* ... */
    .build();
@@ -307,44 +307,36 @@ impl<Op, S, L> Plugin<AwsRestXml, Op, S, L> 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<NewPlugin> {
    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<PrintPlugin> {
/// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`].
pub trait PrintExt<ExistingPlugins> {
    /// 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<PluginStack<PrintPlugin, ExistingPlugins>>;
}

impl<ExistingPlugins> PrintExt<ExistingPlugins> for PluginPipeline<ExistingPlugins> {
    fn print(self) -> PluginPipeline<PluginStack<PrintPlugin, ExistingPlugins>> {
        self.push(PrintPlugin)
    }
}
```

which allows for
This allows for:

```rust
let app /* : PokemonService<Route<B>> */ = PokemonService::builder()
let plugin_pipeline = PluginPipeline::new()
    // [..other plugins..]
    // The custom method!
    .print();
let app /* : PokemonService<Route<B>> */ = 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.
+9 −12
Original line number Diff line number Diff line
@@ -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<PrintPlugin> {
/// This provides a [`print`](PrintExt::print) method on [`PluginPipeline`].
pub trait PrintExt<ExistingPlugins> {
    /// 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<PluginStack<PrintPlugin, ExistingPlugins>>;
}

impl<Builder> PrintExt for Builder where Builder: Pluggable<PrintPlugin> {}
impl<ExistingPlugins> PrintExt<ExistingPlugins> for PluginPipeline<ExistingPlugins> {
    fn print(self) -> PluginPipeline<PluginStack<PrintPlugin, ExistingPlugins>> {
        self.push(PrintPlugin)
    }
}
+121 −0
Original line number Diff line number Diff line
/*
 * 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<SdkBody>;
}

impl<H, R> OperationExt for aws_smithy_http::operation::Operation<H, R> {
    fn into_http(self) -> http::Request<SdkBody> {
        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<Mutex<Vec<&'static str>>>,
}

impl SentinelPlugin {
    pub fn new(name: &'static str, output: Arc<Mutex<Vec<&'static str>>>) -> Self {
        Self { name, output: output }
    }
}

impl<Protocol, Op, S, L> Plugin<Protocol, Op, S, L> for SentinelPlugin {
    type Service = S;
    type Layer = Stack<L, SentinelLayer>;

    fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer> {
        input.layer(SentinelLayer {
            name: self.name,
            output: self.output.clone(),
        })
    }
}

/// A [`Service`] that adds a print log.
#[derive(Clone, Debug)]
pub struct SentinelService<S> {
    inner: S,
    output: Arc<Mutex<Vec<&'static str>>>,
    name: &'static str,
}

impl<R, S> Service<R> for SentinelService<S>
where
    S: Service<R>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        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<Mutex<Vec<&'static str>>>,
}

impl<S> Layer<S> for SentinelLayer {
    type Service = SentinelService<S>;

    fn layer(&self, service: S) -> Self::Service {
        SentinelService {
            inner: service,
            output: self.output.clone(),
            name: self.name,
        }
    }
}
Loading