servant-openapi3-2.0.0: test/Servant/OpenApiSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PackageImports #-}
module Servant.OpenApiSpec where
import Control.Lens
import Data.Aeson (ToJSON (toJSON), Value, encode, genericToJSON)
import Data.Aeson.QQ.Simple
import qualified Data.Aeson.Types as JSON
import Data.Char (toLower)
import Data.Int (Int64)
import Data.OpenApi
import Data.Proxy
import Data.Text (Text)
import Data.Time
import GHC.Generics
import Servant.API
import Servant.OpenApi
import Servant.Test.ComprehensiveAPI (comprehensiveAPI)
import Test.Hspec hiding (example)
checkAPI :: HasCallStack => HasOpenApi api => Proxy api -> Value -> IO ()
checkAPI proxy = checkOpenApi (toOpenApi proxy)
checkOpenApi :: HasCallStack => OpenApi -> Value -> IO ()
checkOpenApi swag js = encode (toJSON swag) `shouldBe` (encode js)
spec :: Spec
spec = describe "HasOpenApi" $ do
it "Todo API" $ checkAPI (Proxy :: Proxy TodoAPI) todoAPI
it "Hackage API (with tags)" $ checkOpenApi hackageOpenApiWithTags hackageAPI
it "GetPost API (test subOperations)" $ checkOpenApi getPostOpenApi getPostAPI
it "Comprehensive API" $ do
let _x = toOpenApi comprehensiveAPI
True `shouldBe` True -- type-level test
main :: IO ()
main = hspec spec
-- =======================================================================
-- Todo API
-- =======================================================================
data Todo = Todo
{ created :: UTCTime
, title :: String
, summary :: Maybe String
} deriving (Generic)
instance ToJSON Todo
instance ToSchema Todo
newtype TodoId = TodoId String deriving (Generic)
instance ToParamSchema TodoId
type TodoAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
todoAPI :: Value
todoAPI = [aesonQQ|
{
"openapi": "3.0.0",
"info": {
"version": "",
"title": ""
},
"components": {
"schemas": {
"Todo": {
"required": [
"created",
"title"
],
"type": "object",
"properties": {
"summary": {
"type": "string"
},
"created": {
"$ref": "#/components/schemas/UTCTime"
},
"title": {
"type": "string"
}
}
},
"UTCTime": {
"example": "2016-07-22T00:00:00Z",
"format": "yyyy-mm-ddThh:MM:ssZ",
"type": "string"
}
}
},
"paths": {
"/todo/{id}": {
"get": {
"responses": {
"404": {
"description": "`id` not found"
},
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/Todo"
}
}
},
"description": ""
}
},
"parameters": [
{
"required": true,
"schema": {
"type": "string"
},
"in": "path",
"name": "id"
}
]
}
}
}
}
|]
-- =======================================================================
-- 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 :: Int64 -- Word64 would make sense too
} 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 = genericDeclareNamedSchema defaultSchemaOptions { fieldLabelModifier = lowerCutPrefix "summary" } proxy
& mapped.schema.example ?~ toJSON UserSummary
{ summaryUsername = "JohnDoe"
, summaryUserid = 123 }
type Group = Text
data UserDetailed = UserDetailed
{ username :: Username
, userid :: Int64
, groups :: [Group]
} deriving (Eq, Show, Generic)
instance ToSchema UserDetailed
newtype Package = Package { packageName :: Text }
deriving (Eq, Show, Generic)
instance ToSchema Package
hackageOpenApiWithTags :: OpenApi
hackageOpenApiWithTags = toOpenApi (Proxy :: Proxy HackageAPI)
& servers .~ ["https://hackage.haskell.org"]
& applyTagsFor usersOps ["users" & description ?~ "Operations about user"]
& applyTagsFor packagesOps ["packages" & description ?~ "Query packages"]
where
usersOps, packagesOps :: Traversal' OpenApi Operation
usersOps = subOperations (Proxy :: Proxy HackageUserAPI) (Proxy :: Proxy HackageAPI)
packagesOps = subOperations (Proxy :: Proxy HackagePackagesAPI) (Proxy :: Proxy HackageAPI)
hackageAPI :: Value
hackageAPI = [aesonQQ|
{
"openapi": "3.0.0",
"servers": [
{
"url": "https://hackage.haskell.org"
}
],
"components": {
"schemas": {
"UserDetailed": {
"required": [
"username",
"userid",
"groups"
],
"type": "object",
"properties": {
"groups": {
"items": {
"type": "string"
},
"type": "array"
},
"username": {
"type": "string"
},
"userid": {
"maximum": 9223372036854775807,
"format": "int64",
"minimum": -9223372036854775808,
"type": "integer"
}
}
},
"Package": {
"required": [
"packageName"
],
"type": "object",
"properties": {
"packageName": {
"type": "string"
}
}
},
"UserSummary": {
"example": {
"username": "JohnDoe",
"userid": 123
},
"required": [
"username",
"userid"
],
"type": "object",
"properties": {
"username": {
"type": "string"
},
"userid": {
"maximum": 9223372036854775807,
"format": "int64",
"minimum": -9223372036854775808,
"type": "integer"
}
}
}
}
},
"info": {
"version": "",
"title": ""
},
"paths": {
"/users": {
"get": {
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"items": {
"$ref": "#/components/schemas/UserSummary"
},
"type": "array"
}
}
},
"description": ""
}
},
"tags": [
"users"
]
}
},
"/packages": {
"get": {
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"items": {
"$ref": "#/components/schemas/Package"
},
"type": "array"
}
}
},
"description": ""
}
},
"tags": [
"packages"
]
}
},
"/user/{username}": {
"get": {
"responses": {
"404": {
"description": "`username` not found"
},
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/UserDetailed"
}
}
},
"description": ""
}
},
"parameters": [
{
"required": true,
"schema": {
"type": "string"
},
"in": "path",
"name": "username"
}
],
"tags": [
"users"
]
}
}
},
"tags": [
{
"name": "users",
"description": "Operations about user"
},
{
"name": "packages",
"description": "Query packages"
}
]
}
|]
-- =======================================================================
-- Get/Post API (test for subOperations)
-- =======================================================================
type GetPostAPI = Get '[JSON] String :<|> Post '[JSON] String
getPostOpenApi :: OpenApi
getPostOpenApi = toOpenApi (Proxy :: Proxy GetPostAPI)
& applyTagsFor getOps ["get" & description ?~ "GET operations"]
where
getOps :: Traversal' OpenApi Operation
getOps = subOperations (Proxy :: Proxy (Get '[JSON] String)) (Proxy :: Proxy GetPostAPI)
getPostAPI :: Value
getPostAPI = [aesonQQ|
{
"components": {},
"openapi": "3.0.0",
"info": {
"version": "",
"title": ""
},
"paths": {
"/": {
"post": {
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"type": "string"
}
}
},
"description": ""
}
}
},
"get": {
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"type": "string"
}
}
},
"description": ""
}
},
"tags": [
"get"
]
}
}
},
"tags": [
{
"name": "get",
"description": "GET operations"
}
]
}
|]