Servant.Swagger

servant-swagger-1.1.3.1: Generate Swagger specification for your servant API.

License | BSD3
Maintainer | Nickolay Kudasov <nickolay@getshoptv.com>
Stability | experimental
Safe Haskell | None
Language | Haskell2010

Servant.Swagger

Contents

- How to use this library
  - Generate ``Swagger``
  - Annotate
  - Test
  - Serve
- ``HasSwagger`` class
- Manipulation
- Testing

Description

This module provides means to generate and manipulate Swagger specification for servant APIs.

Swagger™ is a project used to describe and document RESTful APIs.

The Swagger specification defines a set of files required to describe such an API. These files can then be used by the Swagger-UI project to display the API and Swagger-Codegen to generate clients in various languages. Additional utilities can also take advantage of the resulting files, such as testing tools.

For more information see Swagger™ documentation.

Synopsis

- class HasSwagger api where
- subOperations :: (IsSubAPI sub api, HasSwagger sub) => Proxy sub -> Proxy api -> Traversal' Swagger Operation
- validateEveryToJSON :: forall proxy api. TMap (Every '[Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) => proxy api -> Spec
- validateEveryToJSONWithPatternChecker :: forall proxy api. TMap (Every '[Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) => (Pattern -> Text -> Bool) -> proxy api -> Spec

# How to use this library

This section explains how to use this library to generate Swagger specification, modify it and run automatic tests for a servant API.

For the purposes of this section we will use this servant API:

    >>> data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)
    >>> newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)
    >>> instance ToJSON User
    >>> instance ToSchema User
    >>> instance ToSchema UserId
    >>> instance ToParamSchema UserId
    >>> type GetUsers = Get '[JSON] [User]
    >>> type GetUser  = Capture "user_id" UserId :> Get '[JSON] User
    >>> type PostUser = ReqBody '[JSON] User :> Post '[JSON] UserId
    >>> type UserAPI  = GetUsers :<|> GetUser :<|> PostUser

Here we define a user API with three endpoints. `GetUsers` endpoint returns a list of all users. `GetUser` returns a user given his/her ID. `PostUser` creates a new user and returns his/her ID.

## Generate ``Swagger``

In order to generate ``Swagger`` specification for a servant API, just use ``toSwagger``:

    >>> BSL8.putStrLn $ encode $ toSwagger (Proxy :: Proxy UserAPI)
    {"swagger":"2.0","info":{"version":"","title":""},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}}

By default ``toSwagger`` will generate specification for all API routes, parameters, headers, responses and data schemas.

For some parameters it will also add 400 and/or 404 responses with a description mentioning parameter name.

Data schemas come from ``ToParamSchema`` and ``ToSchema`` classes.

## Annotate

While initially generated ``Swagger`` looks good, it lacks some information it can't get from a servant API.

