{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} -- | This module contains the utilities to define the data types of the response type of an operation module OpenAPI.Generate.Response ( getResponseDefinitions, ) where import qualified Data.Aeson as Aeson import qualified Data.Either as Either import qualified Data.Map as Map import qualified Data.Maybe as Maybe import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import Language.Haskell.TH import Language.Haskell.TH.PprLib hiding ((<>)) import Language.Haskell.TH.Syntax import qualified Network.HTTP.Client as HC import qualified Network.HTTP.Types as HT import qualified OpenAPI.Generate.Doc as Doc import qualified OpenAPI.Generate.Flags as OAF import OpenAPI.Generate.Internal.Operation import OpenAPI.Generate.Internal.Util import qualified OpenAPI.Generate.Model as Model import qualified OpenAPI.Generate.Monad as OAM import qualified OpenAPI.Generate.Types as OAT -- | Generates a response type with a constructor for all possible response types of the operation. -- -- Always generates an error case which is used if no other case matches. getResponseDefinitions :: -- | The operation to generate the response types for OAT.OperationObject -> -- | A function which appends the passed 'Text' to the operation name and returns it (Text -> Text) -> -- | Returns the name of the reponse data type, the response transformation function and the document containing -- the definitions of all response types. OAM.Generator (Name, Q Exp, Q Doc) getResponseDefinitions operation appendToOperationName = do convertToCamelCase <- OAM.getFlag OAF.optConvertToCamelCase responseSuffix <- OAM.getFlag $ T.pack . OAF.optResponseTypeSuffix responseBodySuffix <- OAM.getFlag $ T.pack . OAF.optResponseBodyTypeSuffix let responsesObject = OAT.responses (operation :: OAT.OperationObject) createBodyName = createResponseNameAsText convertToCamelCase appendToOperationName . (responseBodySuffix <>) createName = createResponseName convertToCamelCase appendToOperationName . (responseSuffix <>) responseName = createName "" responseReferenceCases = getStatusCodeResponseCases responsesObject <> getRangeResponseCases responsesObject responseCases <- resolveResponseReferences responseReferenceCases let responseDescriptions = getResponseDescription . (\(_, _, r) -> r) <$> responseCases schemas <- generateResponseCaseDefinitions createBodyName responseCases pure $ (responseName,createResponseTransformerFn createName schemas,) $ vcat <$> sequence [ pure $ Doc.generateHaddockComment [ "Represents a response of the operation '" <> appendToOperationName "" <> "'.", "", "The response constructor is chosen by the status code of the response. If no case matches (no specific case for the response code, no range case, no default case), '" <> createResponseNameAsText convertToCamelCase appendToOperationName (responseSuffix <> errorSuffix) <> "' is used." ], ( `Doc.sideBySide` (text "" $$ Doc.sideComments ("Means either no matching case available or a parse error" : responseDescriptions)) ) . Doc.breakOnTokensWithReplacement ( \case "=" -> "=\n " token -> "\n " <> token ) ["=", "deriving", "|"] . ppr <$> dataD (cxt []) responseName [] Nothing ( fmap ( \(suffix, _, maybeSchema) -> normalC (createName suffix) ( case maybeSchema of Just (type', _) -> [bangType (bang noSourceUnpackedness noSourceStrictness) type'] Nothing -> [] ) ) ((errorSuffix, [||const True||], Just ([t|String|], (Doc.emptyDoc, Set.empty))) : schemas) ) [derivClause Nothing [conT ''Show, conT ''Eq]], printSchemaDefinitions schemas ] -- | First: suffix to append to the data constructor name -- Second: an expression which can be used to determine if this case should be used in regard to the response status -- Third: Reference or concrete response object type ResponseReferenceCase = (Text, TExpQ (HT.Status -> Bool), OAT.Referencable OAT.ResponseObject) -- | Same as @ResponseReferenceCase@ but with resolved reference type ResponseCase = (Text, TExpQ (HT.Status -> Bool), OAT.ResponseObject) -- | Same as @ResponseReferenceCase@ but with type definition type ResponseCaseDefinition = (Text, TExpQ (HT.Status -> Bool), Maybe Model.TypeWithDeclaration) -- | Suffix used for the error case errorSuffix :: Text errorSuffix = "Error" -- | Create the name as 'Text' of the response type / data constructor based on a suffix createResponseNameAsText :: Bool -> (Text -> Text) -> Text -> Text createResponseNameAsText convertToCamelCase appendToOperationName = T.pack . haskellifyText convertToCamelCase True . appendToOperationName -- | Create the name as 'Name' of the response type / data constructor based on a suffix createResponseName :: Bool -> (Text -> Text) -> Text -> Name createResponseName convertToCamelCase appendToOperationName = mkName . T.unpack . createResponseNameAsText convertToCamelCase appendToOperationName -- | Generate the response cases which have a range instead of a single status code getRangeResponseCases :: OAT.ResponsesObject -> [ResponseReferenceCase] getRangeResponseCases responsesObject = Maybe.catMaybes [ ("1XX",[||HT.statusIsInformational||],) <$> OAT.range1XX responsesObject, ("2XX",[||HT.statusIsSuccessful||],) <$> OAT.range2XX responsesObject, ("3XX",[||HT.statusIsRedirection||],) <$> OAT.range3XX responsesObject, ("4XX",[||HT.statusIsClientError||],) <$> OAT.range4XX responsesObject, ("5XX",[||HT.statusIsServerError||],) <$> OAT.range5XX responsesObject, ("Default",[||const True||],) <$> OAT.default' (responsesObject :: OAT.ResponsesObject) ] -- | Generate the response cases based on the available status codes getStatusCodeResponseCases :: OAT.ResponsesObject -> [ResponseReferenceCase] getStatusCodeResponseCases = fmap (\(code, response) -> (T.pack $ show code, [||\status -> HT.statusCode status == code||], response)) . Map.toList . OAT.perStatusCode -- | Resolve the references in response cases -- -- Note: Discards the unresolved references and generates a log message for them resolveResponseReferences :: [ResponseReferenceCase] -> OAM.Generator [ResponseCase] resolveResponseReferences = fmap Maybe.catMaybes . mapM ( \(suffix, guard, response) -> fmap (suffix,guard,) <$> OAM.nested suffix (getResponseObject response) ) -- | Generate the response definitions -- -- If no response schema is available for a case (or with an unsupported media type), an empty data constructor is used generateResponseCaseDefinitions :: (Text -> Text) -> [ResponseCase] -> OAM.Generator [ResponseCaseDefinition] generateResponseCaseDefinitions createBodyName = mapM ( \(suffix, guard, r) -> OAM.nested suffix $ do responseSchema <- getResponseSchema r (suffix,guard,) <$> mapM (Model.defineModelForSchemaNamed $ createBodyName suffix) responseSchema ) -- | Prints the definitions of the different response case data types in 'Q' printSchemaDefinitions :: [ResponseCaseDefinition] -> Q Doc printSchemaDefinitions = fmap vcat . sequence . Maybe.mapMaybe (\(_, _, namedTypeDef) -> fmap (fst . snd) namedTypeDef) -- | Creates a function as 'Q Exp' which can be used in the generated code to transform the response createResponseTransformerFn :: (Text -> Name) -> [ResponseCaseDefinition] -> Q Exp createResponseTransformerFn createName schemas = let responseArgName = mkName "response" bodyName = mkName "body" ifCases = multiIfE $ fmap ( \(suffix, guard, maybeSchema) -> normalGE [|$(unTypeQ guard) (HC.responseStatus $(varE responseArgName))|] ( case maybeSchema of Just (type', _) -> [|$(varE $ createName suffix) <$> (Aeson.eitherDecodeStrict $(varE bodyName) :: Either String $type')|] Nothing -> [|Right $(varE $ createName suffix)|] ) ) schemas <> [normalGE [|otherwise|] [|Left "Missing default response type"|]] transformLambda = lamE [varP responseArgName, varP bodyName] ifCases in [|fmap (fmap (\response -> fmap (Either.either $(varE $ createName errorSuffix) id . $transformLambda response) response))|] getResponseDescription :: OAT.ResponseObject -> Text getResponseDescription response = Doc.escapeText $ OAT.description (response :: OAT.ResponseObject)