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

LicenseBSD3
MaintainerNickolay Kudasov <nickolay@getshoptv.com>
Stabilityexperimental
Safe HaskellNone
LanguageHaskell2010

Servant.Swagger

Contents

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

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:

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

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:

>>> :{
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\",\"host\":\"example.com\",\"info\":{\"version\":\"1.0\",\"title\":\"User API\",\"license\":{\"name\":\"MIT\"},\"description\":\"This is an API for the Users service\"},\"definitions\":{\"User\":{\"required\":[\"name\",\"age\"],\"type\":\"object\",\"properties\":{\"age\":{\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}},\"UserId\":{\"type\":\"integer\"}},\"paths\":{\"/{user_id}\":{\"get\":{\"responses\":{\"404\":{\"description\":\"`user_id` not found\"},\"200\":{\"schema\":{\"$ref\":\"#/definitions/User\"},\"description\":\"\"}},\"produces\":[\"application/json\"],\"parameters\":[{\"required\":true,\"in\":\"path\",\"name\":\"user_id\",\"type\":\"integer\"}]}},\"/\":{\"post\":{\"consumes\":[\"application/json\"],\"responses\":{\"400\":{\"description\":\"Invalid `body`\"},\"201\":{\"schema\":{\"$ref\":\"#/definitions/UserId\"},\"description\":\"\"}},\"produces\":[\"application/json\"],\"parameters\":[{\"required\":true,\"schema\":{\"$ref\":\"#/definitions/User\"},\"in\":\"body\",\"name\":\"body\"}]},\"get\":{\"responses\":{\"200\":{\"schema\":{\"items\":{\"$ref\":\"#/definitions/User\"},\"type\":\"array\"},\"description\":\"\"}},\"produces\":[\"application/json\"]}}}}"

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)
>>> :{
encode $ toSwagger (Proxy :: Proxy UserAPI)
  & applyTagsFor getOps  ["get"  & description ?~ "GET operations"]
  & applyTagsFor postOps ["post" & description ?~ "POST operations"]
:}
"{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"definitions\":{\"User\":{\"required\":[\"name\",\"age\"],\"type\":\"object\",\"properties\":{\"age\":{\"maximum\":9223372036854775807,\"minimum\":-9223372036854775808,\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}},\"UserId\":{\"type\":\"integer\"}},\"paths\":{\"/{user_id}\":{\"get\":{\"responses\":{\"404\":{\"description\":\"`user_id` not found\"},\"200\":{\"schema\":{\"$ref\":\"#/definitions/User\"},\"description\":\"\"}},\"produces\":[\"application/json\"],\"parameters\":[{\"required\":true,\"in\":\"path\",\"name\":\"user_id\",\"type\":\"integer\"}],\"tags\":[\"get\"]}},\"/\":{\"post\":{\"consumes\":[\"application/json\"],\"responses\":{\"400\":{\"description\":\"Invalid `body`\"},\"201\":{\"schema\":{\"$ref\":\"#/definitions/UserId\"},\"description\":\"\"}},\"produces\":[\"application/json\"],\"parameters\":[{\"required\":true,\"schema\":{\"$ref\":\"#/definitions/User\"},\"in\":\"body\",\"name\":\"body\"}],\"tags\":[\"post\"]},\"get\":{\"responses\":{\"200\":{\"schema\":{\"items\":{\"$ref\":\"#/definitions/User\"},\"type\":\"array\"},\"description\":\"\"}},\"produces\":[\"application/json\"],\"tags\":[\"get\"]}}},\"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 Source

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)

Methods

toSwagger :: Proxy api -> Swagger Source

Generate a Swagger specification for a servant API.

Instances

HasSwagger * Raw Source 
(HasSwagger * a, HasSwagger * b) => HasSwagger * ((:<|>) a b) Source 
AllAccept [*] cs => HasSwagger * (Get cs ()) Source 
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs) => HasSwagger * (Get cs (Headers hs a)) Source 
(ToSchema a, AllAccept [*] cs) => HasSwagger * (Get cs a) Source 
AllAccept [*] cs => HasSwagger * (Post cs ()) Source 
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs) => HasSwagger * (Post cs (Headers hs a)) Source 
(ToSchema a, AllAccept [*] cs) => HasSwagger * (Post cs a) Source 
AllAccept [*] cs => HasSwagger * (Delete cs ()) Source 
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs) => HasSwagger * (Delete cs (Headers hs a)) Source 
(ToSchema a, AllAccept [*] cs) => HasSwagger * (Delete cs a) Source 
AllAccept [*] cs => HasSwagger * (Put cs ()) Source 
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs) => HasSwagger * (Put cs (Headers hs a)) Source 
(ToSchema a, AllAccept [*] cs) => HasSwagger * (Put cs a) Source 
AllAccept [*] cs => HasSwagger * (Patch cs ()) Source 
(ToSchema a, AllAccept [*] cs, AllToResponseHeader [*] hs) => HasSwagger * (Patch cs (Headers hs a)) Source 
(ToSchema a, AllAccept [*] cs) => HasSwagger * (Patch cs a) Source 
(ToSchema a, AllAccept [*] cs, HasSwagger k sub) => HasSwagger * ((:>) * k (ReqBody * cs a) sub) Source 
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) * k (Header sym a) sub) Source 
(KnownSymbol sym, HasSwagger k sub) => HasSwagger * ((:>) * k (QueryFlag sym) sub) Source 
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) * k (QueryParams * sym a) sub) Source 
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) * k (QueryParam * sym a) sub) Source 
(KnownSymbol sym, ToParamSchema a, HasSwagger k sub) => HasSwagger * ((:>) * k (Capture * sym a) sub) Source 
(KnownSymbol sym, HasSwagger k sub) => HasSwagger * ((:>) Symbol k sym sub) Source 

Manipulation

subOperations Source

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 Source

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:

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 Source

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.