We can add this information using field lenses from `Data.Swagger`:

    >>> :{
    BSL8.putStrLn $ encode $ toSwagger (Proxy :: Proxy UserAPI)
      & info.title        .~ "User API"
      & info.version      .~ "1.0"
      & info.description  ?~ "This is an API for the Users service"
      & info.license      ?~ "MIT"
      & host              ?~ "example.com"
    :}
    {"swagger":"2.0","info":{"version":"1.0","title":"User API","license":{"name":"MIT"},"description":"This is an API for the Users service"},"host":"example.com","paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}}

It is also useful to annotate or modify certain endpoints. ``subOperations`` provides a convenient way to zoom into a part of an API.

``subOperations` sub api` traverses all operations of the `api` which are also present in `sub`. Furthermore, `sub` is required to be an exact sub API of @api. Otherwise it will not typecheck.

`Data.Swagger.Operation` provides some useful helpers that can be used with ``subOperations``. One example is applying tags to certain endpoints:

    >>> let getOps  = subOperations (Proxy :: Proxy (GetUsers :<|> GetUser)) (Proxy :: Proxy UserAPI)
    >>> let postOps = subOperations (Proxy :: Proxy PostUser) (Proxy :: Proxy UserAPI)
    >>> :{
    BSL8.putStrLn $ encode $ toSwagger (Proxy :: Proxy UserAPI)
      & applyTagsFor getOps  ["get"  & description ?~ "GET operations"]
      & applyTagsFor postOps ["post" & description ?~ "POST operations"]
    :}
    {"swagger":"2.0","info":{"version":"","title":""},"paths":{"/":{"get":{"tags":["get"],"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"tags":["post"],"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"tags":["get"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}},"tags":[{"name":"get","description":"GET operations"},{"name":"post","description":"POST operations"}]}

This applies `"get"` tag to the `GET` endpoints and `"post"` tag to the `POST` endpoint of the User API.

## Test

Automatic generation of data schemas uses ``ToSchema`` instances for the types used in a servant API. But to encode/decode actual data servant uses different classes. For instance in `UserAPI` `User` is always encoded/decoded using ``ToJSON`` and ``FromJSON`` instances.

To be sure your Haskell server/client handles data properly you need to check that ``ToJSON`` instance always generates values that satisfy schema produced by ``ToSchema`` instance.

With ``validateEveryToJSON`` it is possible to test all those instances automatically, without having to write down every type:

    >>> instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrary
    >>> instance Arbitrary UserId where arbitrary = UserId <$> arbitrary
    >>> hspec $ validateEveryToJSON (Proxy :: Proxy UserAPI)

    [User]
    User
    UserId

    Finished in ... seconds
    3 examples, 0 failures

Although servant is great, chances are that your API clients don't use Haskell. In many cases `swagger.json` serves as a specification, not a Haskell type.

In this cases it is a good idea to store generated and annotated ``Swagger`` in a `swagger.json` file under a version control system (such as Git, Subversion, Mercurial, etc.).

It is also recommended to version API based on changes to the `swagger.json` rather than changes to the Haskell API.

See TodoSpec.hs for an example of a complete test suite for a swagger specification.

## Serve

If you're implementing a server for an API, you might also want to serve its ``Swagger`` specification.

See Todo.hs for an example of a server.

# ``HasSwagger`` class

class HasSwagger api where #

Generate a Swagger specification for a servant API.

To generate Swagger specification, your data types need ``ToParamSchema`` and/or ``ToSchema`` instances.

``ToParamSchema`` is used for ``Capture``, ``QueryParam`` and ``Header``. ``ToSchema`` is used for ``ReqBody`` and response data types.

You can easily derive those instances via `Generic`. For more information, refer to swagger2 documentation.

Example:

    newtype Username = Username String deriving (Generic, ToText)

    instance ToParamSchema Username

    data User = User
      { username :: Username
      , fullname :: String
      } deriving (Generic)

    instance ToJSON User
    instance ToSchema User

    type MyAPI = QueryParam "username" Username :> Get '[JSON] User

    mySwagger :: Swagger
    mySwagger = toSwagger (Proxy :: Proxy MyAPI)

Minimal complete definition

toSwagger

Methods

toSwagger :: Proxy api -> Swagger #

Generate a Swagger specification for a servant API.

Instances

HasSwagger * EmptyAPI # |
Methods toSwagger :: Proxy EmptyAPI api -> Swagger #
HasSwagger * Raw # |
Methods toSwagger :: Proxy Raw api -> Swagger #
(HasSwagger * a, HasSwagger * b) => HasSwagger * ((:<|>) a b) # |
Methods toSwagger :: Proxy (a :<|> b) api -> Swagger #
HasSwagger * sub => HasSwagger * (WithNamedContext x c sub) # | ``WithNamedContext`` combinator does not change our specification at all.
Methods toSwagger :: Proxy (WithNamedContext x c sub) api -> Swagger #
(ToSchema a, AllAccept [*] cs, HasSwagger k sub) => HasSwagger * ((:>) k * (ReqBody * cs a) sub) # |
Methods toSwagger :: Proxy ((k :> *) (ReqBody * cs a) sub) api -> Swagger #
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) k * (Header sym a) sub) # |
Methods toSwagger :: Proxy ((k :> *) (Header sym a) sub) api -> Swagger #
(KnownSymbol sym, HasSwagger k sub) => HasSwagger * ((:>) k * (QueryFlag sym) sub) # |
Methods toSwagger :: Proxy ((k :> *) (QueryFlag sym) sub) api -> Swagger #
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) k * (QueryParams * sym a) sub) # |
Methods toSwagger :: Proxy ((k :> *) (QueryParams * sym a) sub) api -> Swagger #
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) k * (QueryParam * sym a) sub) # |
Methods toSwagger :: Proxy ((k :> *) (QueryParam * sym a) sub) api -> Swagger #
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) k * (CaptureAll * sym a) sub) # | Swagger Spec doesn't have a notion of CaptureAll, this instance is the best effort.
Methods toSwagger :: Proxy ((k :> *) (CaptureAll * sym a) sub) api -> Swagger #
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) k * (Capture * sym a) sub) # |
Methods toSwagger :: Proxy ((k :> *) (Capture * sym a) sub) api -> Swagger #
(KnownSymbol sym, HasSwagger k sub) => HasSwagger * ((:>) k Symbol sym sub) # |
Methods toSwagger :: Proxy ((k :> Symbol) sym sub) api -> Swagger #
HasSwagger k sub => HasSwagger * ((:>) k * HttpVersion sub) # | ``HttpVersion`` combinator does not change our specification at all.
Methods toSwagger :: Proxy ((k :> *) HttpVersion sub) api -> Swagger #
HasSwagger k sub => HasSwagger * ((:>) k * RemoteHost sub) # | ``RemoteHost`` combinator does not change our specification at all.
Methods toSwagger :: Proxy ((k :> *) RemoteHost sub) api -> Swagger #
HasSwagger k sub => HasSwagger * ((:>) k * IsSecure sub) # | ``IsSecure`` combinator does not change our specification at all.
Methods toSwagger :: Proxy ((k :> *) IsSecure sub) api -> Swagger #
HasSwagger k sub => HasSwagger * ((:>) k * Vault sub) # | ``Vault`` combinator does not change our specification at all.
Methods toSwagger :: Proxy ((k :> *) Vault sub) api -> Swagger #
(AllAccept [*] cs, AllToResponseHeader [*] hs, KnownNat status, SwaggerMethod k1 method) => HasSwagger * (Verb * k1 method status cs (Headers hs NoContent)) # |
Methods toSwagger :: Proxy (Verb * k1 method status cs (Headers hs NoContent)) api -> Swagger #
(AllAccept [*] cs, KnownNat status, SwaggerMethod k1 method) => HasSwagger * (Verb * k1 method status cs NoContent) # |
Methods toSwagger :: Proxy (Verb * k1 method status cs NoContent) api -> Swagger #
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs, KnownNat status, SwaggerMethod k1 method) => HasSwagger * (Verb * k1 method status cs (Headers hs a)) # |
Methods toSwagger :: Proxy (Verb * k1 method status cs (Headers hs a)) api -> Swagger #
(ToSchema a, AllAccept [*] cs, KnownNat status, SwaggerMethod k1 method) => HasSwagger * (Verb * k1 method status cs a) # |
Methods toSwagger :: Proxy (Verb * k1 method status cs a) api -> Swagger #

