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 initial docs for TypeScript SSDK #1119

Merged
merged 2 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions docs/source/1.0/spec/core/constraint-traits.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _constraint-traits:

=================
Constraint traits
=================
Expand Down
28 changes: 22 additions & 6 deletions docs/source/implementations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Build tooling


---------------
Code generators
Client code generators
---------------

The following code generators are in early development. There's no guarantee
Expand All @@ -136,27 +136,43 @@ of polish or that they work for all use cases.
- Language
- Status
- Description
* - `TypeScript client and server codegen <https://github.com/awslabs/smithy-typescript>`_
* - `TypeScript <https://github.com/awslabs/smithy-typescript>`_
- Java
- 0.x
- TypeScript client and server code generation for Smithy.
* - `Go client codegen <https://github.com/awslabs/smithy-go>`_
* - `Go <https://github.com/awslabs/smithy-go>`_
- Java
- 0.x
- Go client code generation for Smithy.
* - `Rust client codegen <https://github.com/awslabs/smithy-rs>`_
* - `Rust <https://github.com/awslabs/smithy-rs>`_
- Kotlin
- 0.x
- Rust client code generation for Smithy.
* - `Kotlin client codegen <https://github.com/awslabs/smithy-kotlin>`_
* - `Kotlin <https://github.com/awslabs/smithy-kotlin>`_
- Kotlin
- 0.x
- Kotlin client code generation for Smithy.
* - `Swift client codegen <https://github.com/awslabs/smithy-swift>`_
* - `Swift <https://github.com/awslabs/smithy-swift>`_
- Kotlin
- 0.x
- Swift client code generation for Smithy.

----------------------
Server code generators
----------------------

.. list-table::
:header-rows: 1
:widths: 50 15 10 25

* - Project
- Language
- Status
- Additional links
* - `Smithy Server Generator for TypeScript <https://github.com/awslabs/smithy-typescript>`_
gosar marked this conversation as resolved.
Show resolved Hide resolved
- Java
- 0.x (Developer Preview)
- :doc:`Documentation <ts-ssdk/index>`

----------------
Model converters
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Smithy
implementations
1.0/spec/index
1.0/guides/index
ts-ssdk/index
75 changes: 75 additions & 0 deletions docs/source/ts-ssdk/error-handling.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#####################################################
Smithy Server Generator for TypeScript error handling
#####################################################

Errors are structures associated with services and operations in the Smithy model and are used to indicate that the
server encountered a problem during request processing. Structures with the error trait are code generated as subclasses
of ``Error``.

If a code-generated ``Error`` is thrown from a handler implementation, and the ``Error`` is associated with the
operation or service in the model, then the server SDK will serialize the Error according to the rules of the protocol.
For instance, HTTP binding protocols set the status code of the response based on the ``@httpError`` trait.

If a non-code-generated Error, or a code-generated Error that is not associated with the operation or service in the
model, is thrown from a handler, then a *synthetic* ``InternalFailure`` will be rendered as the result instead.

Synthetic errors
================

*Synthetic errors* are errors that are not included in the Smithy model, but can still be thrown by the server SDK. In
general, these errors are not expected to have corresponding code generated types on the client side. These errors fall
into two categories: framework-level errors that are unavoidable, and errors that are associated with the low-level
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default errors from API Gateway should also have its own category as part of the Synthetic errors. With the difference that the service team will have no control over it (as explained briefly in this issue).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those would need their own section once we come up with a more specific API Gateway solution.

There are always going to be intermediaries (CDNs, caches, reverse proxies, what-have-you) that can return error responses that the server has no control over. While we have some customization options for API Gateway, generated clients should be durable to these failures, and I don't think the proper experience is customers having a modeled type for things like 502 Bad Gateway.

transport protocol, such as HTTP.

For backwards compatibility purposes, the names of synthetic errors generally end with ``Exception``.

Unavoidable errors
------------------

.. _TS SSDK internal-failure-exception:

InternalFailureException
~~~~~~~~~~~~~~~~~~~~~~~~

