Skip to content

Commit

Permalink
Docs
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Feb 14, 2019
1 parent 80b03d5 commit b92ac1a
Show file tree
Hide file tree
Showing 19 changed files with 774 additions and 11 deletions.
2 changes: 2 additions & 0 deletions core/src/main/scala/tapir/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.io.{File, InputStream}
import java.nio.ByteBuffer
import java.nio.charset.{Charset, StandardCharsets}
import java.nio.file.Path
import java.util.UUID

import tapir.DecodeResult._
import tapir.generic.{FormCodecDerivation, MultipartCodecDerivation}
Expand Down Expand Up @@ -55,6 +56,7 @@ object Codec extends FormCodecDerivation with MultipartCodecDerivation {
implicit val floatPlainCodec: PlainCodec[Float] = plainCodec[Float](_.toFloat, Schema.SNumber)
implicit val doublePlainCodec: PlainCodec[Double] = plainCodec[Double](_.toDouble, Schema.SNumber)
implicit val booleanPlainCodec: PlainCodec[Boolean] = plainCodec[Boolean](_.toBoolean, Schema.SBoolean)
implicit val uuidPlainCodec: PlainCodec[UUID] = plainCodec[UUID](UUID.fromString, Schema.SString)

def stringCodec(charset: Charset): PlainCodec[String] = plainCodec(identity, Schema.SString, charset)

Expand Down
4 changes: 2 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
# built documents.
#
# The short X.Y version.
version = u'1.0'
version = u'0.0.11'
# The full version, including alpha/beta/rc tags.
release = u'1.0'
release = u'0.0.11'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
16 changes: 16 additions & 0 deletions doc/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Contributing

Tapir is an early stage project. Everything might change. Be warned: the code is often ugly, uses mutable state,
imperative loops and type casts. Luckily, there's no reflection. All suggestions welcome :)

See the list of [issues](https://github.com/softwaremill/tapir/issues) and pick one! Or report your own.

If you are having doubts on the *why* or *how* something works, don't hesitate to ask a question on
[gitter](https://gitter.im/softwaremill/tapir) or via github. This probably means that the documentation, scaladocs or
code is unclear and be improved for the benefit of all.

## Acknowledgments

Tuple-concatenating code is copied from [akka-http](https://github.com/akka/akka-http/blob/master/akka-http/src/main/scala/akka/http/scaladsl/server/util/TupleOps.scala)

Generic derivation configuration is copied from [circe](https://github.com/circe/circe/blob/master/modules/generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala)
48 changes: 48 additions & 0 deletions doc/endpoint/basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Anatomy an endpoint

An endpoint is represented as a value of type `Endpoint[I, E, O, S]`, where:

* `I` is the type of the input parameters
* `E` is the type of the error-output parameters
* `O` is the type of the output parameters
* `S` is the type of streams that are used by the endpoint's inputs/outputs

Input/output parameters (`I`, `E` and `O`) can be:

* of type `Unit`, when there's no input/ouput of the given type
* a single type
* a tuple of types

Hence, an empty, initial endpoint (`tapir.endpoint`), with no inputs and no outputs, from which all other endpoints are
derived has the type:

```scala
val endpoint: Endpoint[Unit, Unit, Unit, Nothing] = ...
```

An endpoint which accepts two parameters of types `UUID` and `Int`, upon error returns a `String`, and on normal
completion returns a `User`, would have the type:

```scala
Endpoint[(UUID, Int), String, User, Nothing]
```

You can think of an endpoint as a function, which takes input parameters of type `I` and returns a result of type
`Either[E, O]`, where inputs or outputs can contain streaming bodies of type `S`.

## Defining an endpoint

The description of an endpoint is an immutable case class, which includes a number of methods:

* the `name`, `description`, etc. methods allow modifying the endpoint information, which will then be included in the
endpoint documentation
* the `get`, `post` etc. methods specify the HTTP method which the endpoint should support
* the `in`, `errorOut` and `out` methods allow adding a new input/output parameter
* `mapIn`, `mapInTo`, ... methods allow mapping the current input/output parameters to another value or to a case class

An important note on mapping: in tapir, all mappings are bi-directional. That's because each mapping can be used to
generate a server or a client, as well as in many cases can be used both for input and for output.

## Next

Read on about describing [endpoint inputs/outputs](ios.md).
108 changes: 108 additions & 0 deletions doc/endpoint/codecs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Codecs

A codec specifies how to map from and to raw values that are sent over the network. Raw values, which are natively
supported by client/server interpreters, include `String`s, byte arrays, `File`s and multiparts.

There are built-in codecs for most common types such as `String`, `Int` etc. Codecs are usually defined as implicit
values and resolved implicitly when they are referenced.

For example, a `query[Int]("quantity")` specifies an input parameter which will be read from the `quantity` query
parameter and decoded into an `Int`. There's an implicit `Codec[Int]` parameter that is referenced by the `query`
method. In a server setting, if the value cannot be parsed to an int, a decoding failure is reported, and the endpoint
won't match the request, or a `400 Bad Request` response is returned (depending on configuration).

## Optional and multiple parameters

Some inputs/outputs allow optional, or multiple parameters:

* path segments are always required
* query and header values can be optional or multiple (repeated query parameters/headers)
* bodies can be optional, but not multiple

In general, optional parameters are represented as `Option` values, and multiple parameters as `List` values.
For example, `header[Option[String]]("X-Auth-Token")` describes an optional header. An input described as
`query[List[String]]("color")` allows multiple occurences of the `color` query parameter, with all values gathered
into a list.

### Implementation note

To support optional and multiple parameters, inputs/outputs don't require implicit `Codec` values (which represent
only mandatory values), but `CodecForOptional` and `CodecForMany` implicit values.

A `CodecForOptional` can be used in a context which *allows* optional values. Given a `Codec[T]`, instances of both
`CodecForOptional[T]` and `CodecForOptional[Option[T]]` will be generated (that's also the way to add support for
custom optional types). The first one will require a value, and report a decoding failure if a value is missing. The
second will properly map to an `Option`, depending if the value is present or not.

## Schemas

A codec also contains the schema of the mapped type. This schema information is used when generating documentation.
For primitive types, the schema values are built-in, and include values such as `Schema.SString`, `Schema.SArray`,
`Schema.SBinary` etc.

For complex types, it is possible to define the schema by hand and apply it to a codec (using the `codec.schema`
method), however usually e.g. json codecs lookup the schema implicitly by requiring an implicit value of type
`SchemaFor[T]`. A schema-for value contains a single `schema: Schema` field.

`SchemaFor[T]` values are automatically derived for case classes using
[Magnolia](https://propensive.com/opensource/magnolia/). It is possible to configure the automatic derivation to use
snake-case, kebab-case or a custom field naming policy, by providing an implicit `tapir.generic.Configuration` value:

```scala
implicit val customConfiguration: Configuration = Configuration.defaults.snakeCaseTransformation
```

## Media types

Codecs carry an additional type parameter, which specifies the media type. Some built-in media types include
`text/plain`, `application/json` and `multipart/form-data`. Custom media types can be added by creating an
implementation of the `tapir.MediaType` trait.

Thanks to codec being parametrised by media types, it is possible to have a `Codec[MyCaseClass, TextPlain, _]` which
specifies how to serialize a case class to plain text, and a different `Codec[MyCaseClass, Json, _]`, which specifies
how to serialize a case class to json. Both can be implicitly available without implicit resolution conflicts.

Different media types can be used in different contexts. When defining a path, query or header parameter, only a codec
with the `TextPlain` media type can be used. However, for bodies, any media types is allowed. For example, the io
described by `jsonBody[T]` requires a json codec.

## Custom types

Support for custom types can be added by writing a codec from scratch, or mapping over an existing codec. However,
custom types can also be supported by mapping over inputs/outputs, not codecs. When to use one and the other?

In general, codecs should be used when translating between raw values and "application-primitives". Codecs also
allow the decoding process to result in an error, or to complete successfully. For example, to support a custom id type:

```scala
def decode(s: String): DecodeResult[MyId] = MyId.parse(s) match {
case Success(v) => DecodeResult.Value(v)
case Failure(f) => DecodeResult.Error(s, f)
}
def encode(id: MyId): String = id.toString

implicit val myIdCodec: Codec[MyId] = Codec.stringPlainCodecUtf8.mapDecode(decode)(encode)
```

Additionally, if a type is supported by a codec, it can be used in multiple contexts, such as query parameters, headers,
bodies, etc. Mapped inputs by construction have a fixed context.

On the other hand, when building composite types out of many values, or when an isomorphic representation of a type
is needed, but only for a single input/output/endpoint, mapping over an input/output is the simpler solution. Note that
while codecs can report errors during decoding, mapping over inputs/outputs doesn't have this possibility.

## Validation

While codecs support reporting decoding failures, this is not meant as a validation solution, as it only works on single
values, while validation often involves multiple combines values.

Decoding failures should be reported when the input is in an incorrect low-level format, when parsing a "raw value"
fails. In other words, decoding failures should be reported for format errors, not business validation failures.

Any validation should be done as part of the "business logic" methods provided to the server interpreters. In case
validation fails, the result of the method can be an error, which is one of the mappings defined in an endpoint
(the `E` in `Endpoint[I, E, O, S]`).

## Next

Read on about [json support](json.md).
55 changes: 55 additions & 0 deletions doc/endpoint/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Form support

## URL-encoded forms

An URL-encoded form input/output can be specified in two ways. First, it is possible to map all form fields as a
`Seq[(String, String)]` or `Map[String, String]` (which is more convenient if fields can't have multiple values):

```scala
formBody[Seq[(String, String)]]: EndpointIO[Seq[(String, String)],
MediaType.XWwwFormUrlencoded, _]
formBody[Map[String, String]]: EndpointIO[Map[String, String],
MediaType.XWwwFormUrlencoded, _]
```

Second, form data can be mapped to a case class. The codec for the case class is generated using a macro at
compile-time. The fields of the case class should have types, for which there is a plain text codec. For example:

```scala
case class RegistrationForm(name: String, age: Int, news: Boolean, city: Option[String])

formBody[RegistrationForm]
```

Each form-field is named the same as the case-class-field. The names can be transformed to snake or kebab case by
providing an implicit `tapir.generic.Configuraton`.

## Multipart forms

Similarly as above, multipart form input/outputs can be specified in two ways. To map to all parts of a multipart body,
use:

```scala
multipartBody[Seq[AnyPart]]: EndpointIO[Seq[AnyPart], MediaType.MultipartFormData, _]
```

where `type AnyPart = Part[_]`. `Part` is a case class containing the `name` of the part, disposition parameters,
headers, and the body.

As with URL-encoded forms, multipart bodies can be mapped directly to case classes, however without the restriction
on codecs for individual fields. Given a field of type `T`, first a plain text codec is looked up, and if one isn't
found, any codec for any media type (e.g. JSON) is searched for.

Each part is named the same as the case-class-field. The names can be transformed to snake or kebab case by
providing an implicit `tapir.generic.Configuraton`.

Additionally, the case class to which the multipart body is mapped can contain both normal fields, and fields of type
`Part[T]`. This is useful, if part metadata (e.g. the filename) is relevant.

For example:

```scala
case class RegistrationForm(userData: User, photo: Part[File], news: Boolean)

multipartBody[RegistrationForm]
```
91 changes: 91 additions & 0 deletions doc/endpoint/ios.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Defining endpoint's input/output

An input is represented as an instance of the `EndpointInput` trait, and an output as an instance of the `EndpointIO`
trait, as all outputs can also be used as inputs. Each input or output can yield/accept a value. For example,
`query[Int]("age"): EndpointInput[Int]` describes an input, which maps to a query parameter `age`, which should be
parsed (using the string-to-integer [codec](codecs.md)) as an `Int`.

The `tapir` package contains a number of convenience methods to define an input or an output for an endpoint.
These are:

* `path[T]`, which captures a path segment as an input parameter of type `T`
* any string, which will be implicitly converted to a constant path segment. Path segments can be combined with the `/`
method, and don't map to any values (have type `EndpointInput[Unit]`)
* `paths`, which maps to the whole remaining path as a `Seq[String]`
* `query[T](name)` captures a query parameter with the given name
* `queryParams` captures all query parameters, represented as `MultiQueryParams`
* `header[T](name)` captures a header with the given name
* `headers` captures all headers, represented as `Seq[(String, String)]`
* `cookies` captures cookies from the `Cookie` header and represents them as `List[CookiePair]`
* `setCookies` captures cookies from the `Set-Cookie` header and represents them as `List[Cookie]`
* `body[T, M]`, `stringBody`, `plainBody[T]`, `jsonBody[T]`, `binaryBody[T]`, `formBody[T]`, `multipartBody[T]`
captures the body
* `streamBody[S]` captures the body as a stream: only a client/server interpreter supporting streams of type `S` can be
used with such an endpoint

For outputs, you can use the `header`, `setCookies` and `body` family of methods.

## Combining inputs and outputs

Endpoint inputs/outputs can be combined in two ways. However they are combined, the values they represent always
accumulate into tuples of values.

First, descriptions can be combined using the `.and` method. Such a combination results in an input/output represented
as a tuple of the given types, can be stored as a value and re-used in multiple endpoints. As all other values in tapir,
endpoint input/output descriptions are immutable. For example, an input specifying two query parameters, `start`
(mandatory) and `limit` (optional) can be written down as:

```scala
val paging: EndpointInput[(UUID, Option[Int])] = query[UUID]("start").and(query[Option[Int]]("limit"))

// we can now use the value in multiple endpoints, e.g.:
val listUsersEndpoint: Endpoint[(UUID, Option[Int]), Unit, List[User], Nothing] =
endpoint.in("user" / "list").in(paging).out(jsonBody[List[User]])
```

Second, inputs can be combined by calling the `in`, `out` and `errorOut` methods on `Endpoint` multiple times. Each time
such a method is invoked, it extends the list of inputs/outputs. This can be useful to separate different groups of
parameters, but also to define template-endpoints, which can then be further specialized. For example, we can define a
base endpoint for our API, where all paths always start with `/api/v1.0`, and errors are always returned as a json:

```scala
val baseEndpoint: Endpoint[Unit, ErrorInfo, Unit, Nothing] = endpoint.in("api" / "v1.0").errorOut(jsonBody[ErrorInfo])
```

Thanks to the fact that inputs/outputs accumulate, we can use the base endpoint to define more inputs, for example:

```scala
val statusEndpoint: Endpoint[Unit, ErrorInfo, Status, Nothing] = baseEndpoint.in("status").out(jsonBody[Status])
```

The above endpoint will correspond to the `api/v1.0/status` path.

## Mapping over input values

Inputs/outputs can also be mapped over. As noted before, all mappings are bi-directional, so that they can be used both
when interpreting an endpoint as a server, and as a client.

There's a couple of ways to map over an input/output. First, there's the `map[II](f: I => II)(g: II => I)` method,
which accepts functions which provide the mapping in both directions. For example:

```scala
case class Paging(from: UUID, limit: Option[Int])
val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit"))
.map((from, limit) => Paging(from, limit))(paging => (paging.from, paging.limit))
```

Creating a mapping between a tuple and a case class is a common operation, hence there's also a
`mapTo(CaseClassCompanion)` method, which automatically provides the mapping functions:

```scala
case class Paging(from: UUID, limit: Option[Int])
val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit"))
.mapTo(Paging)
```

Mapping methods can also be called on an endpoint (which is useful if inputs/outputs are accumulated, for example).
The `Endpoint.mapIn`, `Endpoint.mapInTo` etc. have the same signatures are the ones above.

## Next

Read on about [codecs](codecs.md).
39 changes: 39 additions & 0 deletions doc/endpoint/json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Working with JSON

Json values are supported through codecs which encode/decode values to json strings. However, third-party libraries are
needed for actual json parsing/printing. Currently, [Circe](https://github.com/circe/circe) is supported. To use, add
the following dependency to your project:

```scala
"com.softwaremill.tapir" %% "tapir-json-circe" % "0.0.11"
```

Next, import the package (or extend the `JsonCirce` trait, see [MyTapir](../mytapir.md)):

```scala
import tapir.json.circe._
```

This will bring into scope `Codec`s which, given an in-scope circe `Encoder`/`Decoder`, will create a codec using the
json media type. Circe includes a couple of codec generating methods (manual, semi-auto and auto), so you may choose
whatever suits you.

For example, to automatically generate a JSON codec for a case class:

```scala
import tapir._
import tapir.json.circe._
import io.circe.generic.auto._

case class Book(author: String, title: String, year: Int)

val bookInput: EndpointIO[Book] = jsonBody[Book]
```

To add support for other JSON libraries, see the
[sources](https://github.com/softwaremill/tapir/blob/master/json/circe/src/main/scala/tapir/json/circe/JsonCirce.scala)
for the Circe codec (which is just a couple of lines of code).

## Next

Read on about [working with forms](forms.md).

0 comments on commit b92ac1a

Please sign in to comment.