{-# LANGUAGE ExistentialQuantification #-}
module Ecluse.Core.Server.Contract (
BodySchema (..),
RequestSpec (..),
ResponseStatus (..),
ResponseDoc (..),
ResponseContract,
responseDocs,
responseToWai,
bodilessContract,
ResponseValue,
responseValue,
jsonContract,
documentedJsonContract,
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
|
forall a. SchemaJson (JSONCodec a)
|
SchemaDocumented 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 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 Status
status Text
description JSONCodec a
codec =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description (JSONCodec a -> BodySchema
forall a. JSONCodec a -> BodySchema
SchemaJson JSONCodec a
codec)]
, contractRender :: ResponseValue a -> Answer
contractRender = \(ResponseValue [Header]
headers a
value) ->
Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (ByteString -> AnswerBody
JsonAnswer (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 =
ResponseContract
{ contractDocs :: [ResponseDoc]
contractDocs = [ResponseStatus -> Text -> BodySchema -> ResponseDoc
ResponseDoc (Status -> ResponseStatus
ExactResponse Status
status) Text
description (Text -> BodySchema
SchemaDocumented Text
schema)]
, contractRender :: ResponseValue ByteString -> Answer
contractRender = \(ResponseValue [Header]
headers ByteString
bytes) -> Status -> [Header] -> AnswerBody -> Answer
Answer Status
status [Header]
headers (ByteString -> AnswerBody
JsonAnswer ByteString
bytes)
}
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
data Answer = Answer Status [Header] AnswerBody
data AnswerBody
= JsonAnswer LByteString
| MediaAnswer ByteString LByteString
| MediaStreamAnswer ByteString StreamingBody
| RawAnswer LByteString
| RawStreamAnswer StreamingBody
| NoAnswerBody
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
JsonAnswer ByteString
_ -> [(HeaderName
hContentType, ByteString
"application/json")]
MediaAnswer ByteString
media ByteString
_ -> [(HeaderName
hContentType, ByteString
media)]
MediaStreamAnswer ByteString
media StreamingBody
_ -> [(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
JsonAnswer ByteString
bytes -> Status -> [Header] -> ByteString -> Response
responseLBS Status
status ((HeaderName
hContentType, ByteString
"application/json") Header -> [Header] -> [Header]
forall a. a -> [a] -> [a]
: [Header]
headers) ByteString
bytes
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
MediaStreamAnswer ByteString
media StreamingBody
stream -> Status -> [Header] -> StreamingBody -> Response
responseStream Status
status ((HeaderName
hContentType, ByteString
media) Header -> [Header] -> [Header]
forall a. a -> [a] -> [a]
: [Header]
headers) StreamingBody
stream
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
""