{-# LANGUAGE ExistentialQuantification #-}
module Ecluse.Core.Server.Contract (
BodySchema (..),
RequestSpec (..),
ResponseStatus (..),
ResponseDoc (..),
ResponseContract,
responseDocs,
responseToWai,
bodilessContract,
ResponseValue,
responseValue,
jsonContract,
mediaJsonContract,
documentedJsonContract,
mediaContract,
optionalBodyContract,
emptyContract,
VariableResponse,
variableResponse,
variableOpaqueContract,
PassthroughBody (..),
PassthroughResponse,
passthroughResponse,
passthroughContract,
ResponseChoice (..),
chooseContract,
encodeBody,
) where
import Autodocodec (JSONCodec, toJSONVia)
import Data.Aeson qualified as Aeson
import Network.HTTP.Types (Header, Status, hContentType)
import Network.Wai (Response, StreamingBody, responseLBS, responseStream)
data BodySchema
=
SchemaEmpty
|
SchemaOpaque ByteString
|
SchemaText ByteString
|
forall a. SchemaJson ByteString (JSONCodec a)
|
SchemaDocumented ByteString Text
|
SchemaPassthrough
data RequestSpec = RequestSpec
{ RequestSpec -> Text
reqDescription :: Text
, RequestSpec -> Bool
reqRequired :: Bool
, RequestSpec -> BodySchema
reqSchema :: BodySchema
}
data ResponseStatus
= ExactResponse Status
| DefaultResponse
deriving stock (ResponseStatus -> ResponseStatus -> Bool
(ResponseStatus -> ResponseStatus -> Bool)
-> (ResponseStatus -> ResponseStatus -> Bool) -> Eq ResponseStatus
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ResponseStatus -> ResponseStatus -> Bool
== :: ResponseStatus -> ResponseStatus -> Bool
$c/= :: ResponseStatus -> ResponseStatus -> Bool
/= :: ResponseStatus -> ResponseStatus -> Bool
Eq, Int -> ResponseStatus -> ShowS
[ResponseStatus] -> ShowS
ResponseStatus -> String
(Int -> ResponseStatus -> ShowS)
-> (ResponseStatus -> String)
-> ([ResponseStatus] -> ShowS)
-> Show ResponseStatus
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ResponseStatus -> ShowS
showsPrec :: Int -> ResponseStatus -> ShowS
$cshow :: ResponseStatus -> String
show :: ResponseStatus -> String
$cshowList :: [ResponseStatus] -> ShowS
showList :: [ResponseStatus] -> ShowS
Show)
data ResponseDoc = ResponseDoc
{ ResponseDoc -> ResponseStatus
responseStatus :: ResponseStatus
, ResponseDoc -> Text
responseDescription :: Text
, ResponseDoc -> BodySchema
responseBodySchema :: BodySchema
}
data Answer = Answer Status [Header] AnswerBody
data AnswerBody
= MediaAnswer ByteString LByteString
| RawAnswer LByteString
| RawStreamAnswer StreamingBody
| NoAnswerBody
data ResponseContract response = ResponseContract
{ forall response. ResponseContract response -> [ResponseDoc]
contractDocs :: [ResponseDoc]
, forall response. ResponseContract response -> response -> Answer
contractRender :: response -> Answer
}
responseDocs :: ResponseContract response -> [ResponseDoc]
responseDocs :: forall response. ResponseContract response -> [ResponseDoc]
responseDocs = ResponseContract response -> [ResponseDoc]
forall response. ResponseContract response -> [ResponseDoc]
contractDocs
data ResponseValue a = ResponseValue [Header] a
responseValue :: [Header] -> a -> ResponseValue a
responseValue :: forall a. [Header] -> a -> ResponseValue a
responseValue = [Header] -> a -> ResponseValue a
forall a. [Header] -> a -> ResponseValue a
ResponseValue
data ResponseChoice a b
= FirstResponse a
| SecondResponse b
chooseContract :: ResponseContract a -> ResponseContract b -> ResponseContract (ResponseChoice a b)
chooseContract :: forall a b.
ResponseContract a
-> ResponseContract b -> ResponseContract (ResponseChoice a b)
chooseContract ResponseContract a
left ResponseContract b
right =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = ResponseContract a -> [ResponseDoc]
forall response. ResponseContract response -> [ResponseDoc]
contractDocs ResponseContract a
left [ResponseDoc] -> [ResponseDoc] -> [ResponseDoc]
forall a. Semigroup a => a -> a -> a
<> ResponseContract b -> [ResponseDoc]
forall response. ResponseContract response -> [ResponseDoc]
contractDocs ResponseContract b
right
, contractRender :: ResponseChoice a b -> Answer
contractRender = \case
FirstResponse a
value -> ResponseContract a -> a -> Answer
forall response. ResponseContract response -> response -> Answer
contractRender ResponseContract a
left a
value
SecondResponse b
value -> ResponseContract b -> b -> Answer
forall response. ResponseContract response -> response -> Answer
contractRender ResponseContract b
right b
value
}
jsonContract :: Status -> Text -> JSONCodec a -> ResponseContract (ResponseValue a)
jsonContract :: forall a.
Status -> Text -> JSONCodec a -> ResponseContract (ResponseValue a)
jsonContract = ByteString
-> Status
-> Text
-> JSONCodec a
-> ResponseContract (ResponseValue a)
forall a.
ByteString
-> Status
-> Text
-> JSONCodec a
-> ResponseContract (ResponseValue a)
mediaJsonContract ByteString
applicationJson
mediaJsonContract :: ByteString -> Status -> Text -> JSONCodec a -> ResponseContract (ResponseValue a)
mediaJsonContract :: forall a.
ByteString
-> Status
-> Text
-> JSONCodec a
-> ResponseContract (ResponseValue a)
mediaJsonContract ByteString
media Status
status Text
description JSONCodec a
codec =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description (ByteString -> JSONCodec a -> BodySchema
forall a. ByteString -> JSONCodec a -> BodySchema
SchemaJson ByteString
media JSONCodec a
codec)]
, contractRender :: ResponseValue a -> Answer
contractRender = \(ResponseValue [Header]
headers a
value) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (ByteString -> ByteString -> AnswerBody
MediaAnswer ByteString
media (JSONCodec a -> a -> ByteString
forall a. JSONCodec a -> a -> ByteString
encodeBody JSONCodec a
codec a
value))
}
documentedJsonContract :: Status -> Text -> Text -> ResponseContract (ResponseValue LByteString)
documentedJsonContract :: Status
-> Text -> Text -> ResponseContract (ResponseValue ByteString)
documentedJsonContract Status
status Text
description Text
schema =
Status
-> Text
-> BodySchema
-> ResponseContract (ResponseValue ByteString)
mediaContract Status
status Text
description (ByteString -> Text -> BodySchema
SchemaDocumented ByteString
applicationJson Text
schema)
mediaContract :: Status -> Text -> BodySchema -> ResponseContract (ResponseValue LByteString)
mediaContract :: Status
-> Text
-> BodySchema
-> ResponseContract (ResponseValue ByteString)
mediaContract Status
status Text
description BodySchema
schema =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description BodySchema
schema]
, contractRender :: ResponseValue ByteString -> Answer
contractRender = \(ResponseValue [Header]
headers ByteString
bytes) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (BodySchema -> ByteString -> AnswerBody
mediaAnswer BodySchema
schema ByteString
bytes)
}
optionalBodyContract :: Status -> Text -> BodySchema -> ResponseContract (ResponseValue (Maybe LByteString))
optionalBodyContract :: Status
-> Text
-> BodySchema
-> ResponseContract (ResponseValue (Maybe ByteString))
optionalBodyContract Status
status Text
description BodySchema
schema =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description BodySchema
schema]
, contractRender :: ResponseValue (Maybe ByteString) -> Answer
contractRender = \(ResponseValue [Header]
headers Maybe ByteString
body) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (AnswerBody
-> (ByteString -> AnswerBody) -> Maybe ByteString -> AnswerBody
forall b a. b -> (a -> b) -> Maybe a -> b
maybe AnswerBody
NoAnswerBody (BodySchema -> ByteString -> AnswerBody
mediaAnswer BodySchema
schema) Maybe ByteString
body)
}
mediaAnswer :: BodySchema -> LByteString -> AnswerBody
mediaAnswer :: BodySchema -> ByteString -> AnswerBody
mediaAnswer BodySchema
schema ByteString
bytes = AnswerBody
-> (ByteString -> AnswerBody) -> Maybe ByteString -> AnswerBody
forall b a. b -> (a -> b) -> Maybe a -> b
maybe AnswerBody
NoAnswerBody (ByteString -> ByteString -> AnswerBody
`MediaAnswer` ByteString
bytes) (BodySchema -> Maybe ByteString
bodyMediaType BodySchema
schema)
bodyMediaType :: BodySchema -> Maybe ByteString
bodyMediaType :: BodySchema -> Maybe ByteString
bodyMediaType = \case
BodySchema
SchemaEmpty -> Maybe ByteString
forall a. Maybe a
Nothing
SchemaOpaque ByteString
media -> ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
media
SchemaText ByteString
media -> ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
media
SchemaJson ByteString
media JSONCodec a
_ -> ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
media
SchemaDocumented ByteString
media Text
_ -> ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just ByteString
media
BodySchema
SchemaPassthrough -> Maybe ByteString
forall a. Maybe a
Nothing
applicationJson :: ByteString
applicationJson :: ByteString
applicationJson = ByteString
"application/json"
emptyContract :: Status -> Text -> ResponseContract (ResponseValue ())
emptyContract :: Status -> Text -> ResponseContract (ResponseValue ())
emptyContract Status
status Text
description =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description BodySchema
SchemaEmpty]
, contractRender :: ResponseValue () -> Answer
contractRender = \(ResponseValue [Header]
headers ()) -> Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers AnswerBody
NoAnswerBody
}
data VariableResponse a = VariableResponse Status [Header] a
variableResponse :: Status -> [Header] -> a -> VariableResponse a
variableResponse :: forall a. Status -> [Header] -> a -> VariableResponse a
variableResponse = Status -> [Header] -> a -> VariableResponse a
forall a. Status -> [Header] -> a -> VariableResponse a
VariableResponse
variableOpaqueContract :: ByteString -> Text -> ResponseContract (VariableResponse LByteString)
variableOpaqueContract :: ByteString
-> Text -> ResponseContract (VariableResponse ByteString)
variableOpaqueContract ByteString
media Text
description =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc ResponseStatus
DefaultResponse Text
description (ByteString -> BodySchema
SchemaOpaque ByteString
media)]
, contractRender :: VariableResponse ByteString -> Answer
contractRender = \(VariableResponse Status
status [Header]
headers ByteString
bytes) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (ByteString -> ByteString -> AnswerBody
MediaAnswer ByteString
media ByteString
bytes)
}
data PassthroughBody
= PassthroughBytes LByteString
| PassthroughStream StreamingBody
| PassthroughEmpty
data PassthroughResponse = PassthroughResponse Status [Header] PassthroughBody
passthroughResponse :: Status -> [Header] -> PassthroughBody -> PassthroughResponse
passthroughResponse :: Status -> [Header] -> PassthroughBody -> PassthroughResponse
passthroughResponse = Status -> [Header] -> PassthroughBody -> PassthroughResponse
PassthroughResponse
passthroughContract :: Text -> ResponseContract PassthroughResponse
passthroughContract :: Text -> ResponseContract PassthroughResponse
passthroughContract Text
description =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc ResponseStatus
DefaultResponse Text
description BodySchema
SchemaPassthrough]
, contractRender :: PassthroughResponse -> Answer
contractRender = \(PassthroughResponse Status
status [Header]
headers PassthroughBody
body) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (AnswerBody -> Answer) -> AnswerBody -> Answer
forall a b. (a -> b) -> a -> b
$ case PassthroughBody
body of
PassthroughBytes ByteString
bytes -> ByteString -> AnswerBody
RawAnswer ByteString
bytes
PassthroughStream StreamingBody
stream -> StreamingBody -> AnswerBody
RawStreamAnswer StreamingBody
stream
PassthroughBody
PassthroughEmpty -> AnswerBody
NoAnswerBody
}
bodilessContract :: ResponseContract response -> ResponseContract response
bodilessContract :: forall response.
ResponseContract response -> ResponseContract response
bodilessContract ResponseContract response
contract =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = (ResponseDoc -> ResponseDoc) -> [ResponseDoc] -> [ResponseDoc]
forall a b. (a -> b) -> [a] -> [b]
map ResponseDoc -> ResponseDoc
withoutDocumentedBody (ResponseContract response -> [ResponseDoc]
forall response. ResponseContract response -> [ResponseDoc]
contractDocs ResponseContract response
contract)
, contractRender :: response -> Answer
contractRender = Answer -> Answer
withoutAnswerBody (Answer -> Answer) -> (response -> Answer) -> response -> Answer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ResponseContract response -> response -> Answer
forall response. ResponseContract response -> response -> Answer
contractRender ResponseContract response
contract
}
where
withoutDocumentedBody :: ResponseDoc -> ResponseDoc
withoutDocumentedBody ResponseDoc
doc = ResponseDoc
doc{responseBodySchema = SchemaEmpty}
responseToWai :: ResponseContract response -> response -> Response
responseToWai :: forall response. ResponseContract response -> response -> Response
responseToWai ResponseContract response
contract = Answer -> Response
answerToResponse (Answer -> Response)
-> (response -> Answer) -> response -> Response
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ResponseContract response -> response -> Answer
forall response. ResponseContract response -> response -> Answer
contractRender ResponseContract response
contract
encodeBody :: JSONCodec a -> a -> LByteString
encodeBody :: forall a. JSONCodec a -> a -> ByteString
encodeBody JSONCodec a
codec = Value -> ByteString
forall a. ToJSON a => a -> ByteString
Aeson.encode (Value -> ByteString) -> (a -> Value) -> a -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. JSONCodec a -> a -> Value
forall a void. ValueCodec a void -> a -> Value
toJSONVia JSONCodec a
codec
withoutAnswerBody :: Answer -> Answer
withoutAnswerBody :: Answer -> Answer
withoutAnswerBody (Answer Status
status [Header]
headers AnswerBody
body) =
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status (AnswerBody -> [Header]
contentTypeOf AnswerBody
body [Header] -> [Header] -> [Header]
forall a. Semigroup a => a -> a -> a
<> [Header]
headers) AnswerBody
NoAnswerBody
where
contentTypeOf :: AnswerBody -> [Header]
contentTypeOf = \case
MediaAnswer ByteString
media ByteString
_ -> [(HeaderName
hContentType, ByteString
media)]
RawAnswer ByteString
_ -> []
RawStreamAnswer StreamingBody
_ -> []
AnswerBody
NoAnswerBody -> []
answerToResponse :: Answer -> Response
answerToResponse :: Answer -> Response
answerToResponse (Answer Status
status [Header]
headers AnswerBody
body) = case AnswerBody
body of
MediaAnswer ByteString
media ByteString
bytes -> Status -> [Header] -> ByteString -> Response
responseLBS Status
status ((HeaderName
hContentType, ByteString
media) Header -> [Header] -> [Header]
forall a. a -> [a] -> [a]
: [Header]
headers) ByteString
bytes
RawAnswer ByteString
bytes -> Status -> [Header] -> ByteString -> Response
responseLBS Status
status [Header]
headers ByteString
bytes
RawStreamAnswer StreamingBody
stream -> Status -> [Header] -> StreamingBody -> Response
responseStream Status
status [Header]
headers StreamingBody
stream
AnswerBody
NoAnswerBody -> Status -> [Header] -> ByteString -> Response
responseLBS Status
status [Header]
headers ByteString
""