InternalFailureException is the catch-all exception for unexpected errors. It indicates either a bug in the framework or
an exception being thrown from the handler that is not modeled. The server SDK will never include any details about
internal failures, such as a meaningful exception message, to the caller, in order to prevent unintended information
disclosure.

Service developers wishing to throw an InternalFailureException to indicate a bug in their code can simply throw a
built-in JavaScript error such as ``TypeError``.

.. _TS SSDK serialization-exception:

SerializationException
~~~~~~~~~~~~~~~~~~~~~~

SerializationException is thrown by the server SDK when a request is unparseable, or if there is a type mismatch between
a member in the serialized request and the member in the Smithy model. Since these failures occur during the
deserialization process, server developers have no ability to customize these messages, and they will short-circuit
request processing before validation occurs.

.. _TS SSDK unknown-operation-exception:

UnknownOperationException
~~~~~~~~~~~~~~~~~~~~~~~~~

UnknownOperationException is returned when the request cannot be matched to a handler known to the server SDK. This can
happen either because the server SDK does not recognize the request protocol, which is common for internet-facing
adamthom-amzn marked this conversation as resolved.
Show resolved Hide resolved
endpoints that receive robotic traffic, or if the request is in a known protocol but for an operation that is unknown to
the handler. The latter case can indicate a misconfiguration, such as an operation-level handler being used incorrectly.

Protocol errors
---------------

UnsupportedMediaTypeException
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

UnsupportedMediaTypeException represents HTTP error 415, returned when a request has a ``Content-Type`` that is not
accepted for the protocol or does not match the ``@mediaType`` trait in the model.

NotAcceptableException
~~~~~~~~~~~~~~~~~~~~~~

NotAcceptableException represents HTTP error 406, returned when a request's ``Accept`` header does not match the type used
to serialize protocol responses, or when it does not match the response payload's ``@mediaType`` value.
162 changes: 162 additions & 0 deletions docs/source/ts-ssdk/handlers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
###############################################
Smithy Server Generator for TypeScript handlers
###############################################

The primary abstraction of a Smithy server is ``ServiceHandler``, the interface to the server's
generated implementation that a service's code will build and call directly.

.. code-block:: typescript

export interface ServiceHandler<Context = {}, RequestType = HttpRequest, ResponseType = HttpResponse> {
handle(request: RequestType, context: Context): Promise<ResponseType>;
}

A ``ServiceHandler`` is simply a function that takes in a request (by default, an ``HttpRequest``) and an optional
``Context`` of arbitrary type, and returns a response (by default, an ``HttpResponse``).

The Smithy Server Generator for TypeScript generates two different sets of handlers: one handler for the service as a
whole, and another handler for each individual operation in the service. This allows the service to choose the handler
that works best for their situation.

The primary difference between service-level and operation-level handlers is where routing (the matching of requests
to Smithy operations) is performed. Operation-level handlers assume that routing is handled outside of the server SDK,
which is commonly the case when a service like Amazon API Gateway is used. Service-level handlers are used when all
requests from an endpoint are handled by a handler, which would be the case when using Node.js's built-in HTTP server.

Consider a service with the following Smithy declaration:

.. code-block:: smithy

@restJson1
service StringWizard {
version: "2018-05-10",
operations: [Echo, Length]
}

The SSDK will generate three different implementations of ``ServiceHandler``. One for ``StringWizard``:

.. code-block:: typescript

export class StringWizardServiceHandler<Context> implements ServiceHandler<Context> { /* ... */ }

As well as one for ``Echo`` and one for ``Length``:

.. code-block:: typescript

export class EchoHandler<Context> implements ServiceHandler<Context> { /* ... */ }

export class LengthHandler<Context> implements ServiceHandler<Context> { /* ... */ }

ServiceHandler factories
========================

Each handler class is protocol-agnostic; the implementation details of protocol serialization, deserialization, and
routing are passed into the handler class's constructor. For convenience, a factory method is generated for the
service's protocol that supply the necessary parameters to the handlers' constructors.

For instance, for the ``Length`` operation of ``StringWizard``, a corresponding handler factory function is generated:

.. code-block:: typescript

export const getLengthHandler = <Context>(operation: Operation<LengthServerInput, LengthServerOutput, Context>):
ServiceHandler<Context, HttpRequest, HttpResponse> => { /* ... */ }