# Manipulation

subOperations #

Arguments

:: (IsSubAPI sub api, HasSwagger sub) |
=> Proxy sub | Part of a servant API.
-> Proxy api | The whole servant API.
-> Traversal' Swagger Operation |

All operations of sub API. This is similar to ``operationsOf`` but ensures that operations indeed belong to the API at compile time.

# Testing

validateEveryToJSON #

Arguments

:: TMap (Every '[Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) |
=> proxy api | Servant API.
-> Spec |

Verify that every type used with ``JSON`` content type in a servant API has compatible ``ToJSON`` and ``ToSchema`` instances using ``validateToJSON``.

NOTE: ``validateEveryToJSON`` does not perform string pattern validation. See ``validateEveryToJSONWithPatternChecker``.

``validateEveryToJSON`` will produce one ``prop`` specification for every type in the API. Each type only gets one test, even if it occurs multiple times in the API.

    >>> data User = User { name :: String, age :: Maybe Int } deriving (Show, Generic, Typeable)
    >>> newtype UserId = UserId String deriving (Show, Generic, Typeable, ToJSON, Arbitrary)
    >>> instance ToJSON User
    >>> instance ToSchema User
    >>> instance ToSchema UserId
    >>> instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrary
    >>> type UserAPI = (Capture "user_id" UserId :> Get '[JSON] User) :<|> (ReqBody '[JSON] User :> Post '[JSON] UserId)

    >>> hspec $ context "ToJSON matches ToSchema" $ validateEveryToJSON (Proxy :: Proxy UserAPI)

    ToJSON matches ToSchema
      User
      UserId

    Finished in ... seconds
    2 examples, 0 failures

For the test to compile all body types should have the following instances:

- ``ToJSON`` and ``ToSchema`` are used to perform the validation;
- ``Typeable`` is used to name the test for each type;
- ``Show`` is used to display value for which ``ToJSON`` does not satisfy ``ToSchema``.
- ``Arbitrary`` is used to arbitrarily generate values.

If any of the instances is missing, you'll get a descriptive type error:

    >>> data Contact = Contact { fullname :: String, phone :: Integer } deriving (Show, Generic)
    >>> instance ToJSON Contact
    >>> instance ToSchema Contact
    >>> type ContactAPI = Get '[JSON] Contact
    >>> hspec $ validateEveryToJSON (Proxy :: Proxy ContactAPI)
    ...
    ...No instance for (Arbitrary Contact)
    ...  arising from a use of ‘validateEveryToJSON’
    ...

validateEveryToJSONWithPatternChecker #

Arguments

:: TMap (Every '[Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) |
=> (Pattern -> Text -> Bool) | ``Pattern`` checker.
-> proxy api | Servant API.
-> Spec |

Verify that every type used with ``JSON`` content type in a servant API has compatible ``ToJSON`` and ``ToSchema`` instances using ``validateToJSONWithPatternChecker``.

For validation without patterns see ``validateEveryToJSON``.