diff --git a/.gitignore b/.gitignore
index b08434527..777ecd776 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,20 @@
dist/
/foo.json
servant-client-0.4.4/
-.stack-work
+cabal-dev
+*.o
+*.hi
+*.chi
+*.chs.h
+*.dyn_o
+*.dyn_hi
+.virtualenv
+.hpc
+.hsenv
+.cabal-sandbox/
+cabal.sandbox.config
+*.prof
+*.aux
+*.hp
+.stack-work/
+swagger.json
diff --git a/README.md b/README.md
index 20a2529c6..9f165348d 100644
--- a/README.md
+++ b/README.md
@@ -14,73 +14,61 @@
-
+
Given the following `servant` API, `servant-swagger` generates the following json.
### [Input](example/File.hs)
+
```haskell
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE DataKinds #-}
module Main where
-import Servant.API
-import Servant.Server
-import Servant.Swagger
-import Data.Proxy
+import Control.Lens
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL8
+import Data.Proxy
+import Data.Swagger
import GHC.Generics
-import Control.Lens
+import Servant
+import Servant.Swagger
-- Types
-data Todo = Todo {
- created :: Int
+data Todo = Todo
+ { created :: Int
, description :: String
} deriving (Show, Eq, Generic)
instance ToJSON Todo
-instance FromJSON Todo
-newtype TodoId = TodoId String deriving (FromText)
+newtype TodoId = TodoId String deriving (FromText, Generic)
-- API
type API = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
-- Swagger Doc
-swagDoc :: SwaggerAPI
-swagDoc = swagger (Proxy :: Proxy API) mempty (BasePath "/") info schemes Nothing []
- where
- schemes = [ Http ]
- license' = APILicense "MIT" (Just "http://mit.com")
- info =
- Info
- (APITitle "Todo API") (APIVersion "1.0")
- (APIDescription "This is a an API that tests servant-swagger support for a Todo")
- (Just license')
- Nothing
- Nothing
+swagDoc :: Swagger
+swagDoc = toSwagger (Proxy :: Proxy API)
+ & info.infoTitle .~ "Todo API"
+ & info.infoVersion .~ "1.0"
+ & info.infoDescription ?~ "This is an API that tests servant-swagger support for a Todo"
+ & info.infoLicense ?~ License "MIT" (Just (URL "http://mit.com"))
-- Documentation and annotations
-instance ToSwaggerParamType TodoId where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerDescription TodoId where toSwaggerDescription = const "TodoId param"
-
-instance ToSwaggerModel Todo where
- toSwagModel Proxy =
- emptyModel
- & swagModelName .~ ModelName "Todo"
- & swagProperties .~ [ ("created", IntegerSwag)
- , ("description", StringSwag)
- , ("extraTodos", Model $ ModelSwag (ModelName "Todo") False)
- ]
- & swagDescription ?~ Description "This is some real Todo right here"
- & swagModelExample ?~ toJSON (Todo 100 "get milk")
- & swagModelRequired .~ ["description"]
+instance ToParamSchema TodoId
+
+instance ToSchema Todo where
+ declareNamedSchema proxy = do
+ (name, schema) <- genericDeclareNamedSchema defaultSchemaOptions proxy
+ return (name, schema
+ & schemaDescription ?~ "This is some real Todo right here"
+ & schemaExample ?~ toJSON (Todo 100 "get milk"))
-- Main, create swaggger.json
main :: IO ()
@@ -90,94 +78,89 @@ main = BL8.writeFile "swagger.json" (encode swagDoc)
### Output
```json
-{
+{
"swagger":"2.0",
- "basePath":"/",
- "schemes":[
- "http"
- ],
- "info":{
+ "info":{
"version":"1.0",
"title":"Todo API",
- "license":{
+ "license":{
"url":"http://mit.com",
"name":"MIT"
},
- "description":"This is a an API that tests servant-swagger support for a Todo"
+ "description":"This is an API that tests servant-swagger support for a Todo"
},
- "definitions":{
- "Todo":{
- "example":{
+ "definitions":{
+ "Todo":{
+ "example":{
"created":100,
"description":"get milk"
},
- "required":[
+ "required":[
+ "created",
"description"
],
"type":"object",
"description":"This is some real Todo right here",
- "properties":{
- "created":{
- "format":"int32",
+ "properties":{
+ "created":{
+ "maximum":9223372036854775807,
+ "minimum":-9223372036854775808,
"type":"integer"
},
- "description":{
+ "description":{
"type":"string"
- },
- "extraTodos":{
- "$ref":"#/definitions/Todo"
}
}
}
},
- "paths":{
- "/todo/{id}":{
- "get":{
- "summary":"",
- "consumes":[
-
- ],
- "responses":{
- "200":{
- "schema":{
+ "paths":{
+ "/todo/{id}":{
+ "get":{
+ "responses":{
+ "404":{
+ "description":"id not found"
+ },
+ "200":{
+ "schema":{
"$ref":"#/definitions/Todo"
},
- "headers":{
-
- },
- "description":"OK"
+ "description":""
}
},
- "produces":[
+ "produces":[
"application/json"
],
- "parameters":[
- {
+ "parameters":[
+ {
"required":true,
"in":"path",
"name":"id",
- "type":"string",
- "description":"TodoId param"
+ "type":"string"
}
- ],
- "description":"",
- "tags":[
-
]
}
}
- },
- "tags":[
-
- ]
+ }
}
```
+
## Try it out
- - All generated swagger docs can be interactively viewed on Swagger Editor
-## Limitations
- - Quite a few, TODO: add this
+All generated swagger specifications can be interactively viewed on [Swagger Editor](http://editor.swagger.io/).
+
+Ready-to-use specification can be served as JSON and interactive API documentation
+can be displayed using [Swagger UI](https://github.com/swagger-api/swagger-ui).
+
+Many Swagger tools, including server and client code generation for many languages, can be found on
+[Swagger's Tools and Integrations page](http://swagger.io/open-source-integrations/).
## FAQ
- Q: How is this project different from the `swagger` package on `hackage` ?
- A: This package is based on the latest Swagger 2.0 API
+
+## Contributing
+
+We are happy to receive bug reports, fixes, documentation enhancements, and other improvements.
+
+Please report bugs via the [github issue tracker](https://github.com/dmjio/servant-swagger/issues).
+
diff --git a/example/File.hs b/example/File.hs
index 03c7777a9..7e0b67576 100644
--- a/example/File.hs
+++ b/example/File.hs
@@ -1,63 +1,51 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE DataKinds #-}
module Main where
-import Servant.API
-import Servant.Server
-import Servant.Swagger
-import Data.Proxy
+import Control.Lens
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL8
+import Data.Proxy
+import Data.Swagger
import GHC.Generics
-import Control.Lens
+import Servant
+import Servant.Swagger
-- Types
-data Todo = Todo {
- created :: Int
+data Todo = Todo
+ { created :: Int
, description :: String
} deriving (Show, Eq, Generic)
instance ToJSON Todo
-instance FromJSON Todo
-newtype TodoId = TodoId String deriving (FromText)
+newtype TodoId = TodoId String deriving (FromText, Generic)
-- API
type API = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
-- Swagger Doc
-swagDoc :: SwaggerAPI
-swagDoc = swagger (Proxy :: Proxy API) mempty (BasePath "/") info schemes Nothing []
- where
- schemes = [ Http ]
- license' = APILicense "MIT" (Just "http://mit.com")
- info =
- Info
- (APITitle "Todo API") (APIVersion "1.0")
- (APIDescription "This is a an API that tests servant-swagger support for a Todo")
- (Just license')
- Nothing
- Nothing
+swagDoc :: Swagger
+swagDoc = toSwagger (Proxy :: Proxy API)
+ & info.infoTitle .~ "Todo API"
+ & info.infoVersion .~ "1.0"
+ & info.infoDescription ?~ "This is an API that tests servant-swagger support for a Todo"
+ & info.infoLicense ?~ License "MIT" (Just (URL "http://mit.com"))
-- Documentation and annotations
-instance ToSwaggerParamType TodoId where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerDescription TodoId where toSwaggerDescription = const "TodoId param"
-
-instance ToSwaggerModel Todo where
- toSwagModel Proxy =
- emptyModel
- & swagModelName .~ ModelName "Todo"
- & swagProperties .~ [ ("created", IntegerSwag)
- , ("description", StringSwag)
- , ("extraTodos", Model $ ModelSwag (ModelName "Todo") False)
- ]
- & swagDescription ?~ Description "This is some real Todo right here"
- & swagModelExample ?~ toJSON (Todo 100 "get milk")
- & swagModelRequired .~ ["description"]
+instance ToParamSchema TodoId
+
+instance ToSchema Todo where
+ declareNamedSchema proxy = do
+ (name, schema) <- genericDeclareNamedSchema defaultSchemaOptions proxy
+ return (name, schema
+ & schemaDescription ?~ "This is some real Todo right here"
+ & schemaExample ?~ toJSON (Todo 100 "get milk"))
-- Main, create swaggger.json
main :: IO ()
main = BL8.writeFile "swagger.json" (encode swagDoc)
+
diff --git a/example/Server.hs b/example/Server.hs
index 55b780b13..b65420c3c 100644
--- a/example/Server.hs
+++ b/example/Server.hs
@@ -1,43 +1,38 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE DataKinds #-}
module Main where
+import Control.Lens
+import Control.Monad.Trans.Either
+import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL8
-import Servant.API
-import Servant.Swagger
-import Servant.Server
import Data.Proxy
-import Data.Aeson
+import Data.Swagger
import GHC.Generics
import Network.Wai.Handler.Warp
-import Control.Monad.Trans.Either
+import Servant
+import Servant.Swagger
-- Test API
-type TodoAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
- :<|> "todo" :> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] (Maybe Todo)
- :<|> "todo" :> "count" :> Get '[JSON] Todo
- :<|> "todo" :> ReqBody '[JSON] Todo :> Post '[JSON] Todo
+type TodoAPI
+ = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
+ :<|> "todo" :> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] (Maybe Todo)
+ :<|> "todo" :> "count" :> Get '[JSON] Todo
+ :<|> "todo" :> ReqBody '[JSON] Todo :> Post '[JSON] Todo
type TestAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
-swagDoc :: SwaggerAPI
-swagDoc = swagger (Proxy :: Proxy TestAPI) mempty (BasePath "/") info schemes Nothing []
- where
- schemes = [ Http ]
- license' = APILicense "MIT" (Just "http://mit.com")
- info =
- Info
- (APITitle "Todo API") (APIVersion "1.0")
- (APIDescription "This is a an API that tests servant-swagger support for a Todo API")
- (Just license')
- Nothing
- Nothing
-
+swagDoc :: Swagger
+swagDoc = toSwagger (Proxy :: Proxy TestAPI)
+ & info.infoTitle .~ "Todo API"
+ & info.infoVersion .~ "1.0"
+ & info.infoDescription ?~ "This is an API that tests servant-swagger support for a Todo"
+ & info.infoLicense ?~ License "MIT" (Just (URL "http://mit.com"))
-type DocsAPI = Get '[JSON] SwaggerAPI
+type DocsAPI = Get '[JSON] Swagger
type API = DocsAPI :<|> TodoAPI
@@ -48,9 +43,9 @@ data Todo = Todo { created :: Int, description :: String }
instance ToJSON Todo
instance FromJSON Todo
-newtype TodoId = TodoId String deriving (FromText)
-newtype TodoCount = TodoCount Int deriving (FromText)
-newtype Completed = Completed Bool deriving (FromText)
+newtype TodoId = TodoId String deriving (FromText, Generic)
+newtype TodoCount = TodoCount Int deriving (FromText, Generic)
+newtype Completed = Completed Bool deriving (FromText, Generic)
api :: Proxy TodoAPI
api = Proxy
@@ -65,40 +60,22 @@ main = do
undefined :<|> undefined :<|> undefined
undefined :<|> undefined
-swagHandler :: EitherT ServantErr IO SwaggerAPI
-swagHandler = pure $ swagger api mempty (BasePath "/") info schemes Nothing []
- where
- schemes = [ Http ]
- license' = APILicense "MIT" (Just "http://mit.com")
- info =
- Info
- (APITitle "Servant Swagger API") (APIVersion "2.0")
- (APIDescription "This is a an API that tests swagger integration")
- (Just license')
- Nothing
- Nothing
+swagHandler :: EitherT ServantErr IO Swagger
+swagHandler = pure $ toSwagger api
+ & info.infoTitle .~ "Todo API"
+ & info.infoVersion .~ "1.0"
+ & info.infoDescription ?~ "This is an API that tests swagger integration"
+ & info.infoLicense ?~ License "MIT" (Just (URL "http://mit.com"))
-- Instances
-instance ToSwaggerModel Todo where
- toSwagModel Proxy =
- SwaggerModel {
- _swagModelName = (ModelName "Todo")
- , _swagProperties = [ ("created", IntegerSwag)
- , ("description", StringSwag)
- ]
- , _swagDescription = Just $ Description "This is some real Todo right here"
- , _swagModelExample = Just $ toJSON $ Todo 100 "get milk"
- , _swagModelRequired = ["description"]
- }
-
-instance ToSwaggerParamType TodoId where
- toSwaggerParamType = const StringSwagParam
-
-instance ToSwaggerParamType Completed where
- toSwaggerParamType = const BooleanSwagParam
-
-instance ToSwaggerDescription TodoId where
- toSwaggerDescription = const "TodoId param"
-
-instance ToSwaggerDescription Completed where
- toSwaggerDescription = const "Completed param"
+instance ToSchema Todo where
+ declareNamedSchema proxy = do
+ (name, schema) <- genericDeclareNamedSchema defaultSchemaOptions proxy
+ return (name, schema
+ & schemaDescription ?~ "This is some real Todo right here"
+ & schemaExample ?~ toJSON (Todo 100 "get milk"))
+
+instance ToParamSchema TodoId
+
+instance ToParamSchema Completed
+
diff --git a/example/example.cabal b/example/example.cabal
index e35875e7a..23b631627 100644
--- a/example/example.cabal
+++ b/example/example.cabal
@@ -21,6 +21,7 @@ executable swagger-server
, either
, servant-server
, servant-swagger
+ , swagger2
, lens
, wai
, warp
@@ -33,6 +34,7 @@ executable swagger-file
, aeson
, bytestring == 0.10.*
, servant
+ , swagger2
, either
, servant-server
, servant-swagger
diff --git a/servant-swagger.cabal b/servant-swagger.cabal
index 7ce3caae9..a1b83bf83 100644
--- a/servant-swagger.cabal
+++ b/servant-swagger.cabal
@@ -25,30 +25,34 @@ library
exposed-modules:
Servant.Swagger
Servant.Swagger.Internal
- default-extensions:
- FlexibleContexts
- , DeriveGeneric
- , DeriveDataTypeable
- , GeneralizedNewtypeDeriving
- , RecordWildCards
- , FlexibleInstances
- , OverloadedLists
- , PolyKinds
- , OverloadedStrings
- , TypeFamilies
- , KindSignatures
- , TypeOperators
- , CPP
- , DataKinds
hs-source-dirs: src
build-depends: aeson
, base >=4.7 && <5
, bytestring
- , containers
- , hashable
+ , http-media
, lens
- , text
- , uuid
, servant
+ , swagger2
+ , text
, unordered-containers
default-language: Haskell2010
+
+test-suite spec
+ ghc-options: -Wall
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: Spec.hs
+ build-depends: base == 4.*
+ , aeson
+ , aeson-qq
+ , hspec
+ , lens
+ , servant
+ , servant-swagger
+ , swagger2
+ , text
+ , time
+ other-modules:
+ Servant.SwaggerSpec
+ default-language: Haskell2010
+
diff --git a/src/Servant/Swagger.hs b/src/Servant/Swagger.hs
index 237c2a1f7..f5765ea52 100644
--- a/src/Servant/Swagger.hs
+++ b/src/Servant/Swagger.hs
@@ -1,104 +1,14 @@
-module Servant.Swagger
- (
- -- * Classes
- HasSwagger (..)
- , ToSwaggerDescription (..)
- , ToHeader (..)
- , ToSwaggerParamType (..)
- , SwaggerParamType (..)
- , ToSwaggerModel (..)
- , ToHeaderDescription (..)
- , ToModelExample (..)
- -- * Types
- , APIDescription (..)
- , Contact (..)
- , ContactName (..)
- , ContactURL (..)
- , ContactEmail (..)
- , TermsOfService (..)
- , SwaggerAPI (..)
- , SwaggerOperation (..)
- , SwaggerRouteInfo (..)
- , Operation (..)
- , Code (..)
- , Verb (..)
- , PathSummary (..)
- , SwaggerType (..)
- , SwaggerModel (..)
- , Info (..)
- , ModelName (..)
- , ContentType (..)
- , APIVersion (..)
- , APITitle (..)
- , APILicense (..)
- , Scheme (..)
- , Description (..)
- , BasePath (..)
- , Response (..)
- , ModelSwag (..)
- , SwaggerHeader (..)
- , responseDescription
- , responseModelName
- , responseHeaders
- , responseIsArray
- , responseCode
- , defResponse
- , Tag (..)
- , TagName (..)
- , TagDescription (..)
- , tagName
- , tagDescription
- -- * Swaggadelic
- , swagger
- , emptyModel
- , swaggerPathInfo
- , emptyRouteDescription
- -- * Lenses
- , swagModelName
- , swagModelExample
- , swagProperties
- , swagModelRequired
- , swagDescription
- , swagRouteTags
- , swagRouteSummary
- , swagRouteResponses
- , swagRouteModels
- , PathDescription (..)
- , swagRouteDescription
- , OperationId (..)
- , swagRouteOperationId
- , defSwaggerInfo
- , createSwaggerJson
- , swaggerInfoTitle
- , swaggerVersion
- , swaggerAPIDescription
- , license
- , contact
- , termsOfService
- ) where
+module Servant.Swagger (
+ HasSwagger(..),
+
+ addTag,
+ subOperations,
+ setResponse,
+
+ ToResponseHeader(..),
+ AllAccept,
+ AllToResponseHeader,
+) where
import Servant.Swagger.Internal
-import Data.Proxy
-import Data.Monoid
-import Control.Lens
-swagger
- :: HasSwagger swagger
- => Proxy swagger
- -> SwaggerRouteInfo swagger
- -> BasePath
- -> Info
- -> [Scheme]
- -> Maybe HostName
- -> [SecurityDefinition]
- -> SwaggerAPI
-swagger proxy (SwaggerRouteInfo routeInfo) basePath info schemes hostName secDefs = do
- let result@SwagResult{..} = routeInfo <> toSwaggerDocs proxy defSwaggerRoute
- defSwaggerAPI info
- & swaggerPaths .~ _resultPaths
- & swaggerSchemes ?~ schemes
- & swaggerDefinitions .~ _resultModels
- & swaggerTags ?~ getAllTags result
- & swaggerBasePath ?~ basePath
- & swaggerHostName .~ hostName
- & swaggerSecurityDefintions ?~ secDefs
diff --git a/src/Servant/Swagger/Internal.hs b/src/Servant/Swagger/Internal.hs
index 566386651..fbff12bbe 100644
--- a/src/Servant/Swagger/Internal.hs
+++ b/src/Servant/Swagger/Internal.hs
@@ -1,985 +1,362 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE DefaultSignatures #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DeriveDataTypeable #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE OverloadedLists #-}
-{-# LANGUAGE PolyKinds #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE KindSignatures #-}
-{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ConstraintKinds #-}
-#if !MIN_VERSION_base(4,8,0)
-{-# LANGUAGE OverlappingInstances #-}
-#endif
-------------------------------------------------------------------------------
-module Servant.Swagger.Internal where
-------------------------------------------------------------------------------
-#if !MIN_VERSION_base(4,8,0)
-import Control.Applicative
-#endif
-import Data.Text (Text)
-import Data.Aeson
-import Data.Aeson.Types ( typeMismatch )
-import qualified Data.Set as S
-import Data.Hashable
-import Data.List
-import Data.Maybe
-import Data.Data
-import qualified Data.ByteString.Lazy.Char8 as BL8
-import qualified Data.ByteString.Char8 as B8
-import Data.Bool
-import GHC.Generics
-import Data.String
-import Control.Lens hiding ((.=))
-import qualified Data.HashMap.Strict as HM
-import Data.Monoid
-import qualified Data.Text as T
-import qualified Data.Text.Lazy as L
-import GHC.TypeLits
-import Servant.API hiding (Header)
-import qualified Servant.API.Header as H
-import qualified Data.UUID as UUID
-
--- | Helper to generate swagger.json file
-createSwaggerJson :: SwaggerAPI -> IO ()
-createSwaggerJson = BL8.writeFile "swagger.json" . encode
-
--- | This is the root document object for the API specification.
-data SwaggerAPI = SwaggerAPI {
- _swaggerInfo :: Info -- ^ Required, provides metadata about the API
- , _swaggerPaths :: HM.HashMap PathName SwaggerOperation -- ^ Required
- , _swaggerSchemes :: Maybe [Scheme] -- ^ Schemes for this API (i.e HTTP/HTTPS)
- , _swaggerDefinitions :: HM.HashMap ModelName SwaggerModel -- ^
- , _swaggerTags :: Maybe [Tag] -- ^ A list of tags that provide additional metadat
- , _swaggerBasePath :: Maybe BasePath -- ^ The base path upon which this API is served
- , _swaggerHostName :: Maybe HostName -- ^ Host name or IP
- , _swaggerSecurityDefintions :: Maybe [SecurityDefinition]
- -- ^ Security scheme definitions that can be used across the specification.
- , _swaggerExternalDocs :: Maybe ExternalDocs
- } deriving Show
-
--- | Smart Constructor for `SwaggerAPI`
-defSwaggerAPI :: Info -> SwaggerAPI
-defSwaggerAPI info = SwaggerAPI info mempty mempty mempty mempty mempty mempty mempty mempty
-
--- | `ToJSON` for `SwaggerAPI`
-instance ToJSON SwaggerAPI where
- toJSON SwaggerAPI{..} =
- object $ [
- "swagger" .= ("2.0" :: Text)
- , "info" .= _swaggerInfo
- , "paths" .= do Object $ HM.fromList $ map f $ HM.toList _swaggerPaths
- , "definitions" .= do Object $ HM.fromList $ map g $ HM.toList _swaggerDefinitions
- ] ++
- [ "host" .= _swaggerHostName | isJust _swaggerHostName ] ++
- [ "schemes" .= _swaggerSchemes | isJust _swaggerSchemes ] ++
- [ "basePath" .= _swaggerBasePath | isJust _swaggerBasePath ] ++
- [ "externalDocs" .= _swaggerExternalDocs | isJust _swaggerExternalDocs ] ++
- [ "tags" .= _swaggerTags | isJust _swaggerTags ]
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+module Servant.Swagger.Internal where
+
+import Control.Arrow (first)
+import Control.Lens
+import Data.Aeson
+import Data.Data.Lens (template)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Data.List (dropWhileEnd)
+import Data.Monoid
+import Data.Proxy
+import qualified Data.Swagger as Swagger
+import Data.Swagger hiding (Header)
+import Data.Swagger.Declare
+import Data.Text (Text)
+import qualified Data.Text as Text
+import GHC.TypeLits
+import GHC.Exts
+import Network.HTTP.Media (MediaType)
+import Servant.API
+
+class HasSwagger api where
+ toSwagger :: Proxy api -> Swagger
+
+instance HasSwagger Raw where
+ toSwagger _ = mempty & paths.pathsMap.at "/" ?~ mempty
+
+-- | All operations of sub API.
+subOperations :: forall sub api. (IsSubAPI sub api, HasSwagger sub) =>
+ Proxy sub -> Proxy api -> Traversal' Swagger Operation
+subOperations sub _ = paths.pathsMap.itraversed.indices (`elem` ps).template
+ where
+ ps = toSwagger sub ^. paths.pathsMap.to HashMap.keys
+
+-- | Tag an operation.
+addTag :: TagName -> Operation -> Operation
+addTag tag = operationTags %~ (tag:)
+
+-- | Set a response for an operation.
+setResponse :: HttpStatusCode -> Response -> Operation -> Operation
+setResponse code res = operationResponses.responsesResponses.at code ?~ Inline res
+
+(>) :: FilePath -> FilePath -> FilePath
+x > y = case trim y of
+ "" -> "/" <> trim x
+ y' -> "/" <> trim x <> "/" <> y'
+ where
+ trim = dropWhile (== '/') . dropWhileEnd (== '/')
+
+mkEndpoint :: forall a cs hs proxy _verb. (ToSchema a, AllAccept cs, AllToResponseHeader hs)
+ => FilePath
+ -> Lens' PathItem (Maybe Operation)
+ -> HttpStatusCode
+ -> proxy (_verb cs (Headers hs a))
+ -> Swagger
+mkEndpoint path verb code proxy
+ = mkEndpointWithSchemaRef (Just ref) path verb code proxy
+ & definitions .~ defs
+ where
+ (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty
+
+noContentEndpoint :: forall cs proxy verb. (AllAccept cs)
+ => FilePath
+ -> Lens' PathItem (Maybe Operation)
+ -> proxy (verb cs ())
+ -> Swagger
+noContentEndpoint path verb _ = mkEndpointWithSchemaRef Nothing path verb 204 (Proxy :: Proxy (verb cs (Headers '[] ())))
+
+mkEndpointWithSchemaRef :: forall cs hs proxy verb a. (AllAccept cs, AllToResponseHeader hs)
+ => Maybe (Referenced Schema)
+ -> FilePath
+ -> Lens' PathItem (Maybe Operation)
+ -> HttpStatusCode
+ -> proxy (verb cs (Headers hs a))
+ -> Swagger
+mkEndpointWithSchemaRef mref path verb code _ = mempty
+ & paths.pathsMap.at path ?~
+ (mempty & verb ?~ (mempty
+ & operationProduces ?~ MimeList (allContentType (Proxy :: Proxy cs))
+ & operationResponses .~ (mempty
+ & responsesResponses . at code ?~ Inline (mempty
+ & responseSchema .~ mref
+ & responseHeaders .~ toAllResponseHeaders (Proxy :: Proxy hs)))))
+
+-- | Prepend path to all API endpoints.
+prependPath :: FilePath -> Swagger -> Swagger
+prependPath path spec = spec & paths.pathsMap %~ f
+ where
+ f = HashMap.fromList . map (first (path >)) . HashMap.toList
+
+-- | Add parameter to every operation in the spec.
+addParam :: Param -> Swagger -> Swagger
+addParam param spec = spec & template.operationParameters %~ (Inline param :)
+
+-- | Add accepted content types to every operation in the spec.
+addConsumes :: [MediaType] -> Swagger -> Swagger
+addConsumes cs spec = spec & template.operationConsumes %~ (<> Just (MimeList cs))
+
+-- | Add/modify response for every operation in the spec.
+addResponseWith :: (Response -> Response -> Response) -> HttpStatusCode -> Response -> Swagger -> Swagger
+addResponseWith f code new spec = spec
+ & paths.template.responsesResponses . at code %~ Just . Inline . combine
+ where
+ combine (Just (Ref (Reference name))) = case spec ^. responses.at name of
+ Just old -> f old new
+ Nothing -> new -- FIXME: what is the right choice here?
+ combine (Just (Inline old)) = f old new
+ combine Nothing = new
+
+-- | Add/overwrite response for every operation in the spec.
+addResponse :: HttpStatusCode -> Response -> Swagger -> Swagger
+addResponse = addResponseWith (\_old new -> new)
+
+addDefaultResponse404 :: ParamName -> Swagger -> Swagger
+addDefaultResponse404 name = addResponseWith (\old _new -> alter404 old) 404 response404
+ where
+ description404 = name <> " not found"
+ alter404 = description %~ ((name <> " or ") <>)
+ response404 = mempty & description .~ description404
+
+addDefaultResponse400 :: ParamName -> Swagger -> Swagger
+addDefaultResponse400 name = addResponseWith (\old _new -> alter400 old) 400 response400
+ where
+ description400 = "Invalid " <> name
+ alter400 = description %~ (<> (" or " <> name))
+ response400 = mempty & description .~ description400
+
+-- -----------------------------------------------------------------------
+-- DELETE
+-- -----------------------------------------------------------------------
+
+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs) => HasSwagger (Delete cs a) where
+ toSwagger _ = toSwagger (Proxy :: Proxy (Delete cs (Headers '[] a)))
+
+instance (ToSchema a, AllAccept cs, AllToResponseHeader hs) => HasSwagger (Delete cs (Headers hs a)) where
+ toSwagger = mkEndpoint "/" pathItemDelete 200
+
+instance AllAccept cs => HasSwagger (Delete cs ()) where
+ toSwagger = noContentEndpoint "/" pathItemDelete
+
+-- -----------------------------------------------------------------------
+-- GET
+-- -----------------------------------------------------------------------
+
+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs) => HasSwagger (Get cs a) where
+ toSwagger _ = toSwagger (Proxy :: Proxy (Get cs (Headers '[] a)))
+
+instance (ToSchema a, AllAccept cs, AllToResponseHeader hs) => HasSwagger (Get cs (Headers hs a)) where
+ toSwagger = mkEndpoint "/" pathItemGet 200
+
+instance AllAccept cs => HasSwagger (Get cs ()) where
+ toSwagger = noContentEndpoint "/" pathItemGet
+
+-- -----------------------------------------------------------------------
+-- PATCH
+-- -----------------------------------------------------------------------
+
+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs) => HasSwagger (Patch cs a) where
+ toSwagger _ = toSwagger (Proxy :: Proxy (Patch cs (Headers '[] a)))
+
+instance (ToSchema a, AllAccept cs, AllToResponseHeader hs) => HasSwagger (Patch cs (Headers hs a)) where
+ toSwagger = mkEndpoint "/" pathItemPatch 200
+
+instance AllAccept cs => HasSwagger (Patch cs ()) where
+ toSwagger = noContentEndpoint "/" pathItemPatch
+
+-- -----------------------------------------------------------------------
+-- PUT
+-- -----------------------------------------------------------------------
+
+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs) => HasSwagger (Put cs a) where
+ toSwagger _ = toSwagger (Proxy :: Proxy (Put cs (Headers '[] a)))
+
+instance (ToSchema a, AllAccept cs, AllToResponseHeader hs) => HasSwagger (Put cs (Headers hs a)) where
+ toSwagger = mkEndpoint "/" pathItemPut 200
+
+instance AllAccept cs => HasSwagger (Put cs ()) where
+ toSwagger = noContentEndpoint "/" pathItemPut
+
+-- -----------------------------------------------------------------------
+-- POST
+-- -----------------------------------------------------------------------
+
+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs) => HasSwagger (Post cs a) where
+ toSwagger _ = toSwagger (Proxy :: Proxy (Post cs (Headers '[] a)))
+
+instance (ToSchema a, AllAccept cs, AllToResponseHeader hs) => HasSwagger (Post cs (Headers hs a)) where
+ toSwagger = mkEndpoint "/" pathItemPost 201
+
+instance AllAccept cs => HasSwagger (Post cs ()) where
+ toSwagger = noContentEndpoint "/" pathItemPost
+
+
+instance (HasSwagger a, HasSwagger b) => HasSwagger (a :<|> b) where
+ toSwagger _ = toSwagger (Proxy :: Proxy a) <> toSwagger (Proxy :: Proxy b)
+
+instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (sym :> sub) where
+ toSwagger _ = prependPath piece (toSwagger (Proxy :: Proxy sub))
where
- f (PathName pathName, sp) = (T.toLower pathName, toJSON sp)
- g (ModelName modelName, model) = (modelName, toJSON model)
-
-data ExternalDocs = ExternalDocs {
- _externalDescription :: Maybe ExternalDescription -- ^ Optional Description
- , _externalURL :: ExternalURL -- ^ Required URL that links supporting documentation
- } deriving (Show, Eq)
-
-instance ToJSON ExternalDocs where
- toJSON ExternalDocs{..} =
- object $ [ "url" .= _externalURL ] ++
- [ "description" .= _externalDescription
- | isJust _externalDescription
- ]
-
-instance Monoid ExternalDocs where
- mempty = ExternalDocs mempty mempty
- (ExternalDocs a1 b1) `mappend` (ExternalDocs a2 b2)
- = ExternalDocs (a1 <> a2) (b1 <> b2)
-
--- | External Description for External Documentation API
-newtype ExternalDescription = ExternalDescription Text
- deriving (Show, Eq, Monoid, ToJSON)
-
--- | External Description for External Documentation API
-newtype ExternalURL = ExternalURL Text
- deriving (Show, Eq, Monoid, ToJSON)
-
--- | Type used to accumulate information of a Servant path
-data SwaggerRoute = SwaggerRoute {
- _routePathName :: PathName -- ^ Accumulated `PathName`
- , _routeConsumes :: [ContentType] -- ^ Content Types a route consumes
- , _routeModels :: HM.HashMap ModelName SwaggerModel -- ^ Models present in route
- , _routeParams :: [Param] -- ^ Params present in route
- , _routeVerb :: Verb -- ^ Verb of route
- , _routePathSummary :: PathSummary -- ^ Summary of path
- , _routeResponses :: HM.HashMap Text Response -- ^ Additional responses for a Route
- , _routeTags :: [Tag] -- ^ Tags present for this Route
- } deriving Show
-
--- | Default Route used to build up
-defSwaggerRoute :: SwaggerRoute
-defSwaggerRoute = SwaggerRoute mempty [] [] [] Get mempty [] []
-
--- | Default
---
-defSwaggerInfo :: Info
-defSwaggerInfo =
- Info (APITitle mempty)
- (APIVersion "2.0") (APIDescription mempty) Nothing Nothing Nothing
-
--- | Contact name of `Contact` object
-newtype ContactName = ContactName Text
- deriving (Show, Eq, ToJSON, FromJSON, Ord, Monoid)
-
--- | Contact URL of `Contact` object
-newtype ContactURL = ContactURL Text
- deriving (Show, Eq, ToJSON, FromJSON, Ord, Monoid)
-
--- | Contact Email of `Contact` object
-newtype ContactEmail = ContactEmail Text
- deriving (Show, Eq, ToJSON, FromJSON, Ord, Monoid)
-
--- | Contact Object
-data Contact = Contact {
- _contactName :: ContactName
- , _contactURL :: ContactURL
- , _contactEmail :: ContactEmail
- } deriving (Show, Eq, Ord)
-
-instance Monoid Contact where
- mempty = Contact mempty mempty mempty
- (Contact a1 b1 c1) `mappend` (Contact a2 b2 c2) =
- Contact (a1 <> a2) (b1 <> b2) (c1 <> c2)
-
--- | Contact Object
-instance ToJSON Contact where
- toJSON Contact{..} =
- object [
- "name" .= _contactName
- , "url" .= _contactURL
- , "email" .= _contactEmail
- ]
-
--- | Description of API
-newtype APIDescription = APIDescription { _unApiDesc :: Text }
- deriving (Show, Eq, ToJSON, Monoid)
-
--- | Terms of Service of API located in `Info`
-newtype TermsOfService = TermsOfService Text
- deriving (Show, Eq, ToJSON, Monoid)
-
--- | A Swagger metadata for a Servant header
-data SwaggerHeader = SwaggerHeader {
- headerDescription :: Text -- ^ Header description
- , headerType :: SwaggerParamType -- ^ Type of Header
- , headerName :: Text -- ^ Name of Header
- } deriving (Show, Eq)
-
--- | A container for the expected responses of an operation.
-data Response = Response {
- _responseDescription :: Text -- ^ Description of Response
- , _responseModelName :: ModelName -- ^ `Model` this Response returns
- , _responseHeaders :: HM.HashMap Text SwaggerHeader -- ^ HashMap of headers
- , _responseIsArray :: Bool -- ^ Does the response return an Array?
- , _responseCode :: Code -- ^ Response code this route returns
- } deriving (Show, Eq)
-
--- | Default Response for a Path
-defResponse :: Response
-defResponse = Response mempty (ModelName mempty) mempty False (Code 200)
-
--- | Name of `Tag`, that can be applied to an operation
-newtype TagName = TagName Text deriving (Show, Eq, Ord, ToJSON, FromJSON)
-
--- | Description of `Tag`
-newtype TagDescription = TagDescription Text deriving (Show, Eq, Ord, ToJSON, FromJSON)
-
--- | Allows adding meta data to a single tag that is used by the Operation Object
-data Tag = Tag {
- _tagName :: TagName -- ^ Name of `Tag`
- , _tagDescription :: TagDescription -- ^ Description of `Tag`
- } deriving (Show, Eq, Ord)
-
--- | `ToJSON` `Tag` instance
-instance ToJSON Tag where
- toJSON Tag{..} = object [ "name" .= _tagName, "description" .= _tagDescription ]
-
--- | `FromJSON` `Tag` instance
-instance FromJSON Tag where
- parseJSON (Object o) = Tag <$> o .: "name" <*> o .: "description"
- parseJSON x = typeMismatch "Tag" x
-
--- | A declaration of the security schemes available to be used in the specification
-data SecurityDefinition =
- OAuthDef OAuth -- ^ OAuth
- | APIKeyDef APIKey -- ^ APIKey
- | BasicAuthDef BasicAuth -- ^ BasicAuth
- deriving Show
-
--- | Basic Authentication
-data BasicAuth = BasicAuth deriving Show
-
--- ^ OAuth Flow
-data OAuthFlow = Implicit | Password | Application | AccessCode deriving (Show)
--- ^ OAuth URL
-data OAuthURL = OAuthURL deriving (Show)
-data TokenURL = TokenURL deriving (Show)
-data Scopes = Scopes deriving (Show)
-
--- | OAuth Authentication
-data OAuth = OAuth {
- oauthDescription :: Maybe Description -- ^ Optional description for Swagger OAuth object
- , oauthFlow :: OAuthFlow -- ^ OAuth Flow
- , oauthURL :: OAuthURL -- ^ OAuth URL
- , oauthTokenURL :: TokenURL -- ^ Token URL
- , scopes :: Scopes -- ^ OAuth Scopes
- } deriving Show
-
--- | API Key
-newtype APIKeyName =
- APIKeyName Text deriving (Show, Eq)
-
--- | API Location (i.e. is it located in a Query Param or a Header)
-data APIKeyIn = APIKeyQueryParam
- | APIKeyHeader deriving (Show, Eq)
-
--- | API Key Object
-data APIKey = APIKey {
- apiKeyDescription :: Maybe Description -- ^ Optional `Description`
- , apiKeyName :: APIKeyName -- ^ Optional `Description`
- , apiKeyIn :: APIKeyIn
- } deriving Show
-
--- | API Key `ToJSON`
-instance ToJSON APIKey where
- toJSON APIKey{..} =
- object [ "api_key" .= ([] :: [Int]) ]
-
--- | HostName
-newtype HostName = HostName Text
- deriving (Show, Eq, IsString, ToJSON, FromJSON, Monoid)
-
--- | BasePath
-newtype BasePath = BasePath Text
- deriving (Show, Eq, ToJSON, FromJSON, Monoid)
-
--- | Info Objet
-data Info = Info {
- _swaggerInfoTitle :: APITitle -- ^ API Title
- , _swaggerVersion :: APIVersion -- ^ API Version
- , _swaggerAPIDescription :: APIDescription -- ^ API Description
- , _license :: Maybe APILicense -- ^ API Description
- , _contact :: Maybe Contact
- , _termsOfService :: Maybe TermsOfService
- } deriving (Show, Eq)
-
-instance Monoid Info where
- mempty = Info mempty mempty mempty mempty mempty mempty
- (Info a1 b1 c1 d1 e1 f1) `mappend` (Info a2 b2 c2 d2 e2 f2)
- = Info (a1 <> a2) (b1 <> b2) (c1 <> c2) (d1 <> d2) (e1 <> e2) (f1 <> f2)
-
-data APILicense = APILicense {
- _licenseName :: Text
- , _licenseUrl :: Maybe Text
- } deriving (Show, Eq)
-
-instance Monoid APILicense where
- mempty = APILicense mempty mempty
- (APILicense a1 b1) `mappend` (APILicense a2 b2) = APILicense (a1 <> a2) (b1 <> b2)
-
-data SwaggerOperation = SwaggerOperation {
- _paths :: HM.HashMap Verb Operation
- } deriving Show
-
-data SwagResult = SwagResult {
- _resultPaths :: HM.HashMap PathName SwaggerOperation
- , _resultModels :: HM.HashMap ModelName SwaggerModel
- } deriving (Show)
-
-data Verb = Post | Get | Put | Options | Head | Delete | Patch
- deriving (Show, Eq, Read, Generic)
-
-newtype PathSummary = PathSummary Text
- deriving (Show, Eq, ToJSON, FromJSON, Monoid, IsString)
-
-data Operation = Operation {
- _summary :: PathSummary
- , _params :: [Param]
- , _responses :: HM.HashMap Code Response
- , _produces :: [ContentType]
- , _consumes :: [ContentType]
- , _tags :: [Tag]
- , _operationId :: Maybe OperationId
- , _description :: PathDescription
- , _deprecated :: Maybe Deprecated
- } deriving Show
-
-instance Monoid Operation where
- mempty = Operation mempty mempty mempty mempty mempty mempty mempty mempty mempty
- (Operation a1 b1 c1 d1 e1 f1 g1 h1 i1) `mappend`
- (Operation a2 b2 c2 d2 e2 f2 g2 h2 i2) =
- Operation (a1 <> a2) (b1 <> b2) (c1 <> c2) (d1 <> d2) (e1 <> e2) (f1 <> f2) (g1 <> g2)
- (h1 <> h2) (i1 <> i2)
-
-newtype Deprecated = Deprecated Bool deriving (Show, Eq, ToJSON)
-
-instance Monoid Deprecated where
- mempty = Deprecated False
- (Deprecated False) `mappend` (Deprecated False) = Deprecated False
- _ `mappend` _ = Deprecated True
-
-newtype OperationId = OperationId Text deriving (Show, Eq, ToJSON, Monoid)
-newtype PathDescription = PathDescription Text deriving (Show, Eq, ToJSON, Monoid)
-
-newtype Code = Code Int deriving (Show, Eq, Ord, ToJSON, Hashable, Num)
-
-data SwaggerParamType =
- StringSwagParam
- | NumberSwagParam
- | IntegerSwagParam
- | BooleanSwagParam
- | ArraySwagParam
- | FileSwagParam
- deriving (Show, Eq)
-
-data SwaggerType =
- IntegerSwag
- | LongSwag
- | FloatSwag
- | DoubleSwag
- | StringSwag
- | ByteSwag
- | BinarySwag
- | BooleanSwag
- | DateSwag
- | DateTimeSwag
- | PasswordSwag
- | Model ModelSwag
- deriving (Show, Eq)
-
-data ModelSwag = ModelSwag {
- modelSwagName :: ModelName
- , modelSwagIsArray :: Bool
- } deriving (Show, Eq)
-
-data ContentType = JSON | HTML | XML | FormUrlEncoded | PlainText | OctetStream
- deriving (Show, Eq)
-
-data In = PathUrl | Query | Header | FormData | Body deriving Show
-data Scheme = Http | Https | Ws | Wss deriving Show
-
-data Param = Param {
- _in :: In
- , _name :: Text
- , _type :: Maybe SwaggerParamType
- , _items :: Maybe ItemObject
- , _paramDescription :: Text
- , _allowEmptyValue :: Bool
- , _required :: Bool
- , _default :: Maybe Value
- , _isArray :: Bool
- } deriving Show
-
-data ItemObject = ItemObject {
- _itemsType :: SwaggerParamType
- } deriving Show
-
-newtype APIVersion = APIVersion Text deriving (Show, Eq, ToJSON, Monoid)
-newtype APITitle = APITitle Text deriving (Show, Eq, ToJSON, Monoid)
-newtype PathName = PathName { unPathName :: Text }
- deriving (Show, Eq, Hashable, Monoid)
-
-newtype ModelName = ModelName { unModelName :: Text }
- deriving (Show, Eq, Hashable, Monoid)
-
-newtype Description =
- Description { unDescription :: Text } deriving (Show, Eq, ToJSON, Monoid)
-
-data SwaggerModel = SwaggerModel {
- _swagModelName :: ModelName
- , _swagProperties :: [(Text, SwaggerType)]
- , _swagDescription :: Maybe Description
- , _swagModelExample :: Maybe Value
- , _swagModelRequired :: [Text]
- } deriving (Show, Eq)
-
-emptyModel :: SwaggerModel
-emptyModel = SwaggerModel (ModelName mempty) mempty mempty Nothing mempty
-
-data SwaggerRouteDescription = SwaggerRouteDescription {
- _swagRouteTags :: [Tag] -- ^ Tags
- , _swagRouteSummary :: PathSummary -- ^ Description of this endpoint
- , _swagRouteResponses :: HM.HashMap Code Response -- ^ Additional responses for this endpoint
- , _swagRouteModels :: HM.HashMap ModelName SwaggerModel
- , _swagRouteOperationId :: Maybe OperationId
- , _swagRouteDescription :: PathDescription
- } deriving Show
-
-emptyRouteDescription :: SwaggerRouteDescription
-emptyRouteDescription = SwaggerRouteDescription mempty mempty mempty mempty mempty mempty
-
-$(makeLenses ''SwaggerModel)
-$(makeLenses ''ExternalDocs)
-$(makeLenses ''SwaggerRouteDescription)
-$(makeLenses ''SwagResult)
-$(makeLenses ''SwaggerRoute)
-$(makeLenses ''SwaggerAPI)
-$(makeLenses ''Info)
-$(makeLenses ''Contact)
-$(makeLenses ''APILicense)
-$(makeLenses ''Operation)
-$(makeLenses ''Tag)
-$(makeLenses ''Response)
-
-defExternalDocs :: ExternalURL -> ExternalDocs
-defExternalDocs url = mempty & externalURL .~ url
-
-------------------------------------------------------------------------------
--- | Swaggin'
-class HasSwagger h where
- toSwaggerDocs :: Proxy h -> SwaggerRoute -> SwagResult
-------------------------------------------------------------------------------
-class ToSwaggerDescription a where toSwaggerDescription :: Proxy a -> Text
-class ToHeader a where toHeader :: Proxy a -> SwaggerHeader
-class ToResponseHeaders as where toResponseHeaders :: Proxy as -> HM.HashMap Text SwaggerHeader
-instance ToResponseHeaders '[] where toResponseHeaders Proxy = []
-
-instance (ToHeader x, ToResponseHeaders xs) => ToResponseHeaders (x ': xs) where
- toResponseHeaders Proxy = HM.union header' (toResponseHeaders (Proxy :: Proxy xs))
+ piece = symbolVal (Proxy :: Proxy sym)
+
+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (Capture sym a :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & prependPath capture
+ & addDefaultResponse404 (Text.pack name)
where
- header' = transHeader (toHeader (Proxy :: Proxy x))
-
-transHeader :: SwaggerHeader -> HM.HashMap Text SwaggerHeader
-transHeader r@SwaggerHeader{..} = HM.fromList [(headerName, r)]
-
-------------------------------------------------------------------------------
-instance (HasSwagger rest, KnownSymbol sym) => HasSwagger (sym :> rest) where
- toSwaggerDocs Proxy swagRoute =
- toSwaggerDocs (Proxy :: Proxy rest) $ swagRoute & routePathName %~ flip (<>) path
- where path = PathName $ "/" <> T.pack (symbolVal (Proxy :: Proxy sym))
-
-instance (HasSwagger left, HasSwagger right) => HasSwagger (left :<|> right) where
- toSwaggerDocs Proxy swagRoute =
- let swagLeft = toSwaggerDocs (Proxy :: Proxy left) swagRoute
- swagRight = toSwaggerDocs (Proxy :: Proxy right) swagRoute
- paths = HM.unionWith f (swagLeft ^. resultPaths) (swagRight ^. resultPaths)
- models = HM.union (swagLeft ^. resultModels) (swagRight ^. resultModels)
- in SwagResult paths models
- where f (SwaggerOperation l) (SwaggerOperation r) = SwaggerOperation (HM.union l r)
-
-class ToSwaggerParamType a where toSwaggerParamType :: Proxy a -> SwaggerParamType
-instance ToSwaggerParamType Int where toSwaggerParamType = const IntegerSwagParam
-instance ToSwaggerParamType Integer where toSwaggerParamType = const IntegerSwagParam
-instance ToSwaggerParamType UUID.UUID where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType String where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType Text where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType L.Text where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType BL8.ByteString where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType B8.ByteString where toSwaggerParamType = const StringSwagParam
-instance ToSwaggerParamType Double where toSwaggerParamType = const NumberSwagParam
-instance ToSwaggerParamType Float where toSwaggerParamType = const NumberSwagParam
-instance ToSwaggerParamType Bool where toSwaggerParamType Proxy = BooleanSwagParam
-
-instance
- ToSwaggerParamType a => ToSwaggerParamType [a] where
- toSwaggerParamType _ = ArraySwagParam
-
-class ToHeaderDescription a where
- toHeaderDescription :: Proxy a -> Text
-
-instance ( ToSwaggerParamType headerType
- , KnownSymbol headerName
- , ToHeaderDescription headerName
- ) => ToHeader (H.Header headerName headerType) where
- toHeader Proxy = SwaggerHeader desc ht hn
- where
- desc = T.pack . symbolVal $ (Proxy :: Proxy headerName)
- hn = T.pack . symbolVal $ (Proxy :: Proxy headerName)
- ht = toSwaggerParamType (Proxy :: Proxy headerType)
-
-class SwaggerAccept a where toSwaggerAccept :: Proxy a -> ContentType
-instance SwaggerAccept JSON where toSwaggerAccept Proxy = JSON
-instance SwaggerAccept HTML where toSwaggerAccept Proxy = HTML
-instance SwaggerAccept XML where toSwaggerAccept Proxy = XML
-instance SwaggerAccept FormUrlEncoded where toSwaggerAccept Proxy = FormUrlEncoded
-instance SwaggerAccept PlainText where toSwaggerAccept Proxy = PlainText
-instance SwaggerAccept OctetStream where toSwaggerAccept Proxy = OctetStream
-------------------------------------------------------------------------------
-class SwaggerAcceptTypes (xs :: [*]) where toSwaggerAcceptTypes :: Proxy xs -> [ContentType]
-instance SwaggerAcceptTypes '[] where toSwaggerAcceptTypes Proxy = []
-instance (SwaggerAccept x, SwaggerAcceptTypes xs) => SwaggerAcceptTypes (x ': xs) where
- toSwaggerAcceptTypes Proxy =
- toSwaggerAccept (Proxy :: Proxy x) : toSwaggerAcceptTypes (Proxy :: Proxy xs)
-------------------------------------------------------------------------------
-class ToVerb a where toVerb :: Proxy a -> Verb
-instance ToVerb Get where toVerb Proxy = Get
-instance ToVerb Put where toVerb Proxy = Put
-instance ToVerb Patch where toVerb Proxy = Patch
-instance ToVerb Post where toVerb Proxy = Post
-instance ToVerb Delete where toVerb Proxy = Delete
-instance ToVerb Options where toVerb Proxy = Options
-
-class ToSwaggerModel a where
- toSwagModel :: Proxy a -> SwaggerModel
- toSwagModelName :: Proxy a -> ModelName
- toSwagModelName = _swagModelName . toSwagModel
- default toSwagModel :: (Generic a, GToSwaggerModel (Rep a)) => Proxy a -> SwaggerModel
- toSwagModel = undefined
-
-class GToSwaggerModel a where
- gToSwaggerModel :: Proxy a -> f a -> SwaggerModel
-
-instance ToSwaggerModel () where
- toSwagModel Proxy = emptyModel
-
-instance ToSwaggerModel SwaggerAPI where
- toSwagModel Proxy = emptyModel
-
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPABLe #-}
-#endif
- (ToSwaggerModel returnType, ToVerb verb, SwaggerAcceptTypes xs)
- => HasSwagger (verb xs returnType) where
- toSwaggerDocs Proxy swagRoute =
- let swagPath = SwaggerOperation [(toVerb (Proxy :: Proxy verb), path)]
- path = mempty & summary .~ swagRoute ^. routePathSummary
- & params .~ swagRoute ^. routeParams
- & responses .~ [(_responseCode response, response)]
- & produces .~ toSwaggerAcceptTypes (Proxy :: Proxy xs)
- & consumes .~ swagRoute ^. routeConsumes
- in SwagResult [(pathName, swagPath)] newModels
- where
- response = Response "OK" (swagModel ^. swagModelName) [] False 200
- pathName | swagRoute ^. routePathName == PathName "" = PathName "/"
- | otherwise = swagRoute ^. routePathName
- swagModel@SwaggerModel{..} = toSwagModel (Proxy :: Proxy returnType)
- newModels = bool (swagRoute ^. routeModels)
- (HM.insert _swagModelName swagModel (swagRoute ^. routeModels))
- (swagModel /= emptyModel)
-
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPABLe #-}
-#endif
- (ToSwaggerModel returnType, ToVerb verb, SwaggerAcceptTypes xs)
- => HasSwagger (verb xs [returnType]) where
- toSwaggerDocs Proxy swagRoute =
- let swagPath = SwaggerOperation [(toVerb (Proxy :: Proxy verb), path)]
- path = mempty & summary .~ swagRoute ^. routePathSummary
- & params .~ swagRoute ^. routeParams
- & responses .~ [(_responseCode response, response)]
- & produces .~ toSwaggerAcceptTypes (Proxy :: Proxy xs)
- & consumes .~ swagRoute ^. routeConsumes
- in SwagResult [(pathName, swagPath)] newModels
- where
- response = Response "OK" (swagModel ^. swagModelName) [] False 200
- pathName | swagRoute ^. routePathName == PathName "" = PathName "/"
- | otherwise = swagRoute ^. routePathName
- swagModel@SwaggerModel{..} = toSwagModel (Proxy :: Proxy returnType)
- newModels = bool (swagRoute ^. routeModels)
- (HM.insert _swagModelName swagModel (swagRoute ^. routeModels))
- (swagModel /= emptyModel)
-
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPING #-}
-#endif
- (ToSwaggerModel returnType, ToVerb verb, SwaggerAcceptTypes xs, ToResponseHeaders ls)
- => HasSwagger (verb xs (Headers ls [returnType])) where
- toSwaggerDocs Proxy swagRoute =
- let swagPath = SwaggerOperation [(toVerb (Proxy :: Proxy verb), path)]
- path = mempty & summary .~ swagRoute ^. routePathSummary
- & params .~ swagRoute ^. routeParams
- & responses .~ [(_responseCode response, response)]
- & produces .~ toSwaggerAcceptTypes (Proxy :: Proxy xs)
- & consumes .~ swagRoute ^. routeConsumes
- in SwagResult [(swagRoute ^. routePathName, swagPath)] newModels
- where
- response = Response "OK" (swagModel ^. swagModelName)
- (toResponseHeaders (Proxy :: Proxy ls)) True 200
- swagModel@SwaggerModel{..} = toSwagModel (Proxy :: Proxy returnType)
- newModels = bool (swagRoute ^. routeModels)
- (HM.insert _swagModelName swagModel (swagRoute ^. routeModels))
- (swagModel /= emptyModel)
-
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPING #-}
-#endif
- (ToSwaggerModel returnType, ToVerb verb, SwaggerAcceptTypes xs, ToResponseHeaders ls)
- => HasSwagger (verb xs (Headers ls returnType)) where
- toSwaggerDocs Proxy swagRoute =
- let swagPath = SwaggerOperation [(toVerb (Proxy :: Proxy verb), path)]
- path = mempty & summary .~ swagRoute ^. routePathSummary
- & params .~ swagRoute ^. routeParams
- & responses .~ [(_responseCode response, response)]
- & produces .~ toSwaggerAcceptTypes (Proxy :: Proxy xs)
- & consumes .~ swagRoute ^. routeConsumes
- in SwagResult [(swagRoute ^. routePathName, swagPath)] newModels
- where
- response = Response "OK" (swagModel ^. swagModelName) rspHeaders False 200
- swagModel@SwaggerModel{..} = toSwagModel (Proxy :: Proxy returnType)
- rspHeaders = (toResponseHeaders (Proxy :: Proxy ls))
- newModels = bool (swagRoute ^. routeModels)
- (HM.insert _swagModelName swagModel (swagRoute ^. routeModels))
- (swagModel /= emptyModel)
-
-instance (ToSwaggerDescription typ, ToSwaggerParamType typ, KnownSymbol sym, HasSwagger rest) =>
- HasSwagger (Capture sym typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- pName = T.pack $ symbolVal (Proxy :: Proxy sym)
- newPath = PathName $ mconcat ["/{",pName,"}"]
- newParam = Param PathUrl pName
- (Just $ toSwaggerParamType (Proxy :: Proxy typ)) Nothing
- (toSwaggerDescription (Proxy :: Proxy typ)) True True Nothing False
- newSwaggerRoute = swagRoute & routePathName %~ flip (<>) newPath
- & routeParams %~ (:) newParam
-
-instance (ToSwaggerDescription typ, ToSwaggerParamType typ, KnownSymbol sym, HasSwagger rest) =>
- HasSwagger (QueryParam sym typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- pName = T.pack $ symbolVal (Proxy :: Proxy sym)
- newParam = Param Query pName
- (Just $ toSwaggerParamType (Proxy :: Proxy typ)) Nothing
- (toSwaggerDescription (Proxy :: Proxy typ)) True False Nothing False
- newSwaggerRoute = swagRoute & routeParams %~ (:) newParam
-
-instance (ToSwaggerDescription typ, ToSwaggerParamType typ, KnownSymbol sym, HasSwagger rest) =>
- HasSwagger (QueryParams sym typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- ptyp = toSwaggerParamType (Proxy :: Proxy typ)
- pName = T.pack $ symbolVal (Proxy :: Proxy sym)
- newParam = Param Query pName
- (Just ArraySwagParam) (Just $ ItemObject ptyp)
- (toSwaggerDescription (Proxy :: Proxy typ)) True False Nothing True
- newSwaggerRoute = swagRoute & routeParams %~ (:) newParam
-
-------------------------------------------------------------------------------
--- | Query Flag
-instance (ToSwaggerDescription sym, KnownSymbol sym, HasSwagger rest) =>
- HasSwagger (QueryFlag sym :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- pName = T.pack $ symbolVal (Proxy :: Proxy sym)
- newParam = Param Query pName
- (Just StringSwagParam) Nothing
- (toSwaggerDescription (Proxy :: Proxy sym)) True False Nothing False
- newSwaggerRoute = swagRoute & routeParams %~ (:) newParam
-
-------------------------------------------------------------------------------
--- | Raw holds no verb / body information
-instance HasSwagger Raw where
- toSwaggerDocs Proxy swagRoute =
- SwagResult [(swagRoute ^. routePathName, mempty)] []
-
-------------------------------------------------------------------------------
--- | Swagger doesn't support Raw, bypass
-instance HasSwagger rest => HasSwagger (MatrixParam typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) swagRoute
-
-------------------------------------------------------------------------------
--- | Swagger doesn't support matrix flags, bypass
-instance (ToSwaggerDescription typ, ToSwaggerParamType typ, HasSwagger rest) =>
- HasSwagger (MatrixFlag typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) swagRoute
-
-------------------------------------------------------------------------------
--- | Swagger Header
-instance (KnownSymbol sym, ToSwaggerDescription typ, ToSwaggerParamType typ, HasSwagger rest) =>
- HasSwagger (H.Header sym typ :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- newSwaggerRoute = swagRoute & routeParams %~ (:) newParams
- pName = T.pack $ symbolVal (Proxy :: Proxy sym)
- pDesc = toSwaggerDescription (Proxy :: Proxy typ)
- typ = toSwaggerParamType (Proxy :: Proxy typ)
- newParams = Param Header pName (Just typ)
- Nothing pDesc False True Nothing False
-
-------------------------------------------------------------------------------
--- | ReqBody Object
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPABLe #-}
-#endif
- (SwaggerAcceptTypes ctypes, ToSwaggerModel model, HasSwagger rest) =>
- HasSwagger (ReqBody ctypes model :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- swagModel@SwaggerModel {..} = toSwagModel (Proxy :: Proxy model)
- newSwaggerRoute =
- swagRoute & routeModels %~ model
- & routeParams %~ (++) newParam
- & routeConsumes %~ (++) (toSwaggerAcceptTypes (Proxy :: Proxy ctypes))
- model | swagModel == emptyModel = (<> mempty)
- | otherwise = HM.insert _swagModelName (toSwagModel (Proxy :: Proxy model))
- newParam =
- case _swagModelName of
- (ModelName "") -> []
- name -> [ Param Body (unModelName name) Nothing Nothing
- (fromMaybe mempty (unDescription <$> _swagDescription)) True False Nothing False]
-
-------------------------------------------------------------------------------
--- | ReqBody Array
-instance
-#if MIN_VERSION_base(4,8,0)
- {-# OVERLAPPING #-}
-#endif
- (SwaggerAcceptTypes ctypes, ToSwaggerModel model, HasSwagger rest) =>
- HasSwagger (ReqBody ctypes [model] :> rest) where
- toSwaggerDocs Proxy swagRoute = toSwaggerDocs (Proxy :: Proxy rest) newSwaggerRoute
- where
- swagModel@SwaggerModel {..} = toSwagModel (Proxy :: Proxy model)
- newSwaggerRoute =
- swagRoute & routeModels %~ model
- & routeParams %~ (++) newParam
- & routeConsumes %~ (++) (toSwaggerAcceptTypes (Proxy :: Proxy ctypes))
- model | swagModel == emptyModel = (<> mempty)
- | otherwise = HM.insert _swagModelName (toSwagModel (Proxy :: Proxy model))
- newParam =
- case _swagModelName of
- (ModelName "") -> []
- name -> [ Param Body (unModelName name) Nothing Nothing
- (fromMaybe mempty (unDescription <$> _swagDescription)) True False Nothing True]
-
-class ToModelExample model where toExample :: Proxy model -> Maybe Value
-
-instance ToJSON SwaggerHeader where
- toJSON SwaggerHeader{..} =
- object [
- "type" .= headerType
- , "description" .= headerDescription
- ]
-
-instance Monoid SwaggerOperation where
- mempty = SwaggerOperation HM.empty
- SwaggerOperation a `mappend` SwaggerOperation b =
- SwaggerOperation ( a <> b )
-
-instance ToJSON APILicense where
- toJSON APILicense{..} =
- object [ "name" .= _licenseName
- , "url" .= _licenseUrl
- ]
-
-instance Hashable Verb where hash = hash . show
-
-instance ToJSON SwaggerParamType where
- toJSON StringSwagParam = String "string"
- toJSON NumberSwagParam = String "number"
- toJSON IntegerSwagParam = String "integer"
- toJSON BooleanSwagParam = String "boolean"
- toJSON ArraySwagParam = String "array"
- toJSON FileSwagParam = String "file"
-
-instance ToJSON SwaggerType where
- toJSON x =
- let f typ format = object $ [ "type" .= (typ :: Text) ] ++
- if isJust format
- then [ "format" .= ((fromJust format) :: Text) ]
- else []
- in case x of
- IntegerSwag -> f "integer" (Just "int32")
- LongSwag -> f "integer" (Just "int64")
- FloatSwag -> f "number" (Just "float")
- DoubleSwag -> f "number" (Just "double")
- StringSwag -> f "string" Nothing
- ByteSwag -> f "string" (Just "byte")
- BinarySwag -> f "string" (Just "binary")
- BooleanSwag -> f "boolean" Nothing
- DateSwag -> f "string" (Just "date")
- DateTimeSwag -> f "string" (Just "date-time")
- PasswordSwag -> f "string" (Just "password")
- Model ModelSwag{..} ->
- case modelSwagIsArray of
- True ->
- object [ "type" .= ("array" :: Text)
- , "items" .= object [
- "$ref" .= ("#/definitions/" <> unModelName modelSwagName)
- ]
- ]
- False ->
- object [
- "$ref".= ("#/definitions/"<> unModelName modelSwagName)
- ]
-
-instance ToJSON ContentType where
- toJSON JSON = String "application/json"
- toJSON XML = String "application/xml"
- toJSON FormUrlEncoded = String "application/x-www-form-urlencoded"
- toJSON HTML = String "text/html"
- toJSON PlainText = String "text/plain; charset=utf-8"
- toJSON OctetStream = String "application/octet-stream"
-
-instance ToJSON Scheme where
- toJSON Http = String "http"
- toJSON Https = String "https"
- toJSON Ws = String "ws"
- toJSON Wss = String "wss"
-
-instance ToJSON In where
- toJSON PathUrl = "path"
- toJSON Query = "query"
- toJSON Body = "body"
- toJSON Header = "header"
- toJSON FormData = "formData"
-
-instance ToJSON PathName where
- toJSON (PathName x) = String (T.toLower x)
-
-instance ToJSON SwaggerModel where
- toJSON SwaggerModel{..} =
- object $ [
- "type" .= ("object" :: Text)
- , "properties" .= HM.fromList _swagProperties
- ] ++ maybeExample ++ maybeDescription ++ requiredList
+ name = symbolVal (Proxy :: Proxy sym)
+ capture = "{" <> name <> "}"
+ param = mempty
+ & paramName .~ Text.pack name
+ & paramRequired ?~ True
+ & paramSchema .~ ParamOther (mempty
+ & paramOtherSchemaIn .~ ParamPath
+ & parameterSchema .~ toParamSchema (Proxy :: Proxy a))
+
+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (QueryParam sym a :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & addDefaultResponse400 (Text.pack name)
where
- requiredList = [ "required" .= _swagModelRequired | not (null _swagModelRequired) ]
- maybeDescription = maybe [] (\(Description x) -> [ "description" .= x ]) _swagDescription
- maybeExample = maybe [] (\x -> [ "example" .= x ]) _swagModelExample
+ name = symbolVal (Proxy :: Proxy sym)
+ param = mempty
+ & paramName .~ Text.pack name
+ & paramSchema .~ ParamOther (mempty
+ & paramOtherSchemaIn .~ ParamQuery
+ & parameterSchema .~ toParamSchema (Proxy :: Proxy a))
+
+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (QueryParams sym a :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & addDefaultResponse400 (Text.pack name)
+ where
+ name = symbolVal (Proxy :: Proxy sym)
+ param = mempty
+ & paramName .~ Text.pack name
+ & paramSchema .~ ParamOther (mempty
+ & paramOtherSchemaIn .~ ParamQuery
+ & parameterSchema .~ (mempty
+ & schemaType .~ SwaggerArray
+ & schemaItems ?~ SwaggerItemsPrimitive (Just CollectionMulti) (toParamSchema (Proxy :: Proxy a))))
+
+instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (QueryFlag sym :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & addDefaultResponse400 (Text.pack name)
+ where
+ name = symbolVal (Proxy :: Proxy sym)
+ param = mempty
+ & paramName .~ Text.pack name
+ & paramSchema .~ ParamOther (mempty
+ & paramOtherSchemaIn .~ ParamQuery
+ & paramOtherSchemaAllowEmptyValue ?~ True
+ & parameterSchema .~ (toParamSchema (Proxy :: Proxy Bool)
+ & schemaDefault ?~ toJSON False))
+
+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (Header sym a :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & addDefaultResponse400 (Text.pack name)
+ where
+ name = symbolVal (Proxy :: Proxy sym)
+ param = mempty
+ & paramName .~ Text.pack name
+ & paramSchema .~ ParamOther (mempty
+ & paramOtherSchemaIn .~ ParamHeader
+ & parameterSchema .~ toParamSchema (Proxy :: Proxy Bool))
+
+instance (ToSchema a, AllAccept cs, HasSwagger sub) => HasSwagger (ReqBody cs a :> sub) where
+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
+ & addParam param
+ & addConsumes (allContentType (Proxy :: Proxy cs))
+ & addDefaultResponse400 name
+ & definitions %~ (<> defs)
+ where
+ name = "body"
+ (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty
+ param = mempty
+ & paramName .~ "body"
+ & paramSchema .~ ParamBody ref
-setPath :: BasePath -> BasePath
-setPath (BasePath "") = BasePath "/"
-setPath (BasePath x) = BasePath x
+-- =======================================================================
+-- Below are the definitions that should be in Servant.API.ContentTypes
+-- =======================================================================
+class AllAccept cs where
+ allContentType :: Proxy cs -> [MediaType]
-instance ToJSON SwaggerOperation where
- toJSON (SwaggerOperation paths) =
- Object . HM.fromList . map f . HM.toList $ paths
- where
- f (verb, sp) = (T.toLower $ toTxt verb, toJSON sp)
-
-instance ToJSON Operation where
- toJSON Operation {..} =
- object $ [ "parameters" .= _params
- , "responses" .= do Object . HM.fromList . map f . HM.toList $ _responses
- , "produces" .= _produces
- , "consumes" .= _consumes
- , "summary" .= _summary
- , "tags" .= map _tagName _tags
- , "description" .= _description
- ] ++ [ "deprecated" .= _deprecated | isJust _deprecated ]
- ++ [ "operationId" .= _operationId | isJust _operationId ]
- where f (Code x, resp) = (toTxt x, toJSON resp)
-
-instance ToJSON Response where
- toJSON Response {..} = object $ [
- "description" .= _responseDescription
- , "headers" .= _responseHeaders
- ] ++ maybeModelName
+instance AllAccept '[] where
+ allContentType _ = []
+
+instance (Accept c, AllAccept cs) => AllAccept (c ': cs) where
+ allContentType _ = contentType (Proxy :: Proxy c) : allContentType (Proxy :: Proxy cs)
+
+class ToResponseHeader h where
+ toResponseHeader :: Proxy h -> (HeaderName, Swagger.Header)
+
+instance (KnownSymbol sym, ToParamSchema a) => ToResponseHeader (Header sym a) where
+ toResponseHeader _ = (hname, Swagger.Header Nothing schema)
where
- maybeModelName =
- case _responseModelName of
- (ModelName "") -> []
- (ModelName name) ->
- case _responseIsArray of
- True -> [ "schema" .= object [
- "type" .= ("array" :: Text)
- , "items" .= object [
- "$ref" .= ("#/definitions/" <> name)
- ]]]
- False -> ["schema".=object["$ref".=("#/definitions/"<> name)]]
-
-instance ToJSON Param where
- toJSON Param{..} =
- object $ [
- "in" .= _in
- , "name" .= _name
- , "description" .= _paramDescription
- , "required" .= _required
- ] ++ maybeSchema ++ [ "type" .= _type | isJust _type ]
+ hname = Text.pack (symbolVal (Proxy :: Proxy sym))
+ schema = toParamSchema (Proxy :: Proxy a)
+
+class AllToResponseHeader hs where
+ toAllResponseHeaders :: Proxy hs -> HashMap HeaderName Swagger.Header
+
+instance AllToResponseHeader '[] where
+ toAllResponseHeaders _ = mempty
+
+instance (ToResponseHeader h, AllToResponseHeader hs) => AllToResponseHeader (h ': hs) where
+ toAllResponseHeaders _ = HashMap.insert name header headers
where
- maybeSchema =
- case _in of
- Body -> [ "schema" .=
- case _isArray of
- False ->
- object [ "$ref" .= ("#/definitions/" <> _name) ]
- True ->
- object [
- "type" .= ("array" :: Text)
- , "items" .= object [
- "$ref" .= ("#/definitions/" <> _name )
- ]
- ]
- ]
- _ -> []
-
-instance ToSwaggerModel a => ToSwaggerModel (Maybe a) where
- toSwagModel _ = toSwagModel (Proxy :: Proxy a)
-
-instance ToJSON Info where
- toJSON Info{..} =
- object $ [
- "title" .= _swaggerInfoTitle
- , "version" .= _swaggerVersion
- , "description" .= _swaggerAPIDescription
- ] ++ [ "license" .= _license | isJust _license ]
- ++ [ "contact" .= _contact | isJust _contact ]
- ++ [ "termsOfService" .= _termsOfService | isJust _termsOfService ]
-
-toTxt :: Show a => a -> Text
-toTxt = T.pack . show
-
-newtype SwaggerRouteInfo a = SwaggerRouteInfo SwagResult -- deriving Monoid
-
-instance Monoid (SwaggerRouteInfo a) where
- mempty = SwaggerRouteInfo mempty
- SwaggerRouteInfo s1 `mappend` SwaggerRouteInfo s2
- = SwaggerRouteInfo (s1 `mappend` s2)
-
-instance Monoid SwagResult where
- mempty = SwagResult mempty mempty
- SwagResult x1 y1 `mappend` SwagResult x2 y2
- = SwagResult (HM.unionWith mergePaths x1 x2) (HM.union y1 y2)
- where
- mergePaths (SwaggerOperation l) (SwaggerOperation r) = SwaggerOperation (HM.unionWith g l r)
- g p1 p2 =
- p1 & summary %~ (<>) (p2 ^. summary)
- & responses %~ HM.union (p2 ^. responses)
- & tags %~ (++) (p2 ^. tags)
- & operationId .~ p1 ^. operationId <> p2 ^. operationId
- & description .~ p1 ^. description <> p2 ^. description
-
-swaggerPathInfo
- :: ( IsElem endpoint layout, HasLink endpoint, HasSwagger endpoint, HasSwagger layout )
- => Proxy endpoint
- -> Proxy layout
- -> SwaggerRouteDescription
- -> SwaggerRouteInfo layout
-swaggerPathInfo pEndpoint pLayout SwaggerRouteDescription{..} = swagResult
- where
- f [(pName, SwaggerOperation swagPath)] =
- [(pName, SwaggerOperation $ HM.fromList . g . HM.toList $ swagPath)]
- f _ = error "Route non-existant, impossible"
- g [(verb, path)] = [(verb, newPath path)]
- g _ = error "Route non-existant, impossible"
- newPath p = p & summary .~ _swagRouteSummary
- & operationId .~ _swagRouteOperationId
- & description .~ _swagRouteDescription
- & responses %~ HM.union _swagRouteResponses
- & tags %~ (++) _swagRouteTags
- swagResult =
- let finalDocs = toSwaggerDocs pLayout defSwaggerRoute
- SwagResult paths models = toSwaggerDocs pEndpoint defSwaggerRoute
- newModels = _swagRouteModels `HM.union` models
- newPaths = HM.fromList . f . HM.toList $ paths
- pathDocs = SwagResult newPaths newModels
- in SwaggerRouteInfo (finalDocs <> pathDocs)
-
-getAllTags :: SwagResult -> [Tag]
-getAllTags (SwagResult paths _) =
- S.toList . S.fromList . _tags =<< HM.elems =<< _paths <$> HM.elems paths
+ (name, header) = toResponseHeader (Proxy :: Proxy h)
+ headers = toAllResponseHeaders (Proxy :: Proxy hs)
+
+instance AllToResponseHeader hs => AllToResponseHeader (HList hs) where
+ toAllResponseHeaders _ = toAllResponseHeaders (Proxy :: Proxy hs)
+
+-- | Check that every element of @xs@ is an endpoint of @api@.
+type family AllIsElem xs api :: Constraint where
+ AllIsElem '[] api = ()
+ AllIsElem (x ': xs) api = (IsIn x api, AllIsElem xs api)
+
+-- | Apply @(e :>)@ to every API in @xs@.
+type family MapSub e xs where
+ MapSub e '[] = '[]
+ MapSub e (x ': xs) = (e :> x) ': MapSub e xs
+
+-- | Append two type-level lists.
+type family AppendList xs ys where
+ AppendList '[] ys = ys
+ AppendList (x ': xs) ys = x ': AppendList xs ys
+
+-- | Build a list of endpoints from an API.
+type family EndpointsList api where
+ EndpointsList (a :<|> b) = AppendList (EndpointsList a) (EndpointsList b)
+ EndpointsList (e :> a) = MapSub e (EndpointsList a)
+ EndpointsList a = '[a]
+
+-- | Check whether @sub@ is a sub API of @api@.
+type family IsSubAPI sub api :: Constraint where
+ IsSubAPI sub api = AllIsElem (EndpointsList sub) api
+
+type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
+ Or () b = ()
+ Or a () = ()
+
+type family IsIn sub api :: Constraint where
+ IsIn e (a :<|> b) = Or (IsIn e a) (IsIn e b)
+ IsIn (e :> a) (e :> b) = IsIn a b
+ IsIn e e = ()
+
diff --git a/stack.yaml b/stack.yaml
index ebddb4044..c83cc7a8d 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -9,7 +9,8 @@ packages:
- example/
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
-extra-deps: []
+extra-deps:
+- swagger2-1.1.1
# Override default flag values for local packages and extra-deps
flags: {}
diff --git a/test/Servant/SwaggerSpec.hs b/test/Servant/SwaggerSpec.hs
new file mode 100644
index 000000000..de6d61b9d
--- /dev/null
+++ b/test/Servant/SwaggerSpec.hs
@@ -0,0 +1,323 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE QuasiQuotes #-}
+module Servant.SwaggerSpec where
+
+import Control.Lens
+import Data.Aeson
+import qualified Data.Aeson.Types as JSON
+import Data.Aeson.QQ
+import Data.Char (toLower)
+import Data.Proxy
+import Data.Swagger
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Data.Time
+import GHC.Generics
+import Servant.API
+import Servant.Swagger
+import Test.Hspec
+
+checkAPI :: HasSwagger api => Proxy api -> Value -> IO ()
+checkAPI proxy = checkSwagger (toSwagger proxy)
+
+checkSwagger :: Swagger -> Value -> IO ()
+checkSwagger swag js = toJSON swag `shouldBe` js
+
+spec :: Spec
+spec = describe "HasSwagger" $ do
+ it "Todo API" $ checkAPI (Proxy :: Proxy TodoAPI) todoAPI
+ it "Hackage API (with tags)" $ checkSwagger hackageSwaggerWithTags hackageAPI
+
+main :: IO ()
+main = hspec spec
+
+-- =======================================================================
+-- Todo API
+-- =======================================================================
+
+data Todo = Todo
+ { created :: UTCTime
+ , title :: String
+ , description :: Maybe String
+ } deriving (Generic, FromJSON, ToSchema)
+
+newtype TodoId = TodoId String deriving (Generic, ToParamSchema)
+
+type TodoAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
+
+todoAPI :: Value
+todoAPI = [aesonQQ|
+{
+ "swagger":"2.0",
+ "info":
+ {
+ "title": "",
+ "version": ""
+ },
+ "definitions":
+ {
+ "Todo":
+ {
+ "type": "object",
+ "required": [ "created", "title" ],
+ "properties":
+ {
+ "created": { "$ref": "#/definitions/UTCTime" },
+ "title": { "type": "string" },
+ "description": { "type": "string" }
+ }
+ },
+ "UTCTime":
+ {
+ "type": "string",
+ "format": "yyyy-mm-ddThh:MM:ssZ"
+ }
+ },
+ "paths":
+ {
+ "/todo/{id}":
+ {
+ "get":
+ {
+ "responses":
+ {
+ "200":
+ {
+ "schema": { "$ref":"#/definitions/Todo" },
+ "description": ""
+ },
+ "404": { "description": "id not found" }
+ },
+ "produces": [ "application/json" ],
+ "parameters":
+ [
+ {
+ "required": true,
+ "in": "path",
+ "name": "id",
+ "type": "string"
+ }
+ ]
+ }
+ }
+ }
+}
+|]
+
+-- =======================================================================
+-- Hackage API
+-- =======================================================================
+
+type HackageAPI
+ = HackageUserAPI
+ :<|> HackagePackagesAPI
+
+type HackageUserAPI =
+ "users" :> Get '[JSON] [UserSummary]
+ :<|> "user" :> Capture "username" Username :> Get '[JSON] UserDetailed
+
+type HackagePackagesAPI
+ = "packages" :> Get '[JSON] [Package]
+
+type Username = Text
+
+data UserSummary = UserSummary
+ { summaryUsername :: Username
+ , summaryUserid :: Int
+ } deriving (Eq, Show, Generic)
+
+lowerCutPrefix :: String -> String -> String
+lowerCutPrefix s = map toLower . drop (length s)
+
+instance ToJSON UserSummary where
+ toJSON = genericToJSON JSON.defaultOptions { JSON.fieldLabelModifier = lowerCutPrefix "summary" }
+
+instance ToSchema UserSummary where
+ declareNamedSchema proxy = do
+ (name, schema) <- genericDeclareNamedSchema defaultSchemaOptions { fieldLabelModifier = lowerCutPrefix "summary" } proxy
+ return (name, schema
+ & schemaExample ?~ toJSON UserSummary
+ { summaryUsername = "JohnDoe"
+ , summaryUserid = 123 })
+
+type Group = Text
+
+data UserDetailed = UserDetailed
+ { username :: Username
+ , userid :: Int
+ , groups :: [Group]
+ } deriving (Eq, Show, Generic, ToSchema)
+
+newtype Package = Package { packageName :: Text }
+ deriving (Eq, Show, Generic, ToSchema)
+
+hackageSwaggerWithTags :: Swagger
+hackageSwaggerWithTags = toSwagger (Proxy :: Proxy HackageAPI)
+ & host ?~ Host "hackage.haskell.org" Nothing
+ & usersOps %~ addTag "users"
+ & packagesOps %~ addTag "packages"
+ & tags .~
+ [ Tag "users" (Just "Operations about user") Nothing
+ , Tag "packages" (Just "Query packages") Nothing
+ ]
+ where
+ usersOps = subOperations (Proxy :: Proxy HackageUserAPI) (Proxy :: Proxy HackageAPI)
+ packagesOps = subOperations (Proxy :: Proxy HackagePackagesAPI) (Proxy :: Proxy HackageAPI)
+
+hackageAPI :: Value
+hackageAPI = [aesonQQ|
+{
+ "swagger":"2.0",
+ "host":"hackage.haskell.org",
+ "info":{
+ "version":"",
+ "title":""
+ },
+ "definitions":{
+ "UserDetailed":{
+ "required":[
+ "username",
+ "userid",
+ "groups"
+ ],
+ "type":"object",
+ "properties":{
+ "groups":{
+ "items":{
+ "type":"string"
+ },
+ "type":"array"
+ },
+ "username":{
+ "type":"string"
+ },
+ "userid":{
+ "maximum":9223372036854775807,
+ "minimum":-9223372036854775808,
+ "type":"integer"
+ }
+ }
+ },
+ "Package":{
+ "required":[
+ "packageName"
+ ],
+ "type":"object",
+ "properties":{
+ "packageName":{
+ "type":"string"
+ }
+ }
+ },
+ "UserSummary":{
+ "required":[
+ "username",
+ "userid"
+ ],
+ "type":"object",
+ "properties":{
+ "username":{
+ "type":"string"
+ },
+ "userid":{
+ "maximum":9223372036854775807,
+ "minimum":-9223372036854775808,
+ "type":"integer"
+ }
+ },
+ "example":{
+ "username": "JohnDoe",
+ "userid": 123
+ }
+ }
+ },
+ "paths":{
+ "/users":{
+ "get":{
+ "responses":{
+ "200":{
+ "schema":{
+ "items":{
+ "$ref":"#/definitions/UserSummary"
+ },
+ "type":"array"
+ },
+ "description":""
+ }
+ },
+ "produces":[
+ "application/json"
+ ],
+ "tags":[
+ "users"
+ ]
+ }
+ },
+ "/packages":{
+ "get":{
+ "responses":{
+ "200":{
+ "schema":{
+ "items":{
+ "$ref":"#/definitions/Package"
+ },
+ "type":"array"
+ },
+ "description":""
+ }
+ },
+ "produces":[
+ "application/json"
+ ],
+ "tags":[
+ "packages"
+ ]
+ }
+ },
+ "/user/{username}":{
+ "get":{
+ "responses":{
+ "404":{
+ "description":"username not found"
+ },
+ "200":{
+ "schema":{
+ "$ref":"#/definitions/UserDetailed"
+ },
+ "description":""
+ }
+ },
+ "produces":[
+ "application/json"
+ ],
+ "parameters":[
+ {
+ "required":true,
+ "in":"path",
+ "name":"username",
+ "type":"string"
+ }
+ ],
+ "tags":[
+ "users"
+ ]
+ }
+ }
+ },
+ "tags":[
+ {
+ "name":"users",
+ "description":"Operations about user"
+ },
+ {
+ "name":"packages",
+ "description":"Query packages"
+ }
+ ]
+}
+|]
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
index 000000000..a824f8c30
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}