The only parameter of ``getLengthParameter`` is an implementation of
``Operation<LengthServerInput, LengthServerOutput, Context>``. ``Operation`` is a type distributed in
``@aws-smithy/server-common`` that server developers implement in order to perform the business logic of an operation
described in their Smithy model.

.. code-block:: typescript

export type Operation<I, O, Context = {}> = (input: I, context: Context) => Promise<O>;

The Lambda implementation of the ``Length`` operation can then be as simple as:

.. code-block:: typescript

// getLengthHandler is the code generated handler factory
const handler = getLengthHandler(async (input) => {
return {
length: input.string?.length,
$metadata: {}
};
});

export const lambdaHandler: APIGatewayProxyHandler = async (event): Promise<APIGatewayProxyResult> => {
// This uses the shim from @aws-smithy/server-apigateway to convert APIGateway events to HttpRequests
const httpRequest = convertEvent(event);

const httpResponse = await handler.handle(httpRequest, {});

// This uses the shim from @aws-smithy/server-apigateway to convert HttpResponses to APIGateway events
return convertVersion1Response(httpResponse);
};

Since ``getLengthHandler`` is code generated against the input and output types of the ``Length`` operation, this code
has the additional benefit of being type-safe, even though the incoming event is simply a raw HTTP request.
Additionally, although ``getLengthHandler`` can only service requests for the ``Length`` operation, it still asserts
that the incoming request matches the modeled expectations for ``Length``. This means if the developer accidentally
deploys the code for ``Length`` to the Lambda function for ``Echo``, the handler will reject the request instead of
passing it onto the business logic and executing the wrong code.

The handler factory function for services, is similar, but instead of requiring an implementation of ``Operation``,
it requires an implementation of every ``Operation`` in the service. For instance, for ``StringWizardService``,
the handler factory function looks like this:

.. code-block:: typescript

export const getStringWizardServiceHandler = <Context>(service: StringWizardService<Context>):
__ServiceHandler<Context, __HttpRequest, __HttpResponse> => { /* ... */ }

``StringWizardService`` is a generated interface with the following definition:

.. code-block:: typescript

export interface StringWizardService<Context> {
Echo: Operation<EchoServerInput, EchoServerOutput, Context>
Length: Operation<LengthServerInput, LengthServerOutput, Context>
}

This conveys the same type-safety benefits as the operation-level handler factory, as well as ensuring that any
service handler has an implementation for every operation in the service. This means type checks will fail if your
model adds an operation, but the service's source code is not properly updated to add an implementation for it.

.. _TS SSDK context:

Contexts
========

All handlers take an arbitrary ``Context`` of a type specified at runtime via the handler's ``Context`` generic type
argument. This allows the service developer to pass unmodeled data from the request or runtime environment to their
business logic.

For instance, a server running in AWS Lambda behind Amazon API Gateway could define a context that includes the calling
user's ARN, in order to do authorization checks in their business logic:

.. code-block:: typescript

interface HandlerContext {
user: string;
}

and then modify their entry-point implementation to extract the user's identity from the incoming request and pass it to
the handler:

.. code-block:: typescript

export const lambdaHandler: APIGatewayProxyHandler = async (event): Promise<APIGatewayProxyResult> => {
const httpRequest = convertEvent(event);

const userArn = event.requestContext.identity.userArn;
if (!userArn) {
throw new Error("IAM Auth is not enabled");
}
const context = { user: userArn };

const httpResponse = await handler.handle(httpRequest, context);

return convertVersion1Response(httpResponse);
};

The value of ``Context`` is not constrained or modified by the server SDK in any way; it is passed through unmodified to
the ``Operation`` implementation.
14 changes: 14 additions & 0 deletions docs/source/ts-ssdk/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
######################################
Smithy Server Generator for TypeScript
######################################

The Smithy Server Generator for TypeScript is Smithy's officially-supported way to write web services in TypeScript.

.. toctree::
:maxdepth: 3

