-
Notifications
You must be signed in to change notification settings - Fork 244
Add design for inline operation input/output shapes #962
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| # Inline Operation Inputs / Outputs | ||
|
|
||
| This document describes a way to write input and output shapes as an inline | ||
| part of an operation’s definition. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Operation input and output shapes are always structures, almost always have | ||
| boilerplate names, and critically are almost never re-used. In some cases, they | ||
| may not even have types generated for them. Because of those properties, the | ||
| need to fully define them separately from an operation can feel like needless | ||
| boilerplate. Additionally, separating them makes reading an operation at a high | ||
| level more difficult since you have to jump around to get the information you | ||
| need. | ||
|
|
||
| ## Proposal | ||
|
|
||
| Operations will allow inlining input and output definitions, indicated by a | ||
| walrus operator (`:=`). Inlined structures will deviate from normal | ||
| structure definitions in two respects. Firstly, the structure keyword will be | ||
| omitted. Additionally, the names of the structures will be generated. | ||
|
|
||
| ### Walrus Operator | ||
|
|
||
| Rather than using the same standalone colon (`:`) that is used in other | ||
| cases, a walrus operator will be used to indicate an inline definition. The | ||
| reason for this is to visually distinguish it at the outset, as well as to make | ||
| parsers simpler because the walrus operator removes the need for arbitrary | ||
| lookahead. | ||
|
|
||
| This usage of the operator is analogous to how some programming languages use | ||
| it. For instance, Python uses it to assign a name to the result of an | ||
| expression in places where initializing a variable was previously forbidden. | ||
| In Go, it’s used to initialize and assign a variable in one step. | ||
|
|
||
| ### Omitting the structure Keyword | ||
|
|
||
| When used at the top level of a Smithy IDL file, the type keywords are | ||
| necessary to indicate what type of shape you’re making. Since operation | ||
| inputs and outputs may only be structures, this isn’t necessary. | ||
|
|
||
| ### Generated Names | ||
|
|
||
| Inlined structures will have names generated for them if they aren’t | ||
| provided. For inputs, the generated name will be the name of the operation with | ||
| an `Input` suffix. For outputs, the default name will be the name of the | ||
| operation with an `Output` suffix. | ||
|
|
||
| The reason for generating a name is that there’s rarely a better name | ||
| than what is trivially generated. Therefore, requiring users to write it out | ||
| is effectively pointless boilerplate. In AWS models, for instance, over 98% of | ||
| operation input/output structures are named by suffixing the operation name. | ||
|
|
||
| #### Custom Suffixes | ||
|
|
||
| A service team that wants to migrate to using inlined structures may have | ||
| already been using a different set of suffixes, such as `Request` and | ||
| `Response`. To remain consistent, they can use control statements to customize | ||
| their suffixes on a per-file basis. | ||
|
|
||
| `operationInputSuffix` controls the suffix for the input, and | ||
| `operationOutputSuffix` controls the suffix for the output. | ||
|
|
||
| Service teams that use these customizations SHOULD write linters to ensure that | ||
| all of the operations in a given service conform to their expected naming | ||
| convention. | ||
|
|
||
| ### Examples | ||
|
|
||
| ``` | ||
| operation GetUser { | ||
| input := { | ||
| userId: String | ||
| } | ||
|
|
||
| output := { | ||
| username: String | ||
| userId: String | ||
| } | ||
| } | ||
|
|
||
| // Inlined inputs/outputs with traits. Only 20% of current operation | ||
| // inputs/outputs in AWS services use any traits, which mostly consists of | ||
| // documentation. | ||
| operation GetUser { | ||
| input := | ||
| /// Documentation is currently the most popular trait on IO shapes. That | ||
| /// said, there isn't much point to adding docs to an IO shape since the | ||
| /// operation docs will take over that role. | ||
| @sensitive | ||
| @references([{resource: User}]) { | ||
| userId: String | ||
| } | ||
|
|
||
| // If there's only one trait and it's short, this compact form can be used. | ||
| // The references trait is the most likely trait to be used in the future, | ||
| // and in most cases it will be able to use this compact form. | ||
| output := @references([{resource: User}]) { | ||
| username: String | ||
| userId: String | ||
| } | ||
| } | ||
|
|
||
| // Inlined inputs/outputs with mixins. | ||
| operation GetUser { | ||
| input := with BaseUser {} | ||
|
|
||
| output := with BaseUser { | ||
| username: String | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### ABNF | ||
|
|
||
| ``` | ||
| operation_statement = | ||
| "operation" ws identifier ws inlineable_properties | ||
|
|
||
| inlineable_properties = | ||
| "{" *(inlineable_property ws) ws "}" | ||
|
|
||
| inlineable_property = | ||
| node_object_kvp / inline_structure | ||
|
|
||
| inline_structure = | ||
| node_object_key ws ":=" ws inline_structure_value | ||
|
|
||
| inline_structure_value = | ||
| trait_statements [mixins ws] shape_members | ||
| ``` | ||
|
|
||
| The following demonstrate customizing the suffixes. | ||
|
|
||
| ``` | ||
| $version: "2.0" | ||
| $operationInputSuffix: "Request" | ||
| $operationOutputSuffix: "Response" | ||
|
|
||
| namespace com.example | ||
|
|
||
| operation MyOperation { | ||
| // Generated name is: MyOperationRequest | ||
| input := {} | ||
|
|
||
| // Generated name is: MyOperationResponse | ||
| output := {} | ||
| } | ||
| ``` | ||
|
|
||
| ## FAQ | ||
|
|
||
| ### Can apply be used on inlined inputs/outputs? | ||
|
|
||
| Yes. This is only syntactic sugar, the shapes produced are normal shapes in | ||
| every way. | ||
|
|
||
| ### Can inlined shapes be used anywhere else? | ||
|
|
||
| No. There aren't many other places where inline shapes would make sense. Errors, | ||
| for instance, can’t use generated names and are frequently referenced elsewhere. | ||
|
|
||
| Consider the following simplified model that uses theoretical inlined, nested | ||
| structure definition: | ||
|
|
||
| ``` | ||
| structure Foo { | ||
| bar := { | ||
| id: String | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| On day one, the `bar` structure is only referenced in one place, so perhaps | ||
| there’s a desire to inline it. On day two, another structure is introduced that | ||
| references it. | ||
|
|
||
| ``` | ||
| structure Foo { | ||
| bar := { | ||
| id: String | ||
| } | ||
| } | ||
|
|
||
| structure Baz { | ||
| bar: bar | ||
| } | ||
| ``` | ||
|
|
||
| Now the fact that it's inlined has become a detriment, because it's hard to go | ||
| from looking at the definition of `Baz` to finding the definition of `bar`. This | ||
| problem gets worse and worse the larger the model gets and the more the nested | ||
| structure is referenced. This isn't so much a problem for operations, because | ||
| their IO shapes are almost never refrenced elsewhere and even if they were the | ||
| default name makes it pretty clear where to look. | ||
|
|
||
| There is at least one other place where this may make sense: resource | ||
| identifiers. When defining a resource, you could use this syntax to define a | ||
| structure that contains only the resource identifiers. This could be mixed in | ||
| to other shapes in the model. This usage, while interesting, is out of scope | ||
| for this document. | ||
|
|
||
| ### Why can't explicit names be optionally provided? | ||
|
|
||
| Allowing optional names would complicate the parser by requiring additional | ||
| lookahead to disambiguate between a structure named with and the use of a | ||
| with statement. With the ability to customize the generated suffix, the | ||
| only reason to provide an overridden name is in the rare case where the | ||
| structure isn't already using the operation name as a prefix. Since fewer | ||
| than 2% of all AWS services deviate from this pattern, it's an acceptable | ||
| tradeoff to require those cases to separately define their shapes. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.