Skip to content

Commit 988eb61

Browse files
Add Scoped Plugin (#2759)
## Motivation and Context The [`FilterByOperationName`](https://docs.rs/aws-smithy-http-server/0.55.4/aws_smithy_http_server/plugin/struct.FilterByOperationName.html) allows the customer to filter application of a plugin. However this is a _runtime_ filter. A faster and type safe alternative would be a nice option. ## Description Add `Scoped` `Plugin` and `scope` macro. --------- Co-authored-by: david-perez <d@vidp.dev>
1 parent 312d190 commit 988eb61

File tree

8 files changed

+453
-11
lines changed

8 files changed

+453
-11
lines changed

CHANGELOG.next.toml

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ author = "ysaito1001"
183183

184184
[[smithy-rs]]
185185
message = """
186+
The middleware system has been reworked as we push for a unified, simple, and consistent API. The following changes have been made in service of this goal:
187+
188+
- The `Plugin` trait has been simplified.
189+
- The `Operation` structure has been removed.
190+
- A `Scoped` `Plugin` has been added.
191+
186192
The `Plugin` trait has now been simplified and the `Operation` struct has been removed.
187193
188194
## Simplication of the `Plugin` trait
@@ -317,7 +323,7 @@ let plugin = /* some plugin */;
317323
let layer = LayerPlugin::new::<SomeProtocol, SomeOperation>(plugin);
318324
```
319325
320-
## Remove `Operation`
326+
## Removal of `Operation`
321327
322328
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
323329
@@ -356,7 +362,7 @@ let app = PokemonService::builder_without_plugins()
356362
.unwrap();
357363
```
358364
359-
Applying a `tower::Layer` to a _single_ operation is now done through the `Plugin` API:
365+
Applying a `tower::Layer` to a _subset_ of operations is should now be done through the `Plugin` API via `filter_by_operation_id`
360366
361367
```rust
362368
use aws_smithy_http_server::plugin::{PluginLayer, filter_by_operation_name, IdentityPlugin};
@@ -371,7 +377,34 @@ let app = PokemonService::builder_with_plugins(scoped_plugin, IdentityPlugin)
371377
.unwrap();
372378
```
373379
380+
or the new `Scoped` `Plugin` introduced below.
381+
382+
# Addition of `Scoped`
383+
384+
Currently, users can selectively apply a `Plugin` via the `filter_by_operation_id` function
385+
386+
```rust
387+
use aws_smithy_http_server::plugin::filter_by_operation_id;
388+
// Only apply `plugin` to `CheckHealth` and `GetStorage` operation
389+
let filtered_plugin = filter_by_operation_id(plugin, |name| name == CheckHealth::ID || name == GetStorage::ID);
390+
```
391+
392+
In addition to this, we now provide `Scoped`, which selectively applies a `Plugin` at _compiletime_. Users should prefer this to `filter_by_operation_id` when applicable.
393+
394+
```rust
395+
use aws_smithy_http_server::plugin::Scoped;
396+
use pokemon_service_server_sdk::scoped;
397+
398+
scope! {
399+
/// Includes only the `CheckHealth` and `GetStorage` operation.
400+
struct SomeScope {
401+
includes: [CheckHealth, GetStorage]
402+
}
403+
}
404+
let scoped_plugin = Scoped::new::<SomeScope>(plugin);
405+
```
406+
374407
"""
375-
references = ["smithy-rs#2740"]
408+
references = ["smithy-rs#2740", "smithy-rs#2759"]
376409
meta = { "breaking" = true, "tada" = false, "bug" = false }
377410
author = "hlbarber"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.ConstrainedT
6363
import software.amazon.smithy.rust.codegen.server.smithy.generators.MapConstraintViolationGenerator
6464
import software.amazon.smithy.rust.codegen.server.smithy.generators.PubCrateConstrainedCollectionGenerator
6565
import software.amazon.smithy.rust.codegen.server.smithy.generators.PubCrateConstrainedMapGenerator
66+
import software.amazon.smithy.rust.codegen.server.smithy.generators.ScopeMacroGenerator
6667
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGenerator
6768
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGeneratorWithoutPublicConstrainedTypes
6869
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerEnumGenerator
@@ -610,6 +611,8 @@ open class ServerCodegenVisitor(
610611
codegenContext,
611612
serverProtocol,
612613
).render(this)
614+
615+
ScopeMacroGenerator(codegenContext).render(this)
613616
}
614617
}
615618

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.server.smithy.generators
7+
8+
import software.amazon.smithy.model.knowledge.TopDownIndex
9+
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
10+
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
11+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
12+
import software.amazon.smithy.rust.codegen.core.rustlang.writable
13+
import software.amazon.smithy.rust.codegen.core.util.toPascalCase
14+
import software.amazon.smithy.rust.codegen.core.util.toSnakeCase
15+
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
16+
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext
17+
18+
class ScopeMacroGenerator(
19+
private val codegenContext: ServerCodegenContext,
20+
) {
21+
private val runtimeConfig = codegenContext.runtimeConfig
22+
private val codegenScope =
23+
arrayOf(
24+
"SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(runtimeConfig).toType(),
25+
)
26+
27+
/** Calculate all `operationShape`s contained within the `ServiceShape`. */
28+
private val index = TopDownIndex.of(codegenContext.model)
29+
private val operations = index.getContainedOperations(codegenContext.serviceShape).toSortedSet(compareBy { it.id })
30+
31+
private fun macro(): Writable = writable {
32+
val firstOperationName = codegenContext.symbolProvider.toSymbol(operations.first()).name.toPascalCase()
33+
val operationNames = operations.joinToString(" ") {
34+
codegenContext.symbolProvider.toSymbol(it).name.toPascalCase()
35+
}
36+
37+
// When writing `macro_rules!` we add whitespace between `$` and the arguments to avoid Kotlin templating.
38+
39+
// To acheive the desired API we need to calculate the set theoretic complement `B \ A`.
40+
// The macro below, for rules prefixed with `@`, encodes a state machine which performs this.
41+
// The initial state is `(A) () (B)`, where `A` and `B` are lists of elements of `A` and `B`.
42+
// The rules, in order:
43+
// - Terminate on pattern `() (t0, t1, ...) (b0, b1, ...)`, the complement has been calculated as
44+
// `{ t0, t1, ..., b0, b1, ...}`.
45+
// - Send pattern `(x, a0, a1, ...) (t0, t1, ...) (x, b0, b1, ...)` to
46+
// `(a0, a1, ...) (t0, t1, ...) (b0, b1, ...)`, eliminating a matching `x` from `A` and `B`.
47+
// - Send pattern `(a0, a1, ...) (t0, t1, ...) ()` to `(a0, a1, ...) () (t0, t1, ...)`, restarting the search.
48+
// - Send pattern `(a0, a1, ...) (t0, t1, ...) (b0, b1, ...)` to `(a0, a1, ...) (b0, t0, t1, ...) (b1, ...)`,
49+
// iterating through the `B`.
50+
val operationBranches = operations
51+
.map { codegenContext.symbolProvider.toSymbol(it).name.toPascalCase() }.joinToString("") {
52+
"""
53+
// $it match found, pop from both `member` and `not_member`
54+
(@ $ name: ident, $ contains: ident ($it $($ member: ident)*) ($($ temp: ident)*) ($it $($ not_member: ident)*)) => {
55+
scope! { @ $ name, $ contains ($($ member)*) ($($ temp)*) ($($ not_member)*) }
56+
};
57+
// $it match not found, pop from `not_member` into `temp` stack
58+
(@ $ name: ident, $ contains: ident ($it $($ member: ident)*) ($($ temp: ident)*) ($ other: ident $($ not_member: ident)*)) => {
59+
scope! { @ $ name, $ contains ($it $($ member)*) ($ other $($ temp)*) ($($ not_member)*) }
60+
};
61+
"""
62+
}
63+
val crateName = codegenContext.moduleName.toSnakeCase()
64+
65+
// If we have a second operation we can perform further checks
66+
val otherOperationName: String? = operations.toList().getOrNull(1)?.let {
67+
codegenContext.symbolProvider.toSymbol(it).name
68+
}
69+
val furtherTests = if (otherOperationName != null) {
70+
writable {
71+
rustTemplate(
72+
"""
73+
/// ## let a = Plugin::<(), $otherOperationName, u64>::apply(&scoped_a, 6);
74+
/// ## let b = Plugin::<(), $otherOperationName, u64>::apply(&scoped_b, 6);
75+
/// ## assert_eq!(a, 6_u64);
76+
/// ## assert_eq!(b, 3_u32);
77+
""",
78+
)
79+
}
80+
} else {
81+
writable {}
82+
}
83+
84+
rustTemplate(
85+
"""
86+
/// A macro to help with scoping [plugins](#{SmithyHttpServer}::plugin) to a subset of all operations.
87+
///
88+
/// In contrast to [`aws_smithy_http_server::scope`](#{SmithyHttpServer}::scope), this macro has knowledge
89+
/// of the service and any operations _not_ specified will be placed in the opposing group.
90+
///
91+
/// ## Example
92+
///
93+
/// ```rust
94+
/// scope! {
95+
/// /// Includes [`$firstOperationName`], excluding all other operations.
96+
/// struct ScopeA {
97+
/// includes: [$firstOperationName]
98+
/// }
99+
/// }
100+
///
101+
/// scope! {
102+
/// /// Excludes [`$firstOperationName`], excluding all other operations.
103+
/// struct ScopeB {
104+
/// excludes: [$firstOperationName]
105+
/// }
106+
/// }
107+
///
108+
/// ## use #{SmithyHttpServer}::plugin::{Plugin, Scoped};
109+
/// ## use $crateName::scope;
110+
/// ## struct MockPlugin;
111+
/// ## impl<P, Op, S> Plugin<P, Op, S> for MockPlugin { type Service = u32; fn apply(&self, svc: S) -> u32 { 3 } }
112+
/// ## let scoped_a = Scoped::new::<ScopeA>(MockPlugin);
113+
/// ## let scoped_b = Scoped::new::<ScopeB>(MockPlugin);
114+
/// ## let a = Plugin::<(), $crateName::operation_shape::$firstOperationName, u64>::apply(&scoped_a, 6);
115+
/// ## let b = Plugin::<(), $crateName::operation_shape::$firstOperationName, u64>::apply(&scoped_b, 6);
116+
/// ## assert_eq!(a, 3_u32);
117+
/// ## assert_eq!(b, 6_u64);
118+
/// ```
119+
##[macro_export]
120+
macro_rules! scope {
121+
// Completed, render impls
122+
(@ $ name: ident, $ contains: ident () ($($ temp: ident)*) ($($ not_member: ident)*)) => {
123+
$(
124+
impl #{SmithyHttpServer}::plugin::scoped::Membership<$ temp> for $ name {
125+
type Contains = #{SmithyHttpServer}::plugin::scoped::$ contains;
126+
}
127+
)*
128+
$(
129+
impl #{SmithyHttpServer}::plugin::scoped::Membership<$ not_member> for $ name {
130+
type Contains = #{SmithyHttpServer}::plugin::scoped::$ contains;
131+
}
132+
)*
133+
};
134+
// All `not_member`s exhausted, move `temp` into `not_member`
135+
(@ $ name: ident, $ contains: ident ($($ member: ident)*) ($($ temp: ident)*) ()) => {
136+
scope! { @ $ name, $ contains ($($ member)*) () ($($ temp)*) }
137+
};
138+
$operationBranches
139+
(
140+
$(##[$ attrs:meta])*
141+
$ vis:vis struct $ name:ident {
142+
includes: [$($ include:ident),*]
143+
}
144+
) => {
145+
use $ crate::operation_shape::*;
146+
#{SmithyHttpServer}::scope! {
147+
$(##[$ attrs])*
148+
$ vis struct $ name {
149+
includes: [$($ include),*],
150+
excludes: []
151+
}
152+
}
153+
scope! { @ $ name, False ($($ include)*) () ($operationNames) }
154+
};
155+
(
156+
$(##[$ attrs:meta])*
157+
$ vis:vis struct $ name:ident {
158+
excludes: [$($ exclude:ident),*]
159+
}
160+
) => {
161+
use $ crate::operation_shape::*;
162+
163+
#{SmithyHttpServer}::scope! {
164+
$(##[$ attrs])*
165+
$ vis struct $ name {
166+
includes: [],
167+
excludes: [$($ exclude),*]
168+
}
169+
}
170+
scope! { @ $ name, True ($($ exclude)*) () ($operationNames) }
171+
};
172+
}
173+
""",
174+
*codegenScope,
175+
"FurtherTests" to furtherTests,
176+
)
177+
}
178+
179+
fun render(writer: RustWriter) {
180+
macro()(writer)
181+
}
182+
}

