|
|
|
|
@@ -0,0 +1,255 @@
|
|
|
|
|
{-# LANGUAGE DataKinds, TypeFamilies, TypeOperators, FlexibleInstances, OverloadedStrings, ViewPatterns #-}
|
|
|
|
|
{-# LANGUAGE RecordWildCards, GeneralizedNewtypeDeriving, DeriveTraversable, FlexibleContexts, DeriveGeneric #-}
|
|
|
|
|
{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-unused-imports -fcontext-stack=328 #-}
|
|
|
|
|
module SwaggerPetstore.API (
|
|
|
|
|
-- * Client and Server
|
|
|
|
|
ServerConfig(..),
|
|
|
|
|
SwaggerPetstoreBackend,
|
|
|
|
|
createSwaggerPetstoreClient,
|
|
|
|
|
runSwaggerPetstoreServer,
|
|
|
|
|
runSwaggerPetstoreClient,
|
|
|
|
|
runSwaggerPetstoreClientWithManager,
|
|
|
|
|
SwaggerPetstoreClient,
|
|
|
|
|
-- ** Servant
|
|
|
|
|
SwaggerPetstoreAPI,
|
|
|
|
|
) where
|
|
|
|
|
|
|
|
|
|
import SwaggerPetstore.Types
|
|
|
|
|
|
|
|
|
|
import Data.Aeson (Value)
|
|
|
|
|
import Data.Coerce (coerce)
|
|
|
|
|
import Servant.API
|
|
|
|
|
import Servant (serve, ServantErr)
|
|
|
|
|
import Web.HttpApiData
|
|
|
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import Servant.Common.BaseUrl(BaseUrl(..))
|
|
|
|
|
import Servant.Client (ServantError, client, Scheme(Http))
|
|
|
|
|
import Data.Proxy (Proxy(..))
|
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
|
import Data.Function ((&))
|
|
|
|
|
import GHC.Exts (IsString(..))
|
|
|
|
|
import qualified Data.Map as Map
|
|
|
|
|
import GHC.Generics (Generic)
|
|
|
|
|
import Data.Monoid ((<>))
|
|
|
|
|
import Servant.API.Verbs (Verb, StdMethod(..))
|
|
|
|
|
import Control.Monad.Except (ExceptT)
|
|
|
|
|
import Network.HTTP.Client (Manager, newManager, defaultManagerSettings)
|
|
|
|
|
import Network.HTTP.Types.Method (methodOptions)
|
|
|
|
|
|
|
|
|
|
instance ReflectMethod 'OPTIONS where
|
|
|
|
|
reflectMethod _ = methodOptions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data FormUpdatePetWithForm = FormUpdatePetWithForm
|
|
|
|
|
{ updatePetWithFormName :: Text
|
|
|
|
|
, updatePetWithFormStatus :: Text
|
|
|
|
|
} deriving (Show, Eq, Generic)
|
|
|
|
|
|
|
|
|
|
instance FromFormUrlEncoded FormUpdatePetWithForm where
|
|
|
|
|
fromFormUrlEncoded inputs = FormUpdatePetWithForm <$> lookupEither "name" inputs <*> lookupEither "status" inputs
|
|
|
|
|
instance ToFormUrlEncoded FormUpdatePetWithForm where
|
|
|
|
|
toFormUrlEncoded value = [("name", toQueryParam $ updatePetWithFormName value), ("status", toQueryParam $ updatePetWithFormStatus value)]
|
|
|
|
|
|
|
|
|
|
data FormUploadFile = FormUploadFile
|
|
|
|
|
{ uploadFileAdditionalMetadata :: Text
|
|
|
|
|
, uploadFileFile :: FilePath
|
|
|
|
|
} deriving (Show, Eq, Generic)
|
|
|
|
|
|
|
|
|
|
instance FromFormUrlEncoded FormUploadFile where
|
|
|
|
|
fromFormUrlEncoded inputs = FormUploadFile <$> lookupEither "additionalMetadata" inputs <*> lookupEither "file" inputs
|
|
|
|
|
instance ToFormUrlEncoded FormUploadFile where
|
|
|
|
|
toFormUrlEncoded value = [("additionalMetadata", toQueryParam $ uploadFileAdditionalMetadata value), ("file", toQueryParam $ uploadFileFile value)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- For the form data code generation.
|
|
|
|
|
lookupEither :: FromHttpApiData b => Text -> [(Text, Text)] -> Either Text b
|
|
|
|
|
lookupEither key assocs =
|
|
|
|
|
case lookup key assocs of
|
|
|
|
|
Nothing -> Left $ "Could not find parameter " <> key <> " in form data"
|
|
|
|
|
Just value -> parseQueryParam value
|
|
|
|
|
|
|
|
|
|
-- | Servant type-level API, generated from the Swagger spec for SwaggerPetstore.
|
|
|
|
|
type SwaggerPetstoreAPI
|
|
|
|
|
= "pet" :> ReqBody '[JSON] Pet :> Verb 'POST 200 '[JSON] () -- 'addPet' route
|
|
|
|
|
:<|> "pet" :> Capture "petId" Integer :> Header "api_key" Text :> Verb 'DELETE 200 '[JSON] () -- 'deletePet' route
|
|
|
|
|
:<|> "pet" :> "findByStatus" :> QueryParam "status" (QueryList 'CommaSeparated (Text)) :> Verb 'GET 200 '[JSON] [Pet] -- 'findPetsByStatus' route
|
|
|
|
|
:<|> "pet" :> "findByTags" :> QueryParam "tags" (QueryList 'CommaSeparated (Text)) :> Verb 'GET 200 '[JSON] [Pet] -- 'findPetsByTags' route
|
|
|
|
|
:<|> "pet" :> Capture "petId" Integer :> Verb 'GET 200 '[JSON] Pet -- 'getPetById' route
|
|
|
|
|
:<|> "pet" :> ReqBody '[JSON] Pet :> Verb 'PUT 200 '[JSON] () -- 'updatePet' route
|
|
|
|
|
:<|> "pet" :> Capture "petId" Integer :> ReqBody '[FormUrlEncoded] FormUpdatePetWithForm :> Verb 'POST 200 '[JSON] () -- 'updatePetWithForm' route
|
|
|
|
|
:<|> "pet" :> Capture "petId" Integer :> "uploadImage" :> ReqBody '[FormUrlEncoded] FormUploadFile :> Verb 'POST 200 '[JSON] ApiResponse -- 'uploadFile' route
|
|
|
|
|
:<|> "store" :> "order" :> Capture "orderId" Text :> Verb 'DELETE 200 '[JSON] () -- 'deleteOrder' route
|
|
|
|
|
:<|> "store" :> "inventory" :> Verb 'GET 200 '[JSON] (Map.Map String Int) -- 'getInventory' route
|
|
|
|
|
:<|> "store" :> "order" :> Capture "orderId" Integer :> Verb 'GET 200 '[JSON] Order -- 'getOrderById' route
|
|
|
|
|
:<|> "store" :> "order" :> ReqBody '[JSON] Order :> Verb 'POST 200 '[JSON] Order -- 'placeOrder' route
|
|
|
|
|
:<|> "user" :> ReqBody '[JSON] User :> Verb 'POST 200 '[JSON] () -- 'createUser' route
|
|
|
|
|
:<|> "user" :> "createWithArray" :> ReqBody '[JSON] [User] :> Verb 'POST 200 '[JSON] () -- 'createUsersWithArrayInput' route
|
|
|
|
|
:<|> "user" :> "createWithList" :> ReqBody '[JSON] [User] :> Verb 'POST 200 '[JSON] () -- 'createUsersWithListInput' route
|
|
|
|
|
:<|> "user" :> Capture "username" Text :> Verb 'DELETE 200 '[JSON] () -- 'deleteUser' route
|
|
|
|
|
:<|> "user" :> Capture "username" Text :> Verb 'GET 200 '[JSON] User -- 'getUserByName' route
|
|
|
|
|
:<|> "user" :> "login" :> QueryParam "username" Text :> QueryParam "password" Text :> Verb 'GET 200 '[JSON] Text -- 'loginUser' route
|
|
|
|
|
:<|> "user" :> "logout" :> Verb 'GET 200 '[JSON] () -- 'logoutUser' route
|
|
|
|
|
:<|> "user" :> Capture "username" Text :> ReqBody '[JSON] User :> Verb 'PUT 200 '[JSON] () -- 'updateUser' route
|
|
|
|
|
|
|
|
|
|
-- | Server or client configuration, specifying the host and port to query or serve on.
|
|
|
|
|
data ServerConfig = ServerConfig {
|
|
|
|
|
configHost :: String, -- ^ Hostname to serve on, e.g. "127.0.0.1"
|
|
|
|
|
configPort :: Int -- ^ Port to serve on, e.g. 8080
|
|
|
|
|
} deriving (Eq, Ord, Show, Read)
|
|
|
|
|
|
|
|
|
|
-- | List of elements parsed from a query.
|
|
|
|
|
newtype QueryList (p :: CollectionFormat) a = QueryList { fromQueryList :: [a] }
|
|
|
|
|
deriving (Functor, Applicative, Monad, Foldable, Traversable)
|
|
|
|
|
|
|
|
|
|
-- | Formats in which a list can be encoded into a HTTP path.
|
|
|
|
|
data CollectionFormat = CommaSeparated -- ^ CSV format for multiple parameters.
|
|
|
|
|
| SpaceSeparated -- ^ Also called "SSV"
|
|
|
|
|
| TabSeparated -- ^ Also called "TSV"
|
|
|
|
|
| PipeSeparated -- ^ `value1|value2|value2`
|
|
|
|
|
| MultiParamArray -- ^ Using multiple GET parameters, e.g. `foo=bar&foo=baz`. Only for GET params.
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData a => FromHttpApiData (QueryList 'CommaSeparated a) where
|
|
|
|
|
parseQueryParam = parseSeparatedQueryList ','
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData a => FromHttpApiData (QueryList 'TabSeparated a) where
|
|
|
|
|
parseQueryParam = parseSeparatedQueryList '\t'
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData a => FromHttpApiData (QueryList 'SpaceSeparated a) where
|
|
|
|
|
parseQueryParam = parseSeparatedQueryList ' '
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData a => FromHttpApiData (QueryList 'PipeSeparated a) where
|
|
|
|
|
parseQueryParam = parseSeparatedQueryList '|'
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData a => FromHttpApiData (QueryList 'MultiParamArray a) where
|
|
|
|
|
parseQueryParam = error "unimplemented FromHttpApiData for MultiParamArray collection format"
|
|
|
|
|
|
|
|
|
|
parseSeparatedQueryList :: FromHttpApiData a => Char -> Text -> Either Text (QueryList p a)
|
|
|
|
|
parseSeparatedQueryList char = fmap QueryList . mapM parseQueryParam . T.split (== char)
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData a => ToHttpApiData (QueryList 'CommaSeparated a) where
|
|
|
|
|
toQueryParam = formatSeparatedQueryList ','
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData a => ToHttpApiData (QueryList 'TabSeparated a) where
|
|
|
|
|
toQueryParam = formatSeparatedQueryList '\t'
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData a => ToHttpApiData (QueryList 'SpaceSeparated a) where
|
|
|
|
|
toQueryParam = formatSeparatedQueryList ' '
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData a => ToHttpApiData (QueryList 'PipeSeparated a) where
|
|
|
|
|
toQueryParam = formatSeparatedQueryList '|'
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData a => ToHttpApiData (QueryList 'MultiParamArray a) where
|
|
|
|
|
toQueryParam = error "unimplemented ToHttpApiData for MultiParamArray collection format"
|
|
|
|
|
|
|
|
|
|
formatSeparatedQueryList :: ToHttpApiData a => Char -> QueryList p a -> Text
|
|
|
|
|
formatSeparatedQueryList char = T.intercalate (T.singleton char) . map toQueryParam . fromQueryList
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- | Backend for SwaggerPetstore.
|
|
|
|
|
-- The backend can be used both for the client and the server. The client generated from the SwaggerPetstore Swagger spec
|
|
|
|
|
-- is a backend that executes actions by sending HTTP requests (see @createSwaggerPetstoreClient@). Alternatively, provided
|
|
|
|
|
-- a backend, the API can be served using @runSwaggerPetstoreServer@.
|
|
|
|
|
data SwaggerPetstoreBackend m = SwaggerPetstoreBackend {
|
|
|
|
|
addPet :: Pet -> m (){- ^ -},
|
|
|
|
|
deletePet :: Integer -> Maybe Text -> m (){- ^ -},
|
|
|
|
|
findPetsByStatus :: Maybe [Text] -> m [Pet]{- ^ Multiple status values can be provided with comma separated strings -},
|
|
|
|
|
findPetsByTags :: Maybe [Text] -> m [Pet]{- ^ Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. -},
|
|
|
|
|
getPetById :: Integer -> m Pet{- ^ Returns a single pet -},
|
|
|
|
|
updatePet :: Pet -> m (){- ^ -},
|
|
|
|
|
updatePetWithForm :: Integer -> FormUpdatePetWithForm -> m (){- ^ -},
|
|
|
|
|
uploadFile :: Integer -> FormUploadFile -> m ApiResponse{- ^ -},
|
|
|
|
|
deleteOrder :: Text -> m (){- ^ For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors -},
|
|
|
|
|
getInventory :: m (Map.Map String Int){- ^ Returns a map of status codes to quantities -},
|
|
|
|
|
getOrderById :: Integer -> m Order{- ^ For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions -},
|
|
|
|
|
placeOrder :: Order -> m Order{- ^ -},
|
|
|
|
|
createUser :: User -> m (){- ^ This can only be done by the logged in user. -},
|
|
|
|
|
createUsersWithArrayInput :: [User] -> m (){- ^ -},
|
|
|
|
|
createUsersWithListInput :: [User] -> m (){- ^ -},
|
|
|
|
|
deleteUser :: Text -> m (){- ^ This can only be done by the logged in user. -},
|
|
|
|
|
getUserByName :: Text -> m User{- ^ -},
|
|
|
|
|
loginUser :: Maybe Text -> Maybe Text -> m Text{- ^ -},
|
|
|
|
|
logoutUser :: m (){- ^ -},
|
|
|
|
|
updateUser :: Text -> User -> m (){- ^ This can only be done by the logged in user. -}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newtype SwaggerPetstoreClient a = SwaggerPetstoreClient { runClient :: Manager -> BaseUrl -> ExceptT ServantError IO a }
|
|
|
|
|
deriving Functor
|
|
|
|
|
|
|
|
|
|
instance Applicative SwaggerPetstoreClient where
|
|
|
|
|
pure x = SwaggerPetstoreClient (\_ _ -> pure x)
|
|
|
|
|
(SwaggerPetstoreClient f) <*> (SwaggerPetstoreClient x) = SwaggerPetstoreClient (\manager url -> f manager url <*> x manager url)
|
|
|
|
|
|
|
|
|
|
instance Monad SwaggerPetstoreClient where
|
|
|
|
|
(SwaggerPetstoreClient a) >>= f = SwaggerPetstoreClient (\manager url -> do
|
|
|
|
|
value <- a manager url
|
|
|
|
|
runClient (f value) manager url)
|
|
|
|
|
|
|
|
|
|
instance MonadIO SwaggerPetstoreClient where
|
|
|
|
|
liftIO io = SwaggerPetstoreClient (\_ _ -> liftIO io)
|
|
|
|
|
|
|
|
|
|
createSwaggerPetstoreClient :: SwaggerPetstoreBackend SwaggerPetstoreClient
|
|
|
|
|
createSwaggerPetstoreClient = SwaggerPetstoreBackend{..}
|
|
|
|
|
where
|
|
|
|
|
((coerce -> addPet) :<|>
|
|
|
|
|
(coerce -> deletePet) :<|>
|
|
|
|
|
(coerce -> findPetsByStatus) :<|>
|
|
|
|
|
(coerce -> findPetsByTags) :<|>
|
|
|
|
|
(coerce -> getPetById) :<|>
|
|
|
|
|
(coerce -> updatePet) :<|>
|
|
|
|
|
(coerce -> updatePetWithForm) :<|>
|
|
|
|
|
(coerce -> uploadFile) :<|>
|
|
|
|
|
(coerce -> deleteOrder) :<|>
|
|
|
|
|
(coerce -> getInventory) :<|>
|
|
|
|
|
(coerce -> getOrderById) :<|>
|
|
|
|
|
(coerce -> placeOrder) :<|>
|
|
|
|
|
(coerce -> createUser) :<|>
|
|
|
|
|
(coerce -> createUsersWithArrayInput) :<|>
|
|
|
|
|
(coerce -> createUsersWithListInput) :<|>
|
|
|
|
|
(coerce -> deleteUser) :<|>
|
|
|
|
|
(coerce -> getUserByName) :<|>
|
|
|
|
|
(coerce -> loginUser) :<|>
|
|
|
|
|
(coerce -> logoutUser) :<|>
|
|
|
|
|
(coerce -> updateUser)) = client (Proxy :: Proxy SwaggerPetstoreAPI)
|
|
|
|
|
|
|
|
|
|
-- | Run requests in the SwaggerPetstoreClient monad.
|
|
|
|
|
runSwaggerPetstoreClient :: ServerConfig -> SwaggerPetstoreClient a -> ExceptT ServantError IO a
|
|
|
|
|
runSwaggerPetstoreClient clientConfig cl = do
|
|
|
|
|
manager <- liftIO $ newManager defaultManagerSettings
|
|
|
|
|
runSwaggerPetstoreClientWithManager manager clientConfig cl
|
|
|
|
|
|
|
|
|
|
-- | Run requests in the SwaggerPetstoreClient monad using a custom manager.
|
|
|
|
|
runSwaggerPetstoreClientWithManager :: Manager -> ServerConfig -> SwaggerPetstoreClient a -> ExceptT ServantError IO a
|
|
|
|
|
runSwaggerPetstoreClientWithManager manager clientConfig cl =
|
|
|
|
|
runClient cl manager $ BaseUrl Http (configHost clientConfig) (configPort clientConfig) ""
|
|
|
|
|
|
|
|
|
|
-- | Run the SwaggerPetstore server at the provided host and port.
|
|
|
|
|
runSwaggerPetstoreServer :: MonadIO m => ServerConfig -> SwaggerPetstoreBackend (ExceptT ServantErr IO) -> m ()
|
|
|
|
|
runSwaggerPetstoreServer ServerConfig{..} backend =
|
|
|
|
|
liftIO $ Warp.runSettings warpSettings $ serve (Proxy :: Proxy SwaggerPetstoreAPI) (serverFromBackend backend)
|
|
|
|
|
|
|
|
|
|
where
|
|
|
|
|
warpSettings = Warp.defaultSettings & Warp.setPort configPort & Warp.setHost (fromString configHost)
|
|
|
|
|
serverFromBackend SwaggerPetstoreBackend{..} =
|
|
|
|
|
(coerce addPet :<|>
|
|
|
|
|
coerce deletePet :<|>
|
|
|
|
|
coerce findPetsByStatus :<|>
|
|
|
|
|
coerce findPetsByTags :<|>
|
|
|
|
|
coerce getPetById :<|>
|
|
|
|
|
coerce updatePet :<|>
|
|
|
|
|
coerce updatePetWithForm :<|>
|
|
|
|
|
coerce uploadFile :<|>
|
|
|
|
|
coerce deleteOrder :<|>
|
|
|
|
|
coerce getInventory :<|>
|
|
|
|
|
coerce getOrderById :<|>
|
|
|
|
|
coerce placeOrder :<|>
|
|
|
|
|
coerce createUser :<|>
|
|
|
|
|
coerce createUsersWithArrayInput :<|>
|
|
|
|
|
coerce createUsersWithListInput :<|>
|
|
|
|
|
coerce deleteUser :<|>
|
|
|
|
|
coerce getUserByName :<|>
|
|
|
|
|
coerce loginUser :<|>
|
|
|
|
|
coerce logoutUser :<|>
|
|
|
|
|
coerce updateUser)
|