Skip to content

Commit

Permalink
Add Unit type and @input and @output traits
Browse files Browse the repository at this point in the history
This commit introduces the `smithy.api#Unit` type that that is used for
operations with no meaningful input or output, and for tagged union
members with no meaningful value. Operations will now, by default,
target `smithy.api#Union` if they do not define input or output.

This commit also introduces the `@input` and `@output` traits that are
used to specialize structures as for use only as the input or output of
a single operation. The `@input` trait provides structures with more
relaxed backward compatibility constraints so that, for example, when
things like the `@default` value trait are added, it is considered a
backward compatible change to remove the `@required` trait from the
member of a structure marked with the `@input` trait.

New methods were added to smithy-model to account for the fact that
operations always now have input and output shapes. The existing methods
that return Optional shape IDs and structures remain but are marked as
deprecated. Any usage of these deprecated methods were updated
throughout the monorepo to use the newly introduced methods (this
accounts for a lot of the churn).

If an operation defines no input or output, a WARNING is emitted. If an
operation defines input or output other than `smithy.api#Unit` and the
targeted shapes aren't marked with `@input` or `@output` a validation
event is emitted. This also accounts for a lot of churn in test cases
(either to add suppressions or to add input and output). Due to the
suppressed events, Smithy's errorfiles test runner was updated to no
longer require that SUPPRESSED validation events are explicitly listed
in errorfiles (they can be but don't have to be).

Along with updating every operation to define input and output shapes,
this change also removes many JSON AST examples. These examples were
unnecessary and a liability for the team to maintain. The appropriate
place to document how the IDL and JSON AST interact is in the
specification language around the IDL and JSON AST, not spread across
the entire specification. Alternatively, I could have edited each
impacted AST example, but I worried that I would add mistakes and they
are not validated against the IDL examples.
  • Loading branch information
mtdowling committed Nov 16, 2021
1 parent 4fd38fc commit e771467
Show file tree
Hide file tree
Showing 290 changed files with 4,554 additions and 3,637 deletions.
378 changes: 378 additions & 0 deletions designs/operation-input-output-and-unit-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
# Operation input, output, and unit types

* **Author**: Michael Dowling
* **Created**: 2021-11-11

## Abstract

This proposal introduces new traits and shapes that makes operation inputs
and outputs more explicit resulting in a simplified semantic model, simplified
code generation, the ability for operations to opt-in to more flexible
backward compatibility semantics, and more expressiveness for tagged unions.
This is achieved through introducing the `@input` and `@output` traits to
specialize a structure as the input or output of a single operation, and
introducing a built-in [unit](https://en.wikipedia.org/wiki/Unit_type) shape
to explicitly indicate that an operation has no meaningful input or output and
that a tagged union member has no meaningful value.

## Motivation

### Best practices for defining operations are too easy to miss

Operations in Smithy prior to this proposal can have no input, no output,
or target any structure. A longstanding best-practice for Smithy models is
to always define a dedicated input and output structure for every operation,
even if an operation at its inception has no meaningful input or output.
This allows input members and output members to be added over time if ever
needed, which is important for services that plan to stay in production for
decades.

Reusing input and output shapes for multiple operations can hinder how a
service can evolve over time. For example, if operations diverge over time
but share the same input structure, the operations might have input members
that have to resort to documentation to caution users that a member is only
used when calling certain operations but not others.

### Operation backward compatibility semantics push too much complexity onto code generators

It is now considered backward compatible for an operation to change from no
input or output to defining input and output, and this has resulted in
pushing complexity onto Smithy code generators. Smithy code generators today
often generate dedicated "synthetic" input and output shapes for each
operation to account for this kind of model evolution. Generators
essentially make a copy of shapes used as input and output, and in the case
that a shape isn't defined for an operation, they generate one. This allows
the generated code to add context-specific functionality to shapes used as
operation input or output, like methods for customizing middleware, adding
HTTP headers, etc.

The reason for this backward compatibility affordance is largely because
service teams writing models are unaware that defining input and output shapes
for all operations is a best-practice. Its consequence is that each Smithy code
generation project needs to internalize all the nuance of synthetic input and
output shapes independently and figure out how to account for it in their
generators.

## Proposal

This proposal makes the following changes that impact both Smithy 1.0 and
Smithy 2.0:

1. The input and output of operations defaults to `smithy.api#Unit`, a shape
in the Smithy prelude that represents the lack of a meaningful value.
2. A `Unit` shape will be added to the Smithy prelude. This shape is used when
an operation truly has no input or output, and for tagged union members with
no meaningful value.
3. `@input` and `@output` traits will be added that specialize a structure as
the input or output of a single operation. Shapes marked with the `@input`
and `@output` traits can only be referenced by a single operation, and
cannot act as both input and output.
4. A WARNING will be emitted for operations that target shapes that are not
marked with the `@input` and `@ouput` traits.
5. The `@input` and `@output` traits will automatically be applied to input and
output shapes that are defined with the inline operation input and output
syntax being introduced in Smithy IDL 2.0.

### `@input` and `@output` traits

`@input` and `@output` traits specialize a structure as allowed for use only
as top-level operation input and output. Shapes marked with these traits
cannot be targeted by members or used in any other way than to serve as the
input or output of a single operation.

The following example is a valid use of `@input` and `@output` traits:

```
operation GetFoo {
input: GetFooInput,
output: GetFooOutput
}
@input
structure GetFooInput {}
@output
structure GetFooOutput {}
```

The following structure is invalid because a member targets a shape marked
with the `@input` trait:

```
structure Hello {
hi: GetFooInput // <- ERROR
}
```

The `@input` and `@output` traits make code generation easier because
generators can use the defined shape as-is without needing to generate a
synthetic shape. These traits also encourage modelers to define operations
that utilize known best practices, ensuring operations can easily evolve over
time.

#### @input and @output trait definitions

The `@input` and `@output` traits are defined in the prelude as:

```
/// Specializes a structure for use only as the input of a single operation.
@trait(selector: "structure", conflicts: [output, error])
@tags(["diff.error.const"])
structure input {}
/// Specializes a structure for use only as the output of a single operation.
@trait(selector: "structure", conflicts: [input, error])
@tags(["diff.error.const"])
structure output {}
```

#### Relaxed backward compatibility semantics

Structures marked with the `@input` trait will have more relaxed backward
compatibility semantics in that the `@required` trait can be freely removed
from their members. Other proposals are being explored in Smithy that allow
code generators to use traits like `@required` and `@default` to generate
more idiomatic code without sacrificing too much of a service's ability to
evolve their models over time. Code generators SHOULD take these relaxed
backward compatibility semantics into account when generating code for
structures marked with the `@input` trait to avoid breaking previously
generated clients as models evolve.

#### Automatic application using inline syntax

To encourage their use and remove the burden of needing to apply the `@input`
and `@output` traits to structures, the `@input` and `@output` traits will be
added automatically when using the inline operation input and output syntax
introduced in Smithy 2.0. For example, the following model,

```
$version: "2"
namespace smithy.example
operation GetFoo {
input := {}
output := {}
}
```

Will be equivalent to the following:

```
$version: "2"
namespace smithy.example
operation GetFoo {
input: GetFooInput
output: GetFooOutput
}
@input
structure GetFooInput {}
@output
structure GetFooOutput {}
```

#### Additional built-in validation

To encourage models to utilize the `@input` and `@output` traits, built-in
validation will be added to Smithy.

When an operation does not define input or output explicitly a WARNING
validation event will be emitted with the ID `OperationMissingInput` or
`OperationMissingOutput`. This can be avoided by explicitly assigning the input
or output of an operation to a shape, including `smithy.api#Unit`.

If an operation input targets a shape other than `smithy.api#Unit` that is
not marked with the `@input` trait, a WARNING validation event is emitted with
the ID `OperationMissingInputTrait`. A similar event with an ID of
`OperationMissingOutputTrait` is emitted when an operation output targets a
shape that is not `smithy.api#Unit` and is not marked with the `@output` trait.

```
operation GetFoo {
input: GetFooInput, // <- OperationMissingInputTrait WARNING
output: GetFooOutput // <- OperationMissingOutputTrait WARNING
}
structure GetFooInput {}
structure GetFooOutput {}
```

The name of a shape targeted by the `@input` or `@output` trait SHOULD start
with the name of the operation that references it (if any). If not, then a
WARNING is emitted with an ID `OperationInputOutputName`.

For example, the following model would emit an `OperationInputOutputName`
WARNING because the input shape of the operation does not start with the name
of the operation:

```
operation GetFoo {
input: GetFooInput,
output: Foo
}
@input
structure GetFooInput {}
@output
structure Foo {} // <- this should be named GetFooOutput
```

### The `smithy.api#Unit` shape

A [unit](https://en.wikipedia.org/wiki/Unit_type) shape will be added to the
Smithy prelude to represent a shape that has no meaningful value. There will
be a single `Unit` shape in Smithy, modeled as a structure with the internal
`@unitType` trait attached to differentiate it from other structures. The
`@internal` trait on the `@unitType` trait ensures that no other unit
structures can be defined.

The `Unit` shape and `unitType` trait are modeled in the prelude as:

```
namespace smithy.api
/// The single unit type shape, similar to Void and None in other
/// languages, used to represent no meaningful value.
@unitType
structure Unit {}
/// Specializes a structure as a unit type that has no meaningful value.
/// This trait is private, which ensures that only a single Unit shape
/// can be created, smithy.api#Unit.
@trait(selector: "[id=smithy.api#Unit]")
@internal
structure unitType {}
```

`smithy.api#Unit` can only be targeted as the input of an operation, output
of an operation, or from a member of a tagged union.

* Operations that do not define input or output will target `smithy.api#Unit`
by default, and it is no longer considered a backward compatible change to
change the shape targeted as the input or output of an operation.
* The unit type is also useful for unions. In some cases, the tag of a tagged
union is the only meaningful value that needs to be communicated. In
these cases, the union member can target the `Unit` type to indicate that the
associated value has no meaning.

While `smithy.api#Unit` is modeled as a specialized structure, code generators
and protocols MAY special-case how a `Unit` is represented. For example, the
following Smithy model:

```
union ItemAction {
delete: Unit,
replaceWith: Item
}
```

Might be generated in Rust as:

```
enum Message {
Delete,
ReplaceWith(Item)
}
```

### Other considerations

#### Add a Smithy transform to generate synthetic input and output shapes

Because `@input` and `@output` traits are optional, some code generators that
want to define dedicated input and output shapes for every operation will still
want to generate synthetic input and output shapes. To make this easier,
an opt-in model transform will be added to smithy-build to reduce complexity in
Smithy code generators, and centralize the complexity in a single location.

#### AWS model migration from 1.0 to 2.0

Most models created for AWS use dedicated input and output shapes for every
operation. These input and output shapes will be updated to apply the `@input`
and `@output` traits. AWS models that do not define dedicated input and output
shapes (for example, Amazon API Gateway) will be updated to use generated
synthetic input and output shapes marked with the `@input` and `@output`
traits.

## FAQ

**Why do we not want to reuse input and output shapes?**

Every operation should have a dedicated input and output shape that only
functions as the input or output for one operation. The shape should not be
reused by other operations, should not function as both input and output, and
should not be used nested in other members.

1. Referencing the same input or output structure from multiple operations can
lead to backward-compatibility problems in the future if the inputs or
outputs of the operations ever need to diverge. By using the same structure,
teams unnecessarily tie the interfaces and future evolution of operations
together.
2. Using the same structure for both input and output can lead to
backward-compatibility problems in the future if the members or traits used
in input needs to diverge from those used in output. Reuse between operations
is better facilitated by mixins, which allow members and traits to be shared,
but also allows teams to later refactor or even remove the applied mixins.
3. Referencing an operation input or output shape as a member of another shape
gives the type multiple use cases. This makes it harder for code generators
to give special functionality to input and output types without affecting
the way nested types are handled, leading code generators to generate
synthetic types.

**How will we get service teams to use the `@input` and `@output` traits?**

* The inline operation input and output syntax being introduced in Smithy 2.0
is so convenient it will likely be used by most new models, and it will
automatically add these traits.
* Model validation will emit a WARNING when operations target shapes that do
not have the `@input` or `@output` trait.

**What happens to operations that have no input or output?**

* Operations will by default target `smithy.api#Unit`.
* A WARNING will be emitted when an operation defaults to `smithy.api#Unit`
without explicitly targeting `smithy.api#Unit`. This encourages modelers
to be deliberate in their commitment to never needing input or output in
the future for an operation.
* Existing AWS service models will all be updated to define input or output if
they do not already.

**Will this be a breaking change in the reference Java implementation?**

No. The existing methods that return things like `Optional.empty` will continue
to function exactly as they did before, but will be marked as deprecated. New
methods will be added that always return a `ShapeId` or `StructureShape` when
accessing things like the input of an operation or querying `OperationIndex`.

**Won't it be annoying to have to model resource operations that reuse the same members?**

No. Models typically SHOULD NOT use the same input or output structures for
resources that perform CRUDL operations.

1. The data returned in a Get operation SHOULD NOT be coupled to the data
returned in a List operation because:
* Getting the details of a resource SHOULD require more elevated permissions
than knowing a resource exists. If List and Get responses are coupled in
the model, sensitive members that might need to be added later to the
output of a Get response will automatically show up in the List response.
* Getting all the details of a resource can be expensive, which impacts
the ability to meet service level agreements around latency when listing
many resources. If List and Get responses are coupled in the model, then
every additional member added to the resource impacts the latency of List
operations multiplied by the number of resources returned.
2. The data returned in a Get response SHOULD NOT be the same data sent in an
update or create input because properties returned in a Get response are
often generated by the server (like `createdAt`).
3. For any case where this _is_ valid overlap, Smithy IDL 2.0 introduces
mixins, which allow for build-time copy and paste.

**Will this be awkward in AWS SDKs that don't know about unit types?**

No. Model transformations that convert Smithy models to older AWS modeling
formats will remove operation input and output references to `smithy.api#Unit`.
Loading

0 comments on commit e771467

Please sign in to comment.