smithy/designs/inline-io.md

6.8 KiB
Raw Blame History

Inline Operation Inputs / Outputs

This document describes a way to write input and output shapes as an inline part of an operations 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, its 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 youre making. Since operation inputs and outputs may only be structures, this isnt necessary.

Generated Names

Inlined structures will have names generated for them if they arent 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 theres 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, cant 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 theres 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.