servant-swagger-1.2: Generate a Swagger/OpenAPI/OAS 2.0 specification for your servant API.
LicenseBSD3
MaintainerNickolay Kudasov <nickolay@getshoptv.com>
Stabilityexperimental
Safe HaskellSafe-Inferred
LanguageHaskell2010

Servant.Swagger

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:

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

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

For some parameters it will also add 400 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 $ orderedKeys $ 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"
:}
{"definitions":{"User":{"properties":{"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"name":{"type":"string"}},"required":["name","age"],"type":"object"},"UserId":{"type":"integer"}},"host":"example.com","info":{"description":"This is an API for the Users service","license":{"name":"MIT"},"title":"User API","version":"1.0"},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}}},"post":{"consumes":["application/json;charset=utf-8"],"parameters":[{"in":"body","name":"body","required":true,"schema":{"$ref":"#/definitions/User"}}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/UserId"}},"400":{"description":"Invalid `body`"}}}},"/{user_id}":{"get":{"parameters":[{"in":"path","name":"user_id","required":true,"type":"integer"}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid `user_id`"}}}}},"swagger":"2.0"}

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

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

Instances details
HasSwagger EmptyAPI Source # 
Instance details

Defined in Servant.Swagger.Internal

HasSwagger Raw Source # 
Instance details

Defined in Servant.Swagger.Internal

(HasSwagger (ToServantApi routes), KnownSymbol datatypeName, Rep (routes AsApi) ~ D1 ('MetaData datatypeName moduleName packageName isNewtype) f) => HasSwagger (NamedRoutes routes :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (NamedRoutes routes) -> Swagger Source #

(HasSwagger a, HasSwagger b) => HasSwagger (a :<|> b :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (a :<|> b) -> Swagger Source #

SwaggerMethod method => HasSwagger (NoContentVerb method :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

HasSwagger sub => HasSwagger (Vault :> sub :: Type) Source #

Vault combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Vault :> sub) -> Swagger Source #

(KnownSymbol sym, HasSwagger sub) => HasSwagger (sym :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (sym :> sub) -> Swagger Source #

HasSwagger sub => HasSwagger (HttpVersion :> sub :: Type) Source #

HttpVersion combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

(KnownSymbol sym, Typeable a, ToParamSchema a, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (Capture' mods sym a :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Capture' mods sym a :> sub) -> Swagger Source #

(KnownSymbol sym, Typeable a, ToParamSchema a, HasSwagger sub) => HasSwagger (CaptureAll sym a :> sub :: Type) Source #

Swagger Spec doesn't have a notion of CaptureAll, this instance is the best effort.

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (CaptureAll sym a :> sub) -> Swagger Source #

(KnownSymbol desc, HasSwagger api) => HasSwagger (Description desc :> api :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Description desc :> api) -> Swagger Source #

(KnownSymbol desc, HasSwagger api) => HasSwagger (Summary desc :> api :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Summary desc :> api) -> Swagger Source #

HasSwagger sub => HasSwagger (Fragment a :> sub :: Type) Source #

Fragment combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Fragment a :> sub) -> Swagger Source #

(KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasSwagger (Header' mods sym a :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Header' mods sym a :> sub) -> Swagger Source #

HasSwagger sub => HasSwagger (IsSecure :> sub :: Type) Source #

IsSecure combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

(KnownSymbol sym, HasSwagger sub) => HasSwagger (QueryFlag sym :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (QueryFlag sym :> sub) -> Swagger Source #

(KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasSwagger (QueryParam' mods sym a :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (QueryParam' mods sym a :> sub) -> Swagger Source #

(KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (QueryParams sym a :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (QueryParams sym a :> sub) -> Swagger Source #

HasSwagger sub => HasSwagger (RemoteHost :> sub :: Type) Source #

RemoteHost combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

(ToSchema a, AllAccept cs, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (ReqBody' mods cs a :> sub :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (ReqBody' mods cs a :> sub) -> Swagger Source #

(ToSchema a, Accept ct, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (StreamBody' mods fr ct a :> sub :: Type) Source #

This instance is an approximation.

Since: 1.1.7

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (StreamBody' mods fr ct a :> sub) -> Swagger Source #

HasSwagger sub => HasSwagger (WithResource res :> sub :: Type) Source #

WithResource combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (WithResource res :> sub) -> Swagger Source #

(KnownNat status, AllAccept cs, SwaggerMethod method, HasSwagger (UVerb method cs as)) => HasSwagger (UVerb method cs (WithStatus status NoContent ': as) :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (UVerb method cs (WithStatus status NoContent ': as)) -> Swagger Source #

(ToSchema a, HasStatus a, AllAccept cs, SwaggerMethod method, HasSwagger (UVerb method cs as)) => HasSwagger (UVerb method cs (a ': as) :: Type) Source #

@since TODO

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (UVerb method cs (a ': as)) -> Swagger Source #

HasSwagger (UVerb method cs ('[] :: [Type]) :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (UVerb method cs '[]) -> Swagger Source #

HasSwagger sub => HasSwagger (WithNamedContext x c sub :: Type) Source #

WithNamedContext combinator does not change our specification at all.

Instance details

Defined in Servant.Swagger.Internal

(AllAccept cs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs NoContent :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Verb method status cs NoContent) -> Swagger Source #

(AllAccept cs, AllToResponseHeader hs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs (Headers hs NoContent) :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Verb method status cs (Headers hs NoContent)) -> Swagger Source #

(ToSchema a, AllAccept cs, AllToResponseHeader hs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs (Headers hs a) :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Verb method status cs (Headers hs a)) -> Swagger Source #

(ToSchema a, AllAccept cs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs a :: Type) Source # 
Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Verb method status cs a) -> Swagger Source #

(ToSchema a, Accept ct, KnownNat status, SwaggerMethod method) => HasSwagger (Stream method status fr ct a :: Type) Source #

Since: 1.1.7

Instance details

Defined in Servant.Swagger.Internal

Methods

toSwagger :: Proxy (Stream method status fr ct a) -> Swagger 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

:: forall proxy api. 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

:: forall proxy api. 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.