design/src/server/middleware.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,24 @@ A "HTTP layer" can be applied to specific operations.
208208
# extern crate aws_smithy_http_server;
209209
# use tower::{util::service_fn, Layer};
210210
# use std::time::Duration;
211-
# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, PokemonService, input::*, output::*, error::*};
211+
# use pokemon_service_server_sdk::{operation_shape::GetPokemonSpecies, input::*, output::*, error::*};
212212
# use aws_smithy_http_server::{operation::OperationShapeExt, plugin::*, operation::*};
213213
# let handler = |req: GetPokemonSpeciesInput| async { Result::<GetPokemonSpeciesOutput, GetPokemonSpeciesError>::Ok(todo!()) };
214214
# struct LoggingLayer;
215215
# impl LoggingLayer { pub fn new() -> Self { Self } }
216216
# impl<S> Layer<S> for LoggingLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } }
217+
use pokemon_service_server_sdk::{PokemonService, scope};
218+
219+
scope! {
220+
/// Only log on `GetPokemonSpecies` and `GetStorage`
221+
struct LoggingScope {
222+
includes: [GetPokemonSpecies, GetStorage]
223+
}
224+
}
225+
217226
// Construct `LoggingLayer`.
218227
let logging_plugin = LayerPlugin(LoggingLayer::new());
219-
let logging_plugin = filter_by_operation_id(logging_plugin, |name| name == GetPokemonSpecies::ID);
228+
let logging_plugin = Scoped::new::<LoggingScope>(logging_plugin);
220229
let http_plugins = PluginPipeline::new().push(logging_plugin);
221230
222231
let app /* : PokemonService<Route<B>> */ = PokemonService::builder_with_plugins(http_plugins, IdentityPlugin)
@@ -244,11 +253,18 @@ A "model layer" can be applied to specific operations.
244253
# struct BufferLayer;
245254
# impl BufferLayer { pub fn new(size: usize) -> Self { Self } }
246255
# impl<S> Layer<S> for BufferLayer { type Service = S; fn layer(&self, svc: S) -> Self::Service { svc } }
247-
use pokemon_service_server_sdk::PokemonService;
256+
use pokemon_service_server_sdk::{PokemonService, scope};
257+
258+
scope! {
259+
/// Only buffer on `GetPokemonSpecies` and `GetStorage`
260+
struct BufferScope {
261+
includes: [GetPokemonSpecies, GetStorage]
262+
}
263+
}
248264
249265
// Construct `BufferLayer`.
250266
let buffer_plugin = LayerPlugin(BufferLayer::new(3));
251-
let buffer_plugin = filter_by_operation_id(buffer_plugin, |name| name != GetPokemonSpecies::ID);
267+
let buffer_plugin = Scoped::new::<BufferScope>(buffer_plugin);
252268
let model_plugins = PluginPipeline::new().push(buffer_plugin);
253269
254270
let app /* : PokemonService<Route<B>> */ = PokemonService::builder_with_plugins(IdentityPlugin, model_plugins)

