Skip to content

Commit b2bdcba

Browse files
authored
Parameterize Plugin by service rather than protocol (#2772)
## Motivation and Context Closes #1839 Currently, `Plugin` is parameterized by protocol and operation. To improve symmetry, extensibility and uniformity we switch this to be parameterized by service instead. The protocol can still be recovered via the `type Protocol` associated type on `ServiceShape`. ## Description - Add `ServiceShape` trait, encoding the properties of a Smithy service. - Change `Plugin<Protocol, Operation, S>` to `Plugin<Service, Operation, S>`. - Add `FilterByOperation` and `filter_by_operation` `Plugin`s.
1 parent 3d0db56 commit b2bdcba

File tree

22 files changed

+593
-276
lines changed

22 files changed

+593
-276
lines changed

CHANGELOG.next.toml

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,64 @@ meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
182182
author = "ysaito1001"
183183

184184
[[smithy-rs]]
185-
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:
185+
message = """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:
187186
187+
- A `ServiceShape` trait has been added.
188188
- The `Plugin` trait has been simplified.
189189
- The `Operation` structure has been removed.
190190
- A `Scoped` `Plugin` has been added.
191191
192192
The `Plugin` trait has now been simplified and the `Operation` struct has been removed.
193193
194+
## Addition of `ServiceShape`
195+
196+
Since the [0.52 release](https://github.com/awslabs/smithy-rs/releases/tag/release-2022-12-12) the `OperationShape` has existed.
197+
198+
```rust
199+
/// Models the [Smithy Operation shape].
200+
///
201+
/// [Smithy Operation shape]: https://awslabs.github.io/smithy/1.0/spec/core/model.html#operation
202+
pub trait OperationShape {
203+
/// The ID of the operation.
204+
const ID: ShapeId;
205+
206+
/// The operation input.
207+
type Input;
208+
/// The operation output.
209+
type Output;
210+
/// The operation error. [`Infallible`](std::convert::Infallible) in the case where no error
211+
/// exists.
212+
type Error;
213+
}
214+
```
215+
216+
This allowed `Plugin` authors to access these associated types and constants. See the [`PrintPlugin`](https://github.com/awslabs/smithy-rs/blob/main/examples/pokemon-service/src/plugin.rs) as an example.
217+
218+
We continue with this approach and introduce the following trait:
219+
220+
```rust
221+
/// Models the [Smithy Service shape].
222+
///
223+
/// [Smithy Service shape]: https://smithy.io/2.0/spec/service-types.html
224+
pub trait ServiceShape {
225+
/// The [`ShapeId`] of the service.
226+
const ID: ShapeId;
227+
228+
/// The version of the service.
229+
const VERSION: Option<&'static str>;
230+
231+
/// The [Protocol] applied to this service.
232+
///
233+
/// [Protocol]: https://smithy.io/2.0/spec/protocol-traits.html
234+
type Protocol;
235+
236+
/// An enumeration of all operations contained in this service.
237+
type Operations;
238+
}
239+
```
240+
241+
With the changes to `Plugin`, described below, middleware authors now have access to this information at compile time.
242+
194243
## Simplication of the `Plugin` trait
195244
196245
Previously,
@@ -209,14 +258,16 @@ modified an `Operation`.
209258
Now,
210259
211260
```rust
212-
trait Plugin<Protocol, Operation, S> {
213-
type Service;
261+
trait Plugin<Service, Operation, T> {
262+
type Output;
214263
215-
fn apply(&self, svc: S) -> Self::Service;
264+
fn apply(&self, input: T) -> Self::Output;
216265
}
217266
```
218267
219-
maps a `tower::Service` to a `tower::Service`. This is equivalent to `tower::Layer` with two extra type parameters: `Protocol` and `Operation`.
268+
maps a `tower::Service` to a `tower::Service`. This is equivalent to `tower::Layer` with two extra type parameters: `Service` and `Operation`, which implement `ServiceShape` and `OperationShape` respectively.
269+
270+
Having both `Service` and `Operation` as type parameters also provides an even surface for advanced users to extend the codegenerator in a structured way. See [this issue](https://github.com/awslabs/smithy-rs/issues/2777) for more context.
220271
221272
The following middleware setup
222273
@@ -286,18 +337,33 @@ where
286337
287338
pub struct PrintPlugin;
288339
289-
impl<P, Op, S, L> Plugin<P, Op, S, L> for PrintPlugin
340+
impl<Service, Op, T> Plugin<Service, Operation, T> for PrintPlugin
290341
where
291342
Op: OperationShape,
292343
{
293-
type Service = PrintService<S>;
344+
type Output = PrintService<S>;
294345
295-
fn apply(&self, svc: S) -> Self::Service {
346+
fn apply(&self, inner: T) -> Self::Output {
296347
PrintService { inner, name: Op::ID.name() }
297348
}
298349
}
299350
```
300351
352+
Alternatively, using the new `ServiceShape`, implemented on `Ser`:
353+
354+
```rust
355+
impl<Service, Operation, T> Plugin<Service, Operation, T> for PrintPlugin
356+
where
357+
Ser: ServiceShape,
358+
{
359+
type Service = PrintService<S>;
360+
361+
fn apply(&self, inner: T) -> Self::Service {
362+
PrintService { inner, name: Ser::ID.name() }
363+
}
364+
}
365+
```
366+
301367
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:
302368
303369
```rust
@@ -442,3 +508,45 @@ message = "Add a `send_with` function on `-Input` types for sending requests wit
442508
author = "thomas-k-cameron"
443509
references = ["smithy-rs#2652"]
444510
meta = { "breaking" = false, "tada" = true, "bug" = false }
511+
512+
[[smithy-rs]]
513+
message = """Remove `filter_by_operation_id` and `plugin_from_operation_id_fn` in favour of `filter_by_operation` and `plugin_from_operation_fn`.
514+
515+
Previously, we provided `filter_by_operation_id` which filtered `Plugin` application via a predicate over the Shape ID.
516+
517+
```rust
518+
use aws_smithy_http_server::plugin::filter_by_operation_id;
519+
use pokemon_service_server_sdk::operation_shape::CheckHealth;
520+
521+
let filtered = filter_by_operation_id(plugin, |name| name != CheckHealth::NAME);
522+
```
523+
524+
This had the problem that the user is unable to exhaustively match over a `&'static str`. To remedy this we have switched to `filter_by_operation` which is a predicate over an enum containing all operations contained in the service.
525+
526+
```rust
527+
use aws_smithy_http_server::plugin::filter_by_operation_id;
528+
use pokemon_service_server_sdk::service::Operation;
529+
530+
let filtered = filter_by_operation(plugin, |op: Operation| op != Operation::CheckHealth);
531+
```
532+
533+
Similarly, `plugin_from_operation_fn` now allows for
534+
535+
```rust
536+
use aws_smithy_http_server::plugin::plugin_from_operation_fn;
537+
use pokemon_service_server_sdk::service::Operation;
538+
539+
fn map<S>(op: Operation, inner: S) -> PrintService<S> {
540+
match op {
541+
Operation::CheckHealth => PrintService { name: op.shape_id().name(), inner },
542+
Operation::GetPokemonSpecies => PrintService { name: "hello world", inner },
543+
_ => todo!()
544+
}
545+
}
546+
547+
let plugin = plugin_from_operation_fn(map);
548+
```
549+
"""
550+
references = ["smithy-rs#2740", "smithy-rs#2759", "smithy-rs#2779"]
551+
meta = { "breaking" = true, "tada" = false, "bug" = false }
552+
author = "hlbarber"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class ScopeMacroGenerator(
108108
/// ## use #{SmithyHttpServer}::plugin::{Plugin, Scoped};
109109
/// ## use $crateName::scope;
110110
/// ## struct MockPlugin;
111-
/// ## impl<P, Op, S> Plugin<P, Op, S> for MockPlugin { type Service = u32; fn apply(&self, svc: S) -> u32 { 3 } }
111+
/// ## impl<S, Op, T> Plugin<S, Op, T> for MockPlugin { type Output = u32; fn apply(&self, input: T) -> u32 { 3 } }
112112
/// ## let scoped_a = Scoped::new::<ScopeA>(MockPlugin);
113113
/// ## let scoped_b = Scoped::new::<ScopeB>(MockPlugin);
114114
/// ## let a = Plugin::<(), $crateName::operation_shape::$firstOperationName, u64>::apply(&scoped_a, 6);

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

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ class ServerServiceGenerator(
5050
private val crateName = codegenContext.moduleUseName()
5151

5252
private val service = codegenContext.serviceShape
53-
private val serviceName = service.id.name.toPascalCase()
53+
private val serviceId = service.id
54+
private val serviceName = serviceId.name.toPascalCase()
5455
private val builderName = "${serviceName}Builder"
5556
private val builderBodyGenericTypeName = "Body"
5657

@@ -136,30 +137,30 @@ class ServerServiceGenerator(
136137
HandlerType: #{SmithyHttpServer}::operation::Handler<crate::operation_shape::$structName, HandlerExtractors>,
137138
138139
ModelPlugin: #{SmithyHttpServer}::plugin::Plugin<
139-
#{Protocol},
140+
$serviceName,
140141
crate::operation_shape::$structName,
141142
#{SmithyHttpServer}::operation::IntoService<crate::operation_shape::$structName, HandlerType>
142143
>,
143144
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>: #{SmithyHttpServer}::plugin::Plugin<
144-
#{Protocol},
145+
$serviceName,
145146
crate::operation_shape::$structName,
146-
ModelPlugin::Service
147+
ModelPlugin::Output
147148
>,
148149
HttpPlugin: #{SmithyHttpServer}::plugin::Plugin<
149-
#{Protocol},
150+
$serviceName,
150151
crate::operation_shape::$structName,
151152
<
152153
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>
153154
as #{SmithyHttpServer}::plugin::Plugin<
154-
#{Protocol},
155+
$serviceName,
155156
crate::operation_shape::$structName,
156-
ModelPlugin::Service
157+
ModelPlugin::Output
157158
>
158-
>::Service
159+
>::Output
159160
>,
160161
161-
HttpPlugin::Service: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
162-
<HttpPlugin::Service as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
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,
163164
164165
{
165166
use #{SmithyHttpServer}::operation::OperationShapeExt;
@@ -199,30 +200,30 @@ class ServerServiceGenerator(
199200
S: #{SmithyHttpServer}::operation::OperationService<crate::operation_shape::$structName, ServiceExtractors>,
200201
201202
ModelPlugin: #{SmithyHttpServer}::plugin::Plugin<
202-
#{Protocol},
203+
$serviceName,
203204
crate::operation_shape::$structName,
204205
#{SmithyHttpServer}::operation::Normalize<crate::operation_shape::$structName, S>
205206
>,
206207
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>: #{SmithyHttpServer}::plugin::Plugin<
207-
#{Protocol},
208+
$serviceName,
208209
crate::operation_shape::$structName,
209-
ModelPlugin::Service
210+
ModelPlugin::Output
210211
>,
211212
HttpPlugin: #{SmithyHttpServer}::plugin::Plugin<
212-
#{Protocol},
213+
$serviceName,
213214
crate::operation_shape::$structName,
214215
<
215216
#{SmithyHttpServer}::operation::UpgradePlugin::<UpgradeExtractors>
216217
as #{SmithyHttpServer}::plugin::Plugin<
217-
#{Protocol},
218+
$serviceName,
218219
crate::operation_shape::$structName,
219-
ModelPlugin::Service
220+
ModelPlugin::Output
220221
>
221-
>::Service
222+
>::Output
222223
>,
223224
224-
HttpPlugin::Service: #{Tower}::Service<#{Http}::Request<Body>, Response = #{Http}::Response<#{SmithyHttpServer}::body::BoxBody>, Error = ::std::convert::Infallible> + Clone + Send + 'static,
225-
<HttpPlugin::Service as #{Tower}::Service<#{Http}::Request<Body>>>::Future: Send + 'static,
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,
226227
227228
{
228229
use #{SmithyHttpServer}::operation::OperationShapeExt;
@@ -353,7 +354,6 @@ class ServerServiceGenerator(
353354
for (operationShape in operations) {
354355
val fieldName = builderFieldNames[operationShape]!!
355356
val (specBuilderFunctionName, _) = requestSpecMap.getValue(operationShape)
356-
val operationZstTypeName = operationStructNames[operationShape]!!
357357
rustTemplate(
358358
"""
359359
(
@@ -590,6 +590,75 @@ class ServerServiceGenerator(
590590
)
591591
}
592592

593+
private fun serviceShapeImpl(): Writable = writable {
594+
val namespace = serviceId.namespace
595+
val name = serviceId.name
596+
val absolute = serviceId.toString().replace("#", "##")
597+
val version = codegenContext.serviceShape.version?.let { "Some(\"$it\")" } ?: "None"
598+
rustTemplate(
599+
"""
600+
impl #{SmithyHttpServer}::service::ServiceShape for $serviceName {
601+
const ID: #{SmithyHttpServer}::shape_id::ShapeId = #{SmithyHttpServer}::shape_id::ShapeId::new("$absolute", "$namespace", "$name");
602+
603+
const VERSION: Option<&'static str> = $version;
604+
605+
type Protocol = #{Protocol};
606+
607+
type Operations = Operation;
608+
}
609+
""",
610+
"Protocol" to protocol.markerStruct(),
611+
*codegenScope,
612+
)
613+
}
614+
615+
private fun operationEnum(): Writable = writable {
616+
val operations = operationStructNames.values.joinToString(",")
617+
val matchArms: Writable = operationStructNames.map {
618+
(shape, name) ->
619+
writable {
620+
val absolute = shape.id.toString().replace("#", "##")
621+
rustTemplate(
622+
"""
623+
Operation::$name => #{SmithyHttpServer}::shape_id::ShapeId::new("$absolute", "${shape.id.namespace}", "${shape.id.name}")
624+
""",
625+
*codegenScope,
626+
)
627+
}
628+
}.join(",")
629+
rustTemplate(
630+
"""
631+
/// An enumeration of all [operations](https://smithy.io/2.0/spec/service-types.html##operation) in $serviceName.
632+
##[derive(Debug, PartialEq, Eq, Clone, Copy)]
633+
pub enum Operation {
634+
$operations
635+
}
636+
637+
impl Operation {
638+
/// Returns the [operations](https://smithy.io/2.0/spec/service-types.html##operation) [`ShapeId`](#{SmithyHttpServer}::shape_id::ShapeId).
639+
pub fn shape_id(&self) -> #{SmithyHttpServer}::shape_id::ShapeId {
640+
match self {
641+
#{Arms}
642+
}
643+
}
644+
}
645+
""",
646+
*codegenScope,
647+
"Arms" to matchArms,
648+
)
649+
650+
for ((_, value) in operationStructNames) {
651+
rustTemplate(
652+
"""
653+
impl #{SmithyHttpServer}::service::ContainsOperation<crate::operation_shape::$value> for $serviceName {
654+
const VALUE: Operation = Operation::$value;
655+
}
656+
""",
657+
*codegenScope,
658+
)
659+
}
660+
}
661+
593662
fun render(writer: RustWriter) {
594663
writer.rustTemplate(
595664
"""
@@ -600,11 +669,17 @@ class ServerServiceGenerator(
600669
#{RequestSpecs:W}
601670
602671
#{Struct:W}
672+
673+
#{Operations}
674+
675+
#{ServiceImpl}
603676
""",
604677
"Builder" to builder(),
605678
"MissingOperationsError" to missingOperationsError(),
606679
"RequestSpecs" to requestSpecsModule(),
607680
"Struct" to serviceStruct(),
681+
"Operations" to operationEnum(),
682+
"ServiceImpl" to serviceShapeImpl(),
608683
*codegenScope,
609684
)
610685
}

0 commit comments

Comments
 (0)