| License | BSD3 |
|---|---|
| Maintainer | Nickolay Kudasov <nickolay@getshoptv.com> |
| Stability | experimental |
| Safe Haskell | None |
| Language | Haskell2010 |
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.
- 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
SwaggerIn order to generate specification for a servant API, just use Swagger: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 will generate specification for all API routes, parameters, headers, responses and data schemas.toSwagger
For some parameters it will also add 400 and/or 404 responses with a description mentioning parameter name.
Data schemas come from and ToParamSchema classes.ToSchema
Annotate
While initially generated looks good, it lacks some information it can't get from a servant API.Swagger
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.
provides a convenient way to zoom into a part of an API.subOperations
traverses all operations of the subOperations sub apiapi 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 .
One example is applying tags to certain endpoints:subOperations
>>>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 instances for the types
used in a servant API. But to encode/decode actual data servant uses different classes.
For instance in ToSchemaUserAPI User is always encoded/decoded using and ToJSON instances.FromJSON
To be sure your Haskell server/client handles data properly you need to check
that instance always generates values that satisfy schema produced
by ToJSON instance.ToSchema
With it is possible to test all those instances automatically,
without having to write down every type:validateEveryToJSON
>>>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 in a Swaggerswagger.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 specification.Swagger
See Todo.hs for an example of a server.
HasSwagger class
HasSwaggerclass HasSwagger api where Source
Generate a Swagger specification for a servant API.
To generate Swagger specification, your data types need
and/or ToParamSchema instances.ToSchema
is used for ToParamSchema, Capture and QueryParam.
Header is used for ToSchema and response data types.ReqBody
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)
Instances
Manipulation
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 but ensures that operations
indeed belong to the API at compile time.operationsOf
Testing
Arguments
| :: TMap (Every `[Typeable, Show, Arbitrary, ToJSON, ToSchema]`) (BodyTypes JSON api) | |
| => proxy api | Servant API. |
| -> Spec |
Verify that every type used with content type in a servant API
has compatible JSON and ToJSON instances using ToSchema.validateToJSON
NOTE: does not perform string pattern validation.
See validateEveryToJSON.validateEveryToJSONWithPatternChecker
will produce one validateEveryToJSON specification for every type in the API.
Each type only gets one test, even if it occurs multiple times in the API.prop
>>>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:
andToJSONare used to perform the validation;ToSchemais used to name the test for each type;Typeableis used to display value for whichShowdoes not satisfyToJSON.ToSchemais used to arbitrarily generate values.Arbitrary
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) |
|
| -> proxy api | Servant API. |
| -> Spec |
Verify that every type used with content type in a servant API
has compatible JSON and ToJSON instances using ToSchema.validateToJSONWithPatternChecker
For validation without patterns see .validateEveryToJSON