examples/pokemon-service/src/main.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{net::SocketAddr, sync::Arc};
1010
use aws_smithy_http_server::{
1111
extension::OperationExtensionExt,
1212
instrumentation::InstrumentExt,
13-
plugin::{alb_health_check::AlbHealthCheckLayer, IdentityPlugin, PluginPipeline},
13+
plugin::{alb_health_check::AlbHealthCheckLayer, IdentityPlugin, PluginPipeline, Scoped},
1414
request::request_id::ServerRequestIdProviderLayer,
1515
AddExtensionLayer,
1616
};
@@ -26,7 +26,7 @@ use pokemon_service_common::{
2626
capture_pokemon, check_health, get_pokemon_species, get_server_statistics, setup_tracing,
2727
stream_pokemon_radio, State,
2828
};
29-
use pokemon_service_server_sdk::PokemonService;
29+
use pokemon_service_server_sdk::{scope, PokemonService};
3030

3131
#[derive(Parser, Debug)]
3232
#[clap(author, version, about, long_about = None)]
@@ -44,9 +44,18 @@ pub async fn main() {
4444
let args = Args::parse();
4545
setup_tracing();
4646

47+
scope! {
48+
/// A scope containing `GetPokemonSpecies` and `GetStorage`
49+
struct PrintScope {
50+
includes: [GetPokemonSpecies, GetStorage]
51+
}
52+
}
53+
// Scope the `PrintPlugin`, defined in `plugin.rs`, to `PrintScope`
54+
let print_plugin = Scoped::new::<PrintScope>(PluginPipeline::new().print());
55+
4756
let plugins = PluginPipeline::new()
48-
// Apply the `PrintPlugin` defined in `plugin.rs`
49-
.print()
57+
// Apply the scoped `PrintPlugin`
58+
.push(print_plugin)
5059
// Apply the `OperationExtensionPlugin` defined in `aws_smithy_http_server::extension`. This allows other
5160
// plugins or tests to access a `aws_smithy_http_server::extension::OperationExtension` from
5261
// `Response::extensions`, or infer routing failure when it's missing.

rust-runtime/aws-smithy-http-server/src/plugin/filter.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use super::Plugin;
1313
/// Filters the application of an inner [`Plugin`] using a predicate over the
1414
/// [`OperationShape::ID`](crate::operation::OperationShape).
1515
///
16+
/// This contrasts with [`Scoped`](crate::plugin::Scoped) which can be used to selectively apply a [`Plugin`] to a
17+
/// subset of operations at _compile time_.
18+
///
1619
/// See [`filter_by_operation_id`] for more details.
1720
pub struct FilterByOperationId<Inner, F> {
1821
inner: Inner,
@@ -22,6 +25,9 @@ pub struct FilterByOperationId<Inner, F> {
2225
/// Filters the application of an inner [`Plugin`] using a predicate over the
2326
/// [`OperationShape::ID`](crate::operation::OperationShape).
2427
///
28+
/// Users should prefer [`Scoped`](crate::plugin::Scoped) and fallback to [`filter_by_operation_id`] in cases where
29+
/// [`Plugin`] application must be decided at runtime.
30+
///
2531
/// # Example
2632
///
2733
/// ```rust

rust-runtime/aws-smithy-http-server/src/plugin/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ mod filter;
111111
mod identity;
112112
mod layer;
113113
mod pipeline;
114+
#[doc(hidden)]
115+
pub mod scoped;
114116
mod stack;
115117

116118
pub use closure::{plugin_from_operation_id_fn, OperationIdFn};
@@ -119,6 +121,7 @@ pub use filter::{filter_by_operation_id, FilterByOperationId};
119121
pub use identity::IdentityPlugin;
120122
pub use layer::{LayerPlugin, PluginLayer};
121123
pub use pipeline::PluginPipeline;
124+
pub use scoped::Scoped;
122125
pub use stack::PluginStack;
123126

124127
/// A mapping from one [`Service`](tower::Service) to another. This should be viewed as a

0 commit comments

Comments
 (0)