Skip to content

Commit ddba460

Browse files
authored
Better distinguish model and HTTP plugins (#2827)
So far, servers have tacitly worked with the notion that plugins come in two flavors: "HTTP plugins" and "model plugins": - A HTTP plugin acts on the HTTP request before it is deserialized, and acts on the HTTP response after it is serialized. - A model plugin acts on the modeled operation input after it is deserialized, and acts on the modeled operation output or the modeled operation error before it is serialized. However, this notion was never reified in the type system. Thus, users who pass in a model plugin where a HTTP plugin is expected or viceversa in several APIs: ```rust let pipeline = PluginPipeline::new().push(http_plugin).push(model_plugin); let app = SimpleService::builder_with_plugins(http_plugins, IdentityPlugin) .post_operation(handler) /* ... */ .build() .unwrap(); ``` would get the typical Tower service compilation errors, which can get very confusing: ``` error[E0631]: type mismatch in function arguments --> simple/rust-server-codegen/src/main.rs:47:6 | 15 | fn new_svc<S, Ext>(inner: S) -> model_plugin::PostOperationService<S, Ext> { | -------------------------------------------------------------------------- found signature defined here ... 47 | .post_operation(post_operation) | ^^^^^^^^^^^^^^ expected due to this | = note: expected function signature `fn(Upgrade<RestJson1, (PostOperationInput, _), PostOperationService<aws_smithy_http_server::operation::IntoService<simple::operation_shape::PostOperation, _>, _>>) -> _` found function signature `fn(aws_smithy_http_server::operation::IntoService<simple::operation_shape::PostOperation, _>) -> _` = note: required for `LayerFn<fn(aws_smithy_http_server::operation::IntoService<..., ...>) -> ... {new_svc::<..., ...>}>` to implement `Layer<Upgrade<RestJson1, (PostOperationInput, _), PostOperationService<aws_smithy_http_server::operation::IntoService<simple::operation_shape::PostOperation, _>, _>>>` = note: the full type name has been written to '/local/home/davidpz/workplace/smithy-ws/src/SmithyRsSource/target/debug/deps/simple-6577f9f79749ceb9.long-type-4938700695428041031.txt' ``` This commit introduces the `HttpPlugin` and `ModelPlugin` marker traits, allowing plugins to be marked as an HTTP plugin, a model plugin, or both. It also removes the primary way of concatenating plugins, `PluginPipeline`, in favor of `HttpPlugins` and `ModelPlugins`, which eagerly check that whenever a plugin is `push`ed, it is of the expected type. The generated service type in the server SDKs' `builder_with_plugins` constructor now takes an `HttpPlugin` as its first parameter, and a `ModelPlugin` as its second parameter. I think this change perhaps goes counter to the generally accepted wisdom that trait bounds in Rust should be enforced "at the latest possible moment", that is, only when the behavior encoded in the trait implementation is relied upon in the code (citation needed). However, the result is that exposing the concepts of HTTP plugin and model plugin in the type system makes for code that is easier to reason about, and produces more helpful compiler error messages. Documentation about the plugin system has been expanded, particularly on how to implement model plugins. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent d753827 commit ddba460

File tree

21 files changed

+627
-156
lines changed

21 files changed

+627
-156
lines changed

CHANGELOG.next.toml

+38-2
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ message = """The middleware system has been reworked as we push for a unified, s
210210
211211
- A `ServiceShape` trait has been added.
212212
- The `Plugin` trait has been simplified.
213+
- The `HttpMarker` and `ModelMarker` marker traits have been added to better distinguish when plugins run and what they have access to.
213214
- The `Operation` structure has been removed.
214215
- A `Scoped` `Plugin` has been added.
215216
@@ -371,6 +372,8 @@ where
371372
PrintService { inner, name: Op::ID.name() }
372373
}
373374
}
375+
376+
impl HttpMarker for PrintPlugin { }
374377
```
375378
376379
Alternatively, using the new `ServiceShape`, implemented on `Ser`:
@@ -397,6 +400,11 @@ let app = PokemonService::builder_with_plugins(/* HTTP plugins */, /* model plug
397400
.unwrap();
398401
```
399402
403+
To better distinguish when a plugin runs and what it has access to, `Plugin`s now have to additionally implement the `HttpMarker` marker trait, the `ModelMarker` marker trait, or both:
404+
405+
- A HTTP plugin acts on the HTTP request before it is deserialized, and acts on the HTTP response after it is serialized.
406+
- A model plugin acts on the modeled operation input after it is deserialized, and acts on the modeled operation output or the modeled operation error before it is serialized.
407+
400408
The motivation behind this change is to simplify the job of middleware authors, separate concerns, accomodate common cases better, and to improve composition internally.
401409
402410
Because `Plugin` is now closer to `tower::Layer` we have two canonical converters:
@@ -413,6 +421,34 @@ let plugin = /* some plugin */;
413421
let layer = LayerPlugin::new::<SomeProtocol, SomeOperation>(plugin);
414422
```
415423
424+
## Removal of `PluginPipeline`
425+
426+
Since plugins now come in two flavors (those marked with `HttpMarker` and those marked with `ModelMarker`) that shouldn't be mixed in a collection of plugins, the primary way of concatenating plugins, `PluginPipeline` has been removed in favor of the `HttpPlugins` and `ModelPlugins` types, which eagerly check that whenever a plugin is pushed, it is of the expected type.
427+
428+
This worked before, but you wouldn't be able to do apply this collection of plugins anywhere; if you tried to, the compilation error messages would not be very helpful:
429+
430+
```rust
431+
use aws_smithy_http_server::plugin::PluginPipeline;
432+
433+
let pipeline = PluginPipeline::new().push(http_plugin).push(model_plugin);
434+
```
435+
436+
Now collections of plugins must contain plugins of the same flavor:
437+
438+
```rust
439+
use aws_smithy_http_server::plugin::{HttpPlugins, ModelPlugins};
440+
441+
let http_plugins = HttpPlugins::new()
442+
.push(http_plugin)
443+
// .push(model_plugin) // This fails to compile with a helpful error message.
444+
.push(&http_and_model_plugin);
445+
let model_plugins = ModelPlugins::new()
446+
.push(model_plugin)
447+
.push(&http_and_model_plugin);
448+
```
449+
450+
In the above example, `&http_and_model_plugin` implements both `HttpMarker` and `ModelMarker`, so we can add it to both collections.
451+
416452
## Removal of `Operation`
417453
418454
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
@@ -495,8 +531,8 @@ let scoped_plugin = Scoped::new::<SomeScope>(plugin);
495531
```
496532
497533
"""
498-
references = ["smithy-rs#2740", "smithy-rs#2759", "smithy-rs#2779"]
499-
meta = { "breaking" = true, "tada" = false, "bug" = false }
534+
references = ["smithy-rs#2740", "smithy-rs#2759", "smithy-rs#2779", "smithy-rs#2827"]
535+
meta = { "breaking" = true, "tada" = true, "bug" = false }
500536
author = "hlbarber"
501537

502538
[[smithy-rs]]

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRootGenerator.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,22 @@ open class ServerRootGenerator(
106106
//! #### Plugins
107107
//!
108108
//! The [`$serviceName::builder_with_plugins`] method, returning [`$builderName`],
109-
//! accepts a [`Plugin`](aws_smithy_http_server::plugin::Plugin).
109+
//! accepts a plugin marked with [`HttpMarker`](aws_smithy_http_server::plugin::HttpMarker) and a
110+
//! plugin marked with [`ModelMarker`](aws_smithy_http_server::plugin::ModelMarker).
110111
//! Plugins allow you to build middleware which is aware of the operation it is being applied to.
111112
//!
112113
//! ```rust
113114
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin;
114115
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin;
115116
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin;
116117
//! ## use #{Hyper}::Body;
117-
//! use #{SmithyHttpServer}::plugin::PluginPipeline;
118+
//! use #{SmithyHttpServer}::plugin::HttpPlugins;
118119
//! use $crateName::{$serviceName, $builderName};
119120
//!
120-
//! let plugins = PluginPipeline::new()
121+
//! let http_plugins = HttpPlugins::new()
121122
//! .push(LoggingPlugin)
122123
//! .push(MetricsPlugin);
123-
//! let builder: $builderName<Body, _, _> = $serviceName::builder_with_plugins(plugins, IdentityPlugin);
124+
//! let builder: $builderName<Body, _, _> = $serviceName::builder_with_plugins(http_plugins, IdentityPlugin);
124125
//! ```
125126
//!
126127
//! Check out [`#{SmithyHttpServer}::plugin`] to learn more about plugins.

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerRuntimeTypesReExportsGenerator.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
99
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
1010
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
1111
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
12-
import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType
1312

1413
class ServerRuntimeTypesReExportsGenerator(
1514
codegenContext: CodegenContext,
1615
) {
1716
private val runtimeConfig = codegenContext.runtimeConfig
1817
private val codegenScope = arrayOf(
19-
"Router" to ServerRuntimeType.router(runtimeConfig),
2018
"SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(runtimeConfig).toType(),
2119
)
2220

@@ -30,8 +28,11 @@ class ServerRuntimeTypesReExportsGenerator(
3028
pub use #{SmithyHttpServer}::operation::OperationShape;
3129
}
3230
pub mod plugin {
31+
pub use #{SmithyHttpServer}::plugin::HttpPlugins;
32+
pub use #{SmithyHttpServer}::plugin::ModelPlugins;
33+
pub use #{SmithyHttpServer}::plugin::HttpMarker;
34+
pub use #{SmithyHttpServer}::plugin::ModelMarker;
3335
pub use #{SmithyHttpServer}::plugin::Plugin;
34-
pub use #{SmithyHttpServer}::plugin::PluginPipeline;
3536
pub use #{SmithyHttpServer}::plugin::PluginStack;
3637
}
3738
pub mod request {

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt

+18-17
Original file line numberDiff line numberDiff line change
@@ -136,31 +136,31 @@ class ServerServiceGenerator(
136136
where
137137
HandlerType: #{SmithyHttpServer}::operation::Handler<crate::operation_shape::$structName, HandlerExtractors>,
138138
139-
ModelPlugin: #{SmithyHttpServer}::plugin::Plugin<
139+
ModelPl: #{SmithyHttpServer}::plugin::Plugin<
140140
$serviceName,
141141
crate::operation_shape::$structName,
142142
#{SmithyHttpServer}::operation::IntoService<crate::operation_shape::$structName, HandlerType>
143143
>,
144144
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>: #{SmithyHttpServer}::plugin::Plugin<
145145
$serviceName,
146146
crate::operation_shape::$structName,
147-
ModelPlugin::Output
147+
ModelPl::Output
148148
>,
149-
HttpPlugin: #{SmithyHttpServer}::plugin::Plugin<
149+
HttpPl: #{SmithyHttpServer}::plugin::Plugin<
150150
$serviceName,
151151
crate::operation_shape::$structName,
152152
<
153153
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>
154154
as #{SmithyHttpServer}::plugin::Plugin<
155155
$serviceName,
156156
crate::operation_shape::$structName,
157-
ModelPlugin::Output
157+
ModelPl::Output
158158
>
159159
>::Output
160160
>,
161161
162-
HttpPlugin::Output: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
163-
<HttpPlugin::Output as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
162+
HttpPl::Output: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
163+
<HttpPl::Output as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
164164
165165
{
166166
use #{SmithyHttpServer}::operation::OperationShapeExt;
@@ -199,31 +199,31 @@ class ServerServiceGenerator(
199199
where
200200
S: #{SmithyHttpServer}::operation::OperationService<crate::operation_shape::$structName, ServiceExtractors>,
201201
202-
ModelPlugin: #{SmithyHttpServer}::plugin::Plugin<
202+
ModelPl: #{SmithyHttpServer}::plugin::Plugin<
203203
$serviceName,
204204
crate::operation_shape::$structName,
205205
#{SmithyHttpServer}::operation::Normalize<crate::operation_shape::$structName, S>
206206
>,
207207
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>: #{SmithyHttpServer}::plugin::Plugin<
208208
$serviceName,
209209
crate::operation_shape::$structName,
210-
ModelPlugin::Output
210+
ModelPl::Output
211211
>,
212-
HttpPlugin: #{SmithyHttpServer}::plugin::Plugin<
212+
HttpPl: #{SmithyHttpServer}::plugin::Plugin<
213213
$serviceName,
214214
crate::operation_shape::$structName,
215215
<
216216
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>
217217
as #{SmithyHttpServer}::plugin::Plugin<
218218
$serviceName,
219219
crate::operation_shape::$structName,
220-
ModelPlugin::Output
220+
ModelPl::Output
221221
>
222222
>::Output
223223
>,
224224
225-
HttpPlugin::Output: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
226-
<HttpPlugin::Output as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
225+
HttpPl::Output: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
226+
<HttpPl::Output as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
227227
228228
{
229229
use #{SmithyHttpServer}::operation::OperationShapeExt;
@@ -394,16 +394,16 @@ class ServerServiceGenerator(
394394

395395
/** Returns a `Writable` containing the builder struct definition and its implementations. */
396396
private fun builder(): Writable = writable {
397-
val builderGenerics = listOf(builderBodyGenericTypeName, "HttpPlugin", "ModelPlugin").joinToString(", ")
397+
val builderGenerics = listOf(builderBodyGenericTypeName, "HttpPl", "ModelPl").joinToString(", ")
398398
rustTemplate(
399399
"""
400400
/// The service builder for [`$serviceName`].
401401
///
402402
/// Constructed via [`$serviceName::builder_with_plugins`] or [`$serviceName::builder_without_plugins`].
403403
pub struct $builderName<$builderGenerics> {
404404
${builderFields.joinToString(", ")},
405-
http_plugin: HttpPlugin,
406-
model_plugin: ModelPlugin
405+
http_plugin: HttpPl,
406+
model_plugin: ModelPl
407407
}
408408
409409
impl<$builderGenerics> $builderName<$builderGenerics> {
@@ -473,9 +473,10 @@ class ServerServiceGenerator(
473473
///
474474
/// Use [`$serviceName::builder_without_plugins`] if you don't need to apply plugins.
475475
///
476-
/// Check out [`PluginPipeline`](#{SmithyHttpServer}::plugin::PluginPipeline) if you need to apply
476+
/// Check out [`HttpPlugins`](#{SmithyHttpServer}::plugin::HttpPlugins) and
477+
/// [`ModelPlugins`](#{SmithyHttpServer}::plugin::ModelPlugins) if you need to apply
477478
/// multiple plugins.
478-
pub fn builder_with_plugins<Body, HttpPlugin, ModelPlugin>(http_plugin: HttpPlugin, model_plugin: ModelPlugin) -> $builderName<Body, HttpPlugin, ModelPlugin> {
479+
pub fn builder_with_plugins<Body, HttpPl: #{SmithyHttpServer}::plugin::HttpMarker, ModelPl: #{SmithyHttpServer}::plugin::ModelMarker>(http_plugin: HttpPl, model_plugin: ModelPl) -> $builderName<Body, HttpPl, ModelPl> {
479480
$builderName {
480481
#{NotSetFields:W},
481482
http_plugin,

design/src/server/anatomy.md

+16-16
Original file line numberDiff line numberDiff line change
@@ -466,40 +466,40 @@ stateDiagram-v2
466466
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.
467467

468468
You might find yourself wanting to apply _multiple_ plugins to your service.
469-
This can be accommodated via [`PluginPipeline`].
469+
This can be accommodated via [`HttpPlugins`] and [`ModelPlugins`].
470470

471471
```rust
472472
# extern crate aws_smithy_http_server;
473-
use aws_smithy_http_server::plugin::PluginPipeline;
473+
use aws_smithy_http_server::plugin::HttpPlugins;
474474
# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin;
475475
# use aws_smithy_http_server::plugin::IdentityPlugin as MetricsPlugin;
476476

477-
let pipeline = PluginPipeline::new().push(LoggingPlugin).push(MetricsPlugin);
477+
let http_plugins = HttpPlugins::new().push(LoggingPlugin).push(MetricsPlugin);
478478
```
479479

480480
The plugins' runtime logic is executed in registration order.
481481
In the example above, `LoggingPlugin` would run first, while `MetricsPlugin` is executed last.
482482

483-
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.
483+
If you are vending a plugin, you can leverage `HttpPlugins` or `ModelPlugins` as an extension point: you can add custom methods to it using an extension trait.
484484
For example:
485485

486486
```rust
487487
# extern crate aws_smithy_http_server;
488-
use aws_smithy_http_server::plugin::{PluginPipeline, PluginStack};
488+
use aws_smithy_http_server::plugin::{HttpPlugins, PluginStack};
489489
# use aws_smithy_http_server::plugin::IdentityPlugin as LoggingPlugin;
490490
# use aws_smithy_http_server::plugin::IdentityPlugin as AuthPlugin;
491491

492492
pub trait AuthPluginExt<CurrentPlugins> {
493-
fn with_auth(self) -> PluginPipeline<PluginStack<AuthPlugin, CurrentPlugins>>;
493+
fn with_auth(self) -> HttpPlugins<PluginStack<AuthPlugin, CurrentPlugins>>;
494494
}
495495

496-
impl<CurrentPlugins> AuthPluginExt<CurrentPlugins> for PluginPipeline<CurrentPlugins> {
497-
fn with_auth(self) -> PluginPipeline<PluginStack<AuthPlugin, CurrentPlugins>> {
496+
impl<CurrentPlugins> AuthPluginExt<CurrentPlugins> for HttpPlugins<CurrentPlugins> {
497+
fn with_auth(self) -> HttpPlugins<PluginStack<AuthPlugin, CurrentPlugins>> {
498498
self.push(AuthPlugin)
499499
}
500500
}
501501

502-
let pipeline = PluginPipeline::new()
502+
let http_plugins = HttpPlugins::new()
503503
.push(LoggingPlugin)
504504
// Our custom method!
505505
.with_auth();
@@ -518,15 +518,15 @@ You can create an instance of a service builder by calling either `builder_witho
518518
/// The service builder for [`PokemonService`].
519519
///
520520
/// Constructed via [`PokemonService::builder`].
521-
pub struct PokemonServiceBuilder<Body, HttpPlugin, ModelPlugin> {
521+
pub struct PokemonServiceBuilder<Body, HttpPl, ModelPl> {
522522
capture_pokemon_operation: Option<Route<Body>>,
523523
empty_operation: Option<Route<Body>>,
524524
get_pokemon_species: Option<Route<Body>>,
525525
get_server_statistics: Option<Route<Body>>,
526526
get_storage: Option<Route<Body>>,
527527
health_check_operation: Option<Route<Body>>,
528-
http_plugin: HttpPlugin,
529-
model_plugin: ModelPlugin
528+
http_plugin: HttpPl,
529+
model_plugin: ModelPl,
530530
}
531531
```
532532

@@ -537,7 +537,7 @@ The builder has two setter methods for each [Smithy Operation](https://awslabs.g
537537
where
538538
HandlerType:Handler<GetPokemonSpecies, HandlerExtractors>,
539539
540-
ModelPlugin: Plugin<
540+
ModelPl: Plugin<
541541
PokemonService,
542542
GetPokemonSpecies,
543543
IntoService<GetPokemonSpecies, HandlerType>
@@ -547,7 +547,7 @@ The builder has two setter methods for each [Smithy Operation](https://awslabs.g
547547
GetPokemonSpecies,
548548
ModelPlugin::Output
549549
>,
550-
HttpPlugin: Plugin<
550+
HttpPl: Plugin<
551551
PokemonService,
552552
GetPokemonSpecies,
553553
UpgradePlugin::<UpgradeExtractors>::Output
@@ -565,7 +565,7 @@ The builder has two setter methods for each [Smithy Operation](https://awslabs.g
565565
where
566566
S: OperationService<GetPokemonSpecies, ServiceExtractors>,
567567
568-
ModelPlugin: Plugin<
568+
ModelPl: Plugin<
569569
PokemonService,
570570
GetPokemonSpecies,
571571
Normalize<GetPokemonSpecies, S>
@@ -575,7 +575,7 @@ The builder has two setter methods for each [Smithy Operation](https://awslabs.g
575575
GetPokemonSpecies,
576576
ModelPlugin::Output
577577
>,
578-
HttpPlugin: Plugin<
578+
HttpPl: Plugin<
579579
PokemonService,
580580
GetPokemonSpecies,
581581
UpgradePlugin::<UpgradeExtractors>::Output

design/src/server/instrumentation.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ This is enabled via the `instrument` method provided by the `aws_smithy_http_ser
6666
# let handler = |req: GetPokemonSpeciesInput| async { Result::<GetPokemonSpeciesOutput, GetPokemonSpeciesError>::Ok(todo!()) };
6767
use aws_smithy_http_server::{
6868
instrumentation::InstrumentExt,
69-
plugin::{IdentityPlugin, PluginPipeline}
69+
plugin::{IdentityPlugin, HttpPlugins}
7070
};
7171
use pokemon_service_server_sdk::PokemonService;
7272
73-
let http_plugins = PluginPipeline::new().instrument();
73+
let http_plugins = HttpPlugins::new().instrument();
7474
let app = PokemonService::builder_with_plugins(http_plugins, IdentityPlugin)
7575
.get_pokemon_species(handler)
7676
/* ... */

0 commit comments

Comments
 (0)