-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better distinguish model and HTTP plugins #2827
Better distinguish model and HTTP plugins #2827
Conversation
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.
@@ -23,6 +23,12 @@ where | |||
} | |||
} | |||
|
|||
// Without more information about what the layer `L` does, we can't know whether it's appropriate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not too happy about this. With PluginLayer
and LayerPlugin
, a user can effectively circumvent the whole point of this PR by converting an e.g. model plugin to a layer and using LayerPlugin
to get a HTTP plugin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch.
To prevent confusion, we could remove the constructor here (pub L)
-> (L)
and instead require that the user construct LayerPlugin
directly through HttpPlugins::layer
and ModelPlugins::layer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems a bit heavy-handed to force them to use a collection of plugins to make use of a single plugin that they construct via a layer.
I think it's ok we go ahead with this. I can't think of valid reasons for why someone might want to do a plugin -> layer -> plugin conversion.
A new generated diff is ready to view.
A new doc preview is ready to view. |
@@ -23,6 +23,12 @@ where | |||
} | |||
} | |||
|
|||
// Without more information about what the layer `L` does, we can't know whether it's appropriate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch.
To prevent confusion, we could remove the constructor here (pub L)
-> (L)
and instead require that the user construct LayerPlugin
directly through HttpPlugins::layer
and ModelPlugins::layer
.
@@ -188,3 +244,222 @@ where | |||
<Pl as Plugin<Ser, Op, T>>::apply(self, inner) | |||
} | |||
} | |||
|
|||
/// A HTTP plugin is a plugin that acts on the HTTP request before it is deserialized, and acts on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A HTTP plugin is a plugin
I think this is misleading and why perhaps we should call it HttpMarker
or something. This description would make sense if it was a super trait of Plugin
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it misleading?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that this conveys the notion that end-users should think of plugins as coming in two distinct flavors. It's a more useful mental model for them to have than to think of a plugin as how it's actually implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it misleading?
If I knew of Plugin
and then I wrote a function signature
fn foo<S>(x: impl HttpPlugin, svc: S)
I might except to be able call x.apply
(from Plugin::apply
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Ok, will change.
CHANGELOG.next.toml
Outdated
@@ -586,3 +586,9 @@ message = "The naming `make_token` for fields and the API of `IdempotencyTokenPr | |||
references = ["smithy-rs#2783"] | |||
meta = { "breaking" = true, "tada" = false, "bug" = false } | |||
author = "ysaito1001" | |||
|
|||
[[smithy-rs]] | |||
message = "Server `Plugin`s now need to be marked as a `HttpPlugin`, a `ModelPlugin`, or both. Check out the `plugin` module documentation for more information." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've got a huge changelog entry about the type refactors above - perhaps it'd be nice to squash this one into that and uphold its narrative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a58ec48.
A new generated diff is ready to view.
A new doc preview is ready to view. |
A new generated diff is ready to view.
A new doc preview is ready to view. |
So far, servers have tacitly worked with the notion that plugins come in
two flavors: "HTTP plugins" and "model plugins":
acts on the HTTP response after it is serialized.
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:
would get the typical Tower service compilation errors, which can get
very confusing:
This commit introduces the
HttpPlugin
andModelPlugin
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 ofHttpPlugins
andModelPlugins
, whicheagerly check that whenever a plugin is
push
ed, it is of the expectedtype. The generated service type in the server SDKs'
builder_with_plugins
constructor now takes anHttpPlugin
as itsfirst 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
CHANGELOG.next.toml
if I made changes to the smithy-rs codegen or runtime cratesBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.