Skip to content

Commit 11cf41d

Browse files
authored
Simplify Plugin trait (#2740)
## Motivation and Context #2444 ## Description - Simplify `Plugin`, make it closer to `Layer`. - Remove `Operation`.
1 parent ec45767 commit 11cf41d

File tree

32 files changed

+1091
-1055
lines changed

32 files changed

+1091
-1055
lines changed

CHANGELOG.next.toml

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ message = """`ShapeId` is the new structure used to represent a shape, with its
141141
Before you had an operation and an absolute name as its `NAME` member. You could apply a plugin only to some selected operation:
142142
143143
```
144-
filter_by_operation_name(plugin, |name| name != Op::NAME);
144+
filter_by_operation_name(plugin, |name| name != Op::ID);
145145
```
146146
147147
Your new filter selects on an operation's absolute name, namespace or name.
148148
149149
```
150-
filter_by_operation_id(plugin, |id| id.name() != Op::NAME.name());
150+
filter_by_operation_id(plugin, |id| id.name() != Op::ID.name());
151151
```
152152
153153
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<dyn ResolveEndpoint>` have now been replaced
168168
references = ["smithy-rs#2758"]
169169
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
170170
author = "ysaito1001"
171+
172+
[[smithy-rs]]
173+
message = """
174+
The `Plugin` trait has now been simplified and the `Operation` struct has been removed.
175+
176+
## Simplication of the `Plugin` trait
177+
178+
Previously,
179+
180+
```rust
181+
trait Plugin<P, Op, S, L> {
182+
type Service;
183+
type Layer;
184+
185+
fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer>;
186+
}
187+
```
188+
189+
modified an `Operation`.
190+
191+
Now,
192+
193+
```rust
194+
trait Plugin<Protocol, Operation, S> {
195+
type Service;
196+
197+
fn apply(&self, svc: S) -> Self::Service;
198+
}
199+
```
200+
201+
maps a `tower::Service` to a `tower::Service`. This is equivalent to `tower::Layer` with two extra type parameters: `Protocol` and `Operation`.
202+
203+
The following middleware setup
204+
205+
```rust
206+
pub struct PrintService<S> {
207+
inner: S,
208+
name: &'static str,
209+
}
210+
211+
impl<R, S> Service<R> for PrintService<S>
212+
where
213+
S: Service<R>,
214+
{
215+
async fn call(&mut self, req: R) -> Self::Future {
216+
println!("Hi {}", self.name);
217+
self.inner.call(req)
218+
}
219+
}
220+
221+
pub struct PrintLayer {
222+
name: &'static str,
223+
}
224+
225+
impl<S> Layer<S> for PrintLayer {
226+
type Service = PrintService<S>;
227+
228+
fn layer(&self, service: S) -> Self::Service {
229+
PrintService {
230+
inner: service,
231+
name: self.name,
232+
}
233+
}
234+
}
235+
236+
pub struct PrintPlugin;
237+
238+
impl<P, Op, S, L> Plugin<P, Op, S, L> for PrintPlugin
239+
where
240+
Op: OperationShape,
241+
{
242+
type Service = S;
243+
type Layer = Stack<L, PrintLayer>;
244+
245+
fn map(&self, input: Operation<S, L>) -> Operation<Self::Service, Self::Layer> {
246+
input.layer(PrintLayer { name: Op::NAME })
247+
}
248+
}
249+
```
250+
251+
now becomes
252+
253+
```rust
254+
pub struct PrintService<S> {
255+
inner: S,
256+
name: &'static str,
257+
}
258+
259+
impl<R, S> Service<R> for PrintService<S>
260+
where
261+
S: Service<R>,
262+
{
263+
async fn call(&mut self, req: R) -> Self::Future {
264+
println!("Hi {}", self.name);
265+
self.inner.call(req)
266+
}
267+
}
268+
269+
pub struct PrintPlugin;
270+
271+
impl<P, Op, S, L> Plugin<P, Op, S, L> for PrintPlugin
272+
where
273+
Op: OperationShape,
274+
{
275+
type Service = PrintService<S>;
276+
277+
fn apply(&self, svc: S) -> Self::Service {
278+
PrintService { inner, name: Op::ID.name() }
279+
}
280+
}
281+
```
282+
283+
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:
284+
285+
```rust
286+
let app = PokemonService::builder_with_plugins(/* HTTP plugins */, /* model plugins */)
287+
/* setters */
288+
.build()
289+
.unwrap();
290+
```
291+
292+
The motivation behind this change is to simplify the job of middleware authors, separate concerns, accomodate common cases better, and to improve composition internally.
293+
294+
Because `Plugin` is now closer to `tower::Layer` we have two canonical converters:
295+
296+
```rust
297+
use aws_smithy_http_server::plugin::{PluginLayer, LayerPlugin};
298+
299+
// Convert from `Layer` to `Plugin` which applies uniformly across all operations
300+
let layer = /* some layer */;
301+
let plugin = PluginLayer(layer);
302+
303+
// Convert from `Plugin` to `Layer` for some fixed protocol and operation
304+
let plugin = /* some plugin */;
305+
let layer = LayerPlugin::new::<SomeProtocol, SomeOperation>(plugin);
306+
```
307+
308+
## Remove `Operation`
309+
310+
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
311+
312+
```rust
313+
let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_service(/* tower::Service */);
314+
315+
let app = PokemonService::builder_without_plugins()
316+
.get_pokemon_species_operation(operation)
317+
/* other setters */
318+
.build()
319+
.unwrap();
320+
```
321+
322+
to set an operation with a `tower::Service`, and
323+
324+
```rust
325+
let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_service(/* tower::Service */).layer(/* layer */);
326+
let operation /* : Operation<_, _> */ = GetPokemonSpecies::from_handler(/* closure */).layer(/* layer */);
327+
328+
let app = PokemonService::builder_without_plugins()
329+
.get_pokemon_species_operation(operation)
330+
/* other setters */
331+
.build()
332+
.unwrap();
333+
```
334+
335+
to add a `tower::Layer` (acting on HTTP requests/responses post-routing) to a single operation.
336+
337+
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:
338+
339+
```rust
340+
let app = PokemonService::builder_without_plugins()
341+
.get_pokemon_species_service(/* tower::Service */)
342+
/* other setters */
343+
.build()
344+
.unwrap();
345+
```
346+
347+
Applying a `tower::Layer` to a _single_ operation is now done through the `Plugin` API:
348+
349+
```rust
350+
use aws_smithy_http_server::plugin::{PluginLayer, filter_by_operation_name, IdentityPlugin};
351+
352+
let plugin = PluginLayer(/* layer */);
353+
let scoped_plugin = filter_by_operation_name(plugin, |id| id == GetPokemonSpecies::ID);
354+
355+
let app = PokemonService::builder_with_plugins(scoped_plugin, IdentityPlugin)
356+
.get_pokemon_species(/* handler */)
357+
/* other setters */
358+
.build()
359+
.unwrap();
360+
```
361+
362+
"""
363+
references = ["smithy-rs#2740"]
364+
meta = { "breaking" = true, "tada" = false, "bug" = false }
365+
author = "hlbarber"

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

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.module
2727
import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticInputTrait
2828
import software.amazon.smithy.rust.codegen.core.smithy.traits.SyntheticOutputTrait
2929
import software.amazon.smithy.rust.codegen.core.util.hasTrait
30-
import software.amazon.smithy.rust.codegen.core.util.toPascalCase
3130
import software.amazon.smithy.rust.codegen.core.util.toSnakeCase
3231
import software.amazon.smithy.rust.codegen.server.smithy.generators.DocHandlerGenerator
3332
import software.amazon.smithy.rust.codegen.server.smithy.generators.handlerImports
@@ -73,49 +72,22 @@ class ServerModuleDocProvider(private val codegenContext: ServerCodegenContext)
7372
val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id })
7473

7574
val firstOperation = operations.first() ?: return@writable
76-
val firstOperationSymbol = codegenContext.symbolProvider.toSymbol(firstOperation)
77-
val firstOperationName = firstOperationSymbol.name.toPascalCase()
7875
val crateName = codegenContext.settings.moduleName.toSnakeCase()
7976

8077
rustTemplate(
8178
"""
8279
/// A collection of types representing each operation defined in the service closure.
8380
///
84-
/// ## Constructing an [`Operation`](#{SmithyHttpServer}::operation::OperationShapeExt)
85-
///
86-
/// To apply middleware to specific operations the [`Operation`](#{SmithyHttpServer}::operation::Operation)
87-
/// API must be used.
88-
///
89-
/// Using the [`OperationShapeExt`](#{SmithyHttpServer}::operation::OperationShapeExt) trait
90-
/// implemented on each ZST we can construct an [`Operation`](#{SmithyHttpServer}::operation::Operation)
91-
/// with appropriate constraints given by Smithy.
92-
///
93-
/// #### Example
94-
///
95-
/// ```no_run
96-
/// use $crateName::operation_shape::$firstOperationName;
97-
/// use #{SmithyHttpServer}::operation::OperationShapeExt;
98-
///
99-
#{HandlerImports:W}
100-
///
101-
#{Handler:W}
102-
///
103-
/// let operation = $firstOperationName::from_handler(handler)
104-
/// .layer(todo!("Provide a layer implementation"));
105-
/// ```
106-
///
107-
/// ## Use as Marker Structs
108-
///
109-
/// The [plugin system](#{SmithyHttpServer}::plugin) also makes use of these
81+
/// The [plugin system](#{SmithyHttpServer}::plugin) makes use of these
11082
/// [zero-sized types](https://doc.rust-lang.org/nomicon/exotic-sizes.html##zero-sized-types-zsts) (ZSTs) to
111-
/// parameterize [`Plugin`](#{SmithyHttpServer}::plugin::Plugin) implementations. The traits, such as
112-
/// [`OperationShape`](#{SmithyHttpServer}::operation::OperationShape) can be used to provide
83+
/// parameterize [`Plugin`](#{SmithyHttpServer}::plugin::Plugin) implementations. Their traits, such as
84+
/// [`OperationShape`](#{SmithyHttpServer}::operation::OperationShape), can be used to provide
11385
/// operation specific information to the [`Layer`](#{Tower}::Layer) being applied.
11486
""".trimIndent(),
11587
"SmithyHttpServer" to
11688
ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(),
11789
"Tower" to ServerCargoDependency.Tower.toType(),
118-
"Handler" to DocHandlerGenerator(codegenContext, firstOperation, "handler", commentToken = "///")::render,
90+
"Handler" to DocHandlerGenerator(codegenContext, firstOperation, "handler", commentToken = "///").docSignature(),
11991
"HandlerImports" to handlerImports(crateName, operations, commentToken = "///"),
12092
)
12193
}

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
package software.amazon.smithy.rust.codegen.server.smithy.generators
77

88
import software.amazon.smithy.model.shapes.OperationShape
9-
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
109
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
1110
import software.amazon.smithy.rust.codegen.core.rustlang.rust
12-
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
1311
import software.amazon.smithy.rust.codegen.core.rustlang.writable
1412
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
1513
import software.amazon.smithy.rust.codegen.core.util.inputShape
@@ -55,14 +53,25 @@ class DocHandlerGenerator(
5553
}
5654
}
5755

58-
fun render(writer: RustWriter) {
59-
// This assumes that the `error` (if applicable) `input`, and `output` modules have been imported by the
60-
// caller and hence are in scope.
61-
writer.rustTemplate(
62-
"""
63-
#{Handler:W}
64-
""",
65-
"Handler" to docSignature(),
66-
)
56+
/**
57+
* Similarly to `docSignature`, returns the function signature of an operation handler implementation, with the
58+
* difference that we don't ellide the error for use in `tower::service_fn`.
59+
*/
60+
fun docFixedSignature(): Writable {
61+
val errorT = if (operation.errors.isEmpty()) {
62+
"std::convert::Infallible"
63+
} else {
64+
"${ErrorModule.name}::${errorSymbol.name}"
65+
}
66+
67+
return writable {
68+
rust(
69+
"""
70+
$commentToken async fn $handlerName(input: ${InputModule.name}::${inputSymbol.name}) -> Result<${OutputModule.name}::${outputSymbol.name}, $errorT> {
71+
$commentToken todo!()
72+
$commentToken }
73+
""".trimIndent(),
74+
)
75+
}
6776
}
6877
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class ServerOperationGenerator(
5656
pub struct $operationName;
5757
5858
impl #{SmithyHttpServer}::operation::OperationShape for $operationName {
59-
const NAME: #{SmithyHttpServer}::shape_id::ShapeId = #{SmithyHttpServer}::shape_id::ShapeId::new(${operationIdAbsolute.dq()}, ${operationId.namespace.dq()}, ${operationId.name.dq()});
59+
const ID: #{SmithyHttpServer}::shape_id::ShapeId = #{SmithyHttpServer}::shape_id::ShapeId::new(${operationIdAbsolute.dq()}, ${operationId.namespace.dq()}, ${operationId.name.dq()});
6060
6161
type Input = crate::input::${operationName}Input;
6262
type Output = crate::output::${operationName}Output;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ open class ServerRootGenerator(
5353
val hasErrors = operations.any { it.errors.isNotEmpty() }
5454
val handlers: Writable = operations
5555
.map { operation ->
56-
DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!")::render
56+
DocHandlerGenerator(codegenContext, operation, builderFieldNames[operation]!!, "//!").docSignature()
5757
}
5858
.join("//!\n")
5959

@@ -110,6 +110,7 @@ open class ServerRootGenerator(
110110
//! Plugins allow you to build middleware which is aware of the operation it is being applied to.
111111
//!
112112
//! ```rust
113+
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin;
113114
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as LoggingPlugin;
114115
//! ## use #{SmithyHttpServer}::plugin::IdentityPlugin as MetricsPlugin;
115116
//! ## use #{Hyper}::Body;
@@ -119,7 +120,7 @@ open class ServerRootGenerator(
119120
//! let plugins = PluginPipeline::new()
120121
//! .push(LoggingPlugin)
121122
//! .push(MetricsPlugin);
122-
//! let builder: $builderName<Body, _> = $serviceName::builder_with_plugins(plugins);
123+
//! let builder: $builderName<Body, _, _> = $serviceName::builder_with_plugins(plugins, IdentityPlugin);
123124
//! ```
124125
//!
125126
//! Check out [`#{SmithyHttpServer}::plugin`] to learn more about plugins.

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ class ServerRuntimeTypesReExportsGenerator(
2828
}
2929
pub mod operation {
3030
pub use #{SmithyHttpServer}::operation::OperationShape;
31-
pub use #{SmithyHttpServer}::operation::Operation;
3231
}
3332
pub mod plugin {
3433
pub use #{SmithyHttpServer}::plugin::Plugin;

0 commit comments

Comments
 (0)