Skip to content
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

Add design for inline operation input/output shapes #962

Merged
merged 4 commits into from
Nov 5, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions designs/inline-io.md
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 := {
JordonPhillips marked this conversation as resolved.
Show resolved Hide resolved
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.