introduction
handlers
validation
error-handling
supported-endpoints
60 changes: 60 additions & 0 deletions docs/source/ts-ssdk/introduction.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
##########################################################
Introduction to the Smithy Server Generator for TypeScript
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should we talk about how to use this codegen, i.e., something like

      "plugins": {
        "typescript-ssdk-codegen": {
          "package": "@aws-smithy/example",
          "packageVersion": "1.0.0-alpha.1"
        }

Currently this only comes up in validation section for how to disable, but seems like it should come up earlier. Some section on getting started?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a quickstart section, I'd rather have an example repository we link to. I don't have that repository yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, example repo will be good

##########################################################

The Smithy Server Generator generates a lightweight server-side framework for request handling known as a server SDK,
or SSDK. A server SDK enables server applications, also referred to as services, modeled in Smithy by performing the
reverse of a client SDK: it deserializes the inputs and serializes the outputs of Smithy operations. Smithy services are
always written model-first, which encourages developers to focus on their service's contract with its clients, instead
of leaving the contract to be defined implicitly from their implementation choices.

A Smithy model defines a :ref:`service <service>` with one or more :ref:`operations <operation>`. Each operation has an
input shape and an output shape, as well as a set of associated :ref:`error <error-trait>` shapes. This structure is
universally applicable to Smithy services, regardless of protocol. Server SDKs are generated from these models into the
targeted programming language using the same structure, with interfaces called handlers serving as the entrypoint at
both the service and operation level.

Data flow
=========

Smithy services are generally request-reply services, the basic unit of work for which is one request, corresponding to
an invocation of a modeled operation. An incoming request will first be serviced by an
:doc:`endpoint <supported-endpoints>`, which is responsible for reading and writing bytes from the wire, and parsing and
validating the low-level transport protocol, such as HTTP. These endpoints are separate from the server SDK;
examples include Amazon API Gateway, Node.js's HTTP module, or Express.

Next, the request passes through a shim layer, which converts the endpoint's request and response types into the ones
used by the server SDK. These shim layers can be one of the prebuilt libraries published alongside the server SDK,
or purpose built for an endpoint with no corresponding library. Since they are just type conversions, their logic should
be easy to understand, and have no dependencies on any particular Smithy implementation detail or specific
Smithy-modeled service.

In this phase, a service developer also has an opportunity to create a :ref:`context <TS SSDK context>` for the
operation invocation. Contexts generally encapsulate out-of-band, unmodeled data, such as the result of authentication
or pertinent metadata from the endpoint. Contexts are passed as-is to the operation implementation via the server SDK.

After conversion, the service developer invokes the server SDK directly by passing the request to a
:doc:`handler <handlers>`. This is the first time in request processing that the developer yields control of
execution to the SSDK. All of the preceding steps must be written explicitly.

The generated implementation of the handler first performs routing, which determines which operation the supplied
request is intended to invoke. If the request does not correspond to any of the handler's known operations, an
:ref:`UnknownOperationException <TS SSDK unknown-operation-exception>` is generated and returned by the handler. Next,
the handler deserializes the HTTP request and parses it into an object of a type generated from the operation's input.
If the input is unparseable, a :ref:`SerializationException <TS SSDK serialization-exception>` is generated and
returned. Finally, the handler performs :doc:`input validation <validation>` on the deserialized object. If
validation succeeds, the supplied operation implementation is invoked, yielding control back to the service developer.

The developer's operation implementation receives the deserialized input object and the context supplied to the handler.
It must return either an object conforming to the type of the output object, or throw an
:doc:`error <error-handling>`. In either case, the result is serialized into a response appropriate to the
service's protocol and returned from the handler, where the service developer must pass the response through the shim
layer before passing the converted response back to the endpoint.

This execution flow allows the service developer to choose not only their endpoint, but the programming model of their
service. For instance, all of the shim conversion and handler invocation can be refactored into a convenience method,
or the service developer could choose to incorporate their favorite open source middleware library, of which the server
SDK would simply be one layer. It also allows open-ended request preprocessing and response postprocessing to happen
independent of Smithy. For instance, a developer could add support for request or response compression, or a custom
authentication and authorization framework could be plugged into the application before the server SDK is invoked,
without having to fight against a more heavyweight abstraction.
Loading