mirror of
https://github.com/scarf-sh/tie.git
synced 2024-11-22 02:13:22 +03:00
Initial commit
This commit is contained in:
commit
7aa1cc6353
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
dist-newstyle/
|
||||
.DS_Store
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Revision history for openapi3-server-gen
|
||||
|
||||
## 0.1.0.0 -- YYYY-mm-dd
|
||||
|
||||
* First version. Released on an unsuspecting world.
|
217
src/Tie.hs
Normal file
217
src/Tie.hs
Normal file
@ -0,0 +1,217 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
module Tie
|
||||
( generate,
|
||||
Writer,
|
||||
fileWriter,
|
||||
withTestWriter,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.HashMap.Strict.InsOrd as InsOrd
|
||||
import qualified Data.OpenApi as OpenApi
|
||||
import qualified Data.Set as Set
|
||||
import Data.Yaml (decodeFileThrow)
|
||||
import Prettyprinter (Doc, vsep)
|
||||
import Tie.Codegen.Cabal (codegenCabalFile)
|
||||
import Tie.Codegen.Imports
|
||||
( codegenExtraApiModuleDependencies,
|
||||
codegenModuleHeader,
|
||||
codegenResponseDependencies,
|
||||
codegenSchemaDependencies,
|
||||
)
|
||||
import Tie.Codegen.Operation
|
||||
( codegenOperation,
|
||||
codegenOperations,
|
||||
)
|
||||
import Tie.Codegen.Response (codegenResponseAuxFile, codegenResponses)
|
||||
import Tie.Codegen.Schema (codegenSchema)
|
||||
import Tie.Name
|
||||
( Name,
|
||||
apiHaskellFileName,
|
||||
apiHaskellModuleName,
|
||||
cabalFileName,
|
||||
fromText,
|
||||
responseHaskellFileName,
|
||||
responseHaskellModuleName,
|
||||
toOperationHaskellFileName,
|
||||
toResponseHaskellFileName,
|
||||
toResponseHaskellModuleName,
|
||||
toSchemaHaskellFileName,
|
||||
toSchemaHaskellModuleName,
|
||||
)
|
||||
import Tie.Operation
|
||||
( Operation (..),
|
||||
errors,
|
||||
operationResponseDependencies,
|
||||
operationSchemaDependencies,
|
||||
pathItemsToOperation,
|
||||
)
|
||||
import Tie.Resolve (newResolver)
|
||||
import Tie.Type
|
||||
( Named,
|
||||
Type,
|
||||
namedTypeDependencies,
|
||||
schemaToType,
|
||||
transitiveDependencies,
|
||||
typeDependencies,
|
||||
)
|
||||
import Tie.Writer (Writer, fileWriter, withTestWriter)
|
||||
import Prelude hiding (Type)
|
||||
|
||||
-- | Our own version of nubOrd that both nubs and sorts
|
||||
nubOrd :: Ord a => [a] -> [a]
|
||||
nubOrd = Set.toList . Set.fromList
|
||||
|
||||
-- | Read an OpenAPI spec. Throws in case it can not
|
||||
-- be read or deserialized.
|
||||
readOpenApiSpec ::
|
||||
MonadIO m =>
|
||||
FilePath ->
|
||||
m OpenApi.OpenApi
|
||||
readOpenApiSpec filePath =
|
||||
liftIO (decodeFileThrow filePath)
|
||||
|
||||
-- | Extracts all the schemas form an 'OpenApi.OpenApi'.
|
||||
specSchemas :: OpenApi.OpenApi -> [(Text, OpenApi.Schema)]
|
||||
specSchemas =
|
||||
InsOrd.toList . OpenApi._componentsSchemas . OpenApi._openApiComponents
|
||||
|
||||
specPaths :: OpenApi.OpenApi -> [(FilePath, OpenApi.PathItem)]
|
||||
specPaths =
|
||||
InsOrd.toList . OpenApi._openApiPaths
|
||||
|
||||
specComponents :: OpenApi.OpenApi -> OpenApi.Components
|
||||
specComponents =
|
||||
OpenApi._openApiComponents
|
||||
|
||||
generate :: MonadIO m => Writer m -> FilePath -> m ()
|
||||
generate write inputFile = do
|
||||
-- TODO make configurable
|
||||
let apiName = "OpenAPI"
|
||||
packageName = "open-api"
|
||||
|
||||
openApi <- readOpenApiSpec inputFile
|
||||
|
||||
-- Helper to resolve components in the spec.
|
||||
let resolver =
|
||||
newResolver
|
||||
(specComponents openApi)
|
||||
(\_ -> error "could not resolve reference")
|
||||
|
||||
-- Extract all the Operations from the spec
|
||||
operations <-
|
||||
pathItemsToOperation
|
||||
resolver
|
||||
errors
|
||||
(specPaths openApi)
|
||||
|
||||
-- Only extract the direct, shallow dependencies. This is used to get a precise
|
||||
-- import list for the api and schema modules.
|
||||
let shallow :: Named Type -> [Name]
|
||||
shallow =
|
||||
namedTypeDependencies
|
||||
|
||||
-- Deeply traverse a type and extracts all dependencies. Used to get a list
|
||||
-- of all the things we have to generate.
|
||||
let transitive :: Named Type -> [Name]
|
||||
transitive =
|
||||
transitiveDependencies
|
||||
|
||||
-- Transitive closure of all the referenced Schemas
|
||||
let allReferencedSchemas :: [Name]
|
||||
allReferencedSchemas =
|
||||
foldMap (operationSchemaDependencies transitive) operations
|
||||
|
||||
-- Walk through all the available Schemas and generate code for the
|
||||
-- referenced ones.
|
||||
for_ (specSchemas openApi) $ \(name, schema) -> do
|
||||
let name' = fromText name
|
||||
path = toSchemaHaskellFileName apiName name'
|
||||
header = codegenModuleHeader (toSchemaHaskellModuleName apiName name')
|
||||
when (name' `elem` allReferencedSchemas) $ do
|
||||
type_ <- schemaToType resolver schema
|
||||
let dependencyCode =
|
||||
codegenSchemaDependencies apiName $
|
||||
nubOrd (typeDependencies shallow type_)
|
||||
output <- codegenSchema name' type_
|
||||
write path $
|
||||
vsep
|
||||
[ header,
|
||||
mempty,
|
||||
dependencyCode,
|
||||
mempty,
|
||||
output
|
||||
]
|
||||
|
||||
-- For each Operation, generate data types for the responses.
|
||||
for_ operations $ \operation@Operation {name} -> do
|
||||
let path = toResponseHaskellFileName apiName name
|
||||
header = codegenModuleHeader (toResponseHaskellModuleName apiName name)
|
||||
|
||||
dependencyCode =
|
||||
codegenSchemaDependencies apiName $
|
||||
nubOrd (operationSchemaDependencies shallow operation)
|
||||
responsesCode <- codegenResponses resolver operation
|
||||
write path $
|
||||
vsep
|
||||
[ header,
|
||||
mempty,
|
||||
dependencyCode,
|
||||
mempty,
|
||||
responsesCode
|
||||
]
|
||||
|
||||
-- Generate auxiliary definitions in Response.hs
|
||||
let path = responseHaskellFileName apiName
|
||||
header = codegenModuleHeader (responseHaskellModuleName apiName)
|
||||
|
||||
write path $
|
||||
vsep
|
||||
[ header,
|
||||
mempty,
|
||||
codegenResponseAuxFile
|
||||
]
|
||||
|
||||
-- Generate a single Api.hs module containing the server for the api
|
||||
operationsCode <- codegenOperations resolver operations
|
||||
let path = apiHaskellFileName apiName
|
||||
|
||||
header =
|
||||
codegenModuleHeader (apiHaskellModuleName apiName)
|
||||
schemaDependencyCode =
|
||||
map
|
||||
(codegenSchemaDependencies apiName . nubOrd . operationSchemaDependencies shallow)
|
||||
operations
|
||||
responseDependencyCode =
|
||||
map
|
||||
(codegenResponseDependencies apiName . nubOrd . operationResponseDependencies)
|
||||
operations
|
||||
|
||||
write path $
|
||||
vsep
|
||||
[ header,
|
||||
mempty,
|
||||
codegenExtraApiModuleDependencies apiName,
|
||||
mempty,
|
||||
vsep schemaDependencyCode,
|
||||
mempty,
|
||||
vsep responseDependencyCode,
|
||||
mempty,
|
||||
operationsCode
|
||||
]
|
||||
|
||||
-- Last but not least, generate the Cabal file
|
||||
let allReferencedModules :: [Text]
|
||||
allReferencedModules =
|
||||
nubOrd $
|
||||
map (toSchemaHaskellModuleName apiName) allReferencedSchemas
|
||||
++ foldMap (map (toResponseHaskellModuleName apiName) . operationResponseDependencies) operations
|
||||
++ [ apiHaskellModuleName apiName,
|
||||
responseHaskellModuleName apiName
|
||||
]
|
||||
|
||||
path = cabalFileName packageName
|
||||
write path (codegenCabalFile packageName allReferencedModules)
|
40
src/Tie/Codegen/Cabal.hs
Normal file
40
src/Tie/Codegen/Cabal.hs
Normal file
@ -0,0 +1,40 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Tie.Codegen.Cabal (codegenCabalFile) where
|
||||
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
|
||||
codegenCabalFile :: Text -> [Text] -> Doc ann
|
||||
codegenCabalFile packageName exposedModules =
|
||||
PP.vsep
|
||||
[ "cabal-version:" <+> "3.0",
|
||||
"name:" <+> PP.pretty packageName,
|
||||
"version:" <+> "0.1.0.0",
|
||||
"library" <> PP.line
|
||||
<> PP.indent
|
||||
2
|
||||
( PP.vsep
|
||||
[ "build-depends:" <> PP.line
|
||||
<> PP.indent
|
||||
2
|
||||
( PP.vsep
|
||||
[ "," <+> "aeson",
|
||||
"," <+> "attoparsec",
|
||||
"," <+> "base",
|
||||
"," <+> "ghc-prim",
|
||||
"," <+> "http-api-data",
|
||||
"," <+> "http-types",
|
||||
"," <+> "text",
|
||||
"," <+> "wai"
|
||||
]
|
||||
),
|
||||
"exposed-modules:" <> PP.line
|
||||
<> PP.indent
|
||||
2
|
||||
( PP.vsep
|
||||
(map PP.pretty exposedModules)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
77
src/Tie/Codegen/Imports.hs
Normal file
77
src/Tie/Codegen/Imports.hs
Normal file
@ -0,0 +1,77 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Codegen.Imports
|
||||
( codegenModuleHeader,
|
||||
codegenSchemaDependencies,
|
||||
codegenResponseDependencies,
|
||||
codegenExtraApiModuleDependencies,
|
||||
)
|
||||
where
|
||||
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
import qualified Prettyprinter.Render.Text as PP
|
||||
import Tie.Name
|
||||
( ApiName,
|
||||
Name,
|
||||
responseHaskellModuleName,
|
||||
toResponseHaskellModuleName,
|
||||
toSchemaHaskellModuleName,
|
||||
)
|
||||
|
||||
codegenModuleHeader :: Text -> Doc ann
|
||||
codegenModuleHeader moduleName =
|
||||
"{-#" <+> "LANGUAGE" <+> "DuplicateRecordFields" <+> "#-}"
|
||||
<> PP.line
|
||||
<> "{-#" <+> "LANGUAGE" <+> "OverloadedStrings" <+> "#-}"
|
||||
<> PP.line
|
||||
<> "{-#" <+> "LANGUAGE" <+> "RankNTypes" <+> "#-}"
|
||||
<> PP.line
|
||||
<> "{-#" <+> "LANGUAGE" <+> "RecordWildCards" <+> "#-}"
|
||||
<> PP.line
|
||||
<> "module" <+> PP.pretty moduleName <+> "where"
|
||||
<> PP.line
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Control.Applicative"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Control.Monad.IO.Class"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Aeson"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Aeson.Parser"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Aeson.Types"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Attoparsec.ByteString"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Maybe"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Data.Text"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "GHC.Types"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Network.HTTP.Types"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Network.Wai"
|
||||
<> PP.line
|
||||
<> "import" <+> "qualified" <+> "Web.HttpApiData"
|
||||
|
||||
codegenExtraApiModuleDependencies :: ApiName -> Doc ann
|
||||
codegenExtraApiModuleDependencies apiName =
|
||||
"import" <+> PP.pretty (responseHaskellModuleName apiName)
|
||||
|
||||
codegenSchemaDependencies :: ApiName -> [Name] -> Doc ann
|
||||
codegenSchemaDependencies apiName dependencies =
|
||||
PP.vsep
|
||||
[ "import" <+> PP.pretty (toSchemaHaskellModuleName apiName dependency)
|
||||
| dependency <- dependencies
|
||||
]
|
||||
|
||||
codegenResponseDependencies :: ApiName -> [Name] -> Doc ann
|
||||
codegenResponseDependencies apiName dependencies =
|
||||
PP.vsep
|
||||
[ "import" <+> PP.pretty (toResponseHaskellModuleName apiName dependency)
|
||||
| dependency <- dependencies
|
||||
]
|
253
src/Tie/Codegen/Operation.hs
Normal file
253
src/Tie/Codegen/Operation.hs
Normal file
@ -0,0 +1,253 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Codegen.Operation
|
||||
( codegenOperation,
|
||||
codegenOperations,
|
||||
)
|
||||
where
|
||||
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
import qualified Prettyprinter.Render.Text as PP
|
||||
import Tie.Codegen.Response (codegenResponses)
|
||||
import Tie.Codegen.Schema (codegenFieldType, codegenParamSchema)
|
||||
import Tie.Name
|
||||
( Name,
|
||||
toApiMemberName,
|
||||
toApiResponseTypeName,
|
||||
toParamBinder,
|
||||
toParamName,
|
||||
)
|
||||
import Tie.Operation
|
||||
( Operation (..),
|
||||
Param (..),
|
||||
Path,
|
||||
PathSegment (..),
|
||||
RequestBody (..),
|
||||
Response (..),
|
||||
)
|
||||
import Tie.Resolve (Resolver)
|
||||
|
||||
codegenOperations :: Monad m => Resolver m -> [Operation] -> m (PP.Doc ann)
|
||||
codegenOperations resolver operations = do
|
||||
dataApiDecl <- codegenApiType resolver operations
|
||||
operationsCode <- traverse (codegenOperation resolver) operations
|
||||
let apiDecl =
|
||||
-- TODO instead of "application" take name from openapi spec
|
||||
"application" <+> "::" <+> "(" <> "Control.Monad.IO.Class.MonadIO" <+> "m" <> ")" <+> "=>" <+> "(" <> "forall" <+> "a" <+> "." <+> "Network.Wai.Request" <+> "->" <+> "m"
|
||||
<+> "a"
|
||||
<+> "->"
|
||||
<+> "IO"
|
||||
<+> "a" <> ")"
|
||||
<+> "->"
|
||||
<+> "Api"
|
||||
<+> "m"
|
||||
<+> "->"
|
||||
<+> "Network.Wai.Application"
|
||||
<+> "->"
|
||||
<+> "Network.Wai.Application" <> PP.line
|
||||
<> "application"
|
||||
<+> "run"
|
||||
<+> "api"
|
||||
<+> "notFound"
|
||||
<+> "request"
|
||||
<+> "respond"
|
||||
<+> "=" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "case" <+> "Network.Wai.pathInfo" <+> "request" <+> "of" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.concatWith
|
||||
(\x y -> x <> PP.line <> PP.line <> y)
|
||||
( operationsCode
|
||||
++ [ "_" <+> "->" <> PP.line
|
||||
<> PP.indent 4 ("notFound" <+> "request" <+> "respond")
|
||||
]
|
||||
)
|
||||
)
|
||||
<> PP.line
|
||||
<> "where"
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "unsupportedMethod" <+> "_" <+> "=" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "respond" <+> "(" <> "Network.Wai.responseBuilder"
|
||||
<+> "(" <> "toEnum"
|
||||
<+> "405" <> ")"
|
||||
<+> "[]"
|
||||
<+> "mempty" <> ")"
|
||||
)
|
||||
<> PP.line
|
||||
<> "invalidRequest" <+> "_" <+> "="
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "respond" <+> "(" <> "Network.Wai.responseBuilder"
|
||||
<+> "(" <> "toEnum"
|
||||
<+> "401" <> ")"
|
||||
<+> "[]"
|
||||
<+> "mempty" <> ")"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
pure (dataApiDecl <> PP.line <> PP.line <> apiDecl)
|
||||
|
||||
codegenApiType :: Monad m => Resolver m -> [Operation] -> m (PP.Doc ann)
|
||||
codegenApiType resolver operations = do
|
||||
operationsFieldsCode <- traverse (codegenApiTypeOperation resolver) operations
|
||||
let fieldsCode =
|
||||
PP.concatWith (\x y -> x <> "," <> PP.line <> y) operationsFieldsCode
|
||||
|
||||
dataDecl =
|
||||
"data" <+> "Api" <+> "m" <+> "=" <+> "Api" <+> "{" <> PP.line
|
||||
<> PP.indent 4 fieldsCode
|
||||
<> PP.line
|
||||
<> "}"
|
||||
pure dataDecl
|
||||
|
||||
codegenApiTypeOperation :: Monad m => Resolver m -> Operation -> m (PP.Doc ann)
|
||||
codegenApiTypeOperation resolver Operation {..} = do
|
||||
paramsCode <-
|
||||
sequence
|
||||
[ codegenParamSchema name schema
|
||||
| VariableSegment Param {name, schema} <- path
|
||||
]
|
||||
pure $
|
||||
toApiMemberName name <+> "::"
|
||||
<+> PP.concatWith
|
||||
(\x y -> x <+> "->" <+> y)
|
||||
( paramsCode
|
||||
++ [ codegenFieldType jsonRequestBodyContent
|
||||
| Just RequestBody {jsonRequestBodyContent} <- [requestBody]
|
||||
]
|
||||
++ ["m" <+> toApiResponseTypeName name]
|
||||
)
|
||||
|
||||
codegenOperation :: Monad m => Resolver m -> Operation -> m (PP.Doc ann)
|
||||
codegenOperation resolver operation@Operation {..} = do
|
||||
pure $
|
||||
codegenPathGuard path $
|
||||
codegenMethodGuard
|
||||
[ ( method,
|
||||
codegenRequestBodyGuard
|
||||
requestBody
|
||||
( codegenCallApiMember name path requestBody
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
codegenCallApiMember :: Name -> Path -> Maybe RequestBody -> PP.Doc ann
|
||||
codegenCallApiMember operationName path requestBody =
|
||||
"run" <+> "request" <+> "$" <+> "do" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "response" <+> "<-" <+> toApiMemberName operationName
|
||||
<+> "api"
|
||||
<+> PP.hsep [toParamBinder name | VariableSegment Param {name} <- path]
|
||||
<+> (maybe mempty (\_ -> "body") requestBody)
|
||||
<> PP.line
|
||||
<> "Control.Monad.IO.Class.liftIO"
|
||||
<+> "(" <> "respond"
|
||||
<+> "(" <> "toResponse"
|
||||
<+> "response" <> ")" <> ")"
|
||||
)
|
||||
|
||||
codegenPath :: Monad m => Resolver m -> Path -> m (PP.Doc ann)
|
||||
codegenPath resolver path = do
|
||||
segments <- traverse (codegenSegment resolver) path
|
||||
pure (PP.concatWith (\x y -> x <+> ":>" <+> y) segments)
|
||||
|
||||
-- | Codegen a 'PathSegment'.
|
||||
codegenSegment :: Monad m => Resolver m -> PathSegment Param -> m (PP.Doc ann)
|
||||
codegenSegment _resolver segment = case segment of
|
||||
StaticSegment literal ->
|
||||
pure ("\"" <> PP.pretty literal <> "\"")
|
||||
VariableSegment Param {name, schema} -> do
|
||||
code <- codegenParamSchema name schema
|
||||
let capture =
|
||||
"Capture" <+> "\"" <> toParamName name <> "\"" <+> code
|
||||
pure capture
|
||||
|
||||
codegenPathGuard :: Path -> PP.Doc ann -> PP.Doc ann
|
||||
codegenPathGuard path continuation =
|
||||
codegenPathPattern path <+> "->" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( foldr
|
||||
($)
|
||||
continuation
|
||||
[codegenParamGuard param | VariableSegment param <- path]
|
||||
)
|
||||
|
||||
codegenParamGuard :: Param -> PP.Doc ann -> PP.Doc ann
|
||||
codegenParamGuard Param {name} continuation =
|
||||
"case" <+> "Web.HttpApiData.parseUrlPiece" <+> toParamBinder name <+> "of" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "Left" <+> "_" <+> "->" <+> "invalidRequest" <+> "\"" <> toParamName name <> "\"" <> PP.line
|
||||
<> "Right" <+> toParamBinder name <+> "->"
|
||||
<> PP.line
|
||||
<> PP.indent 4 continuation
|
||||
)
|
||||
|
||||
codegenPathPattern :: Path -> PP.Doc ann
|
||||
codegenPathPattern path =
|
||||
"["
|
||||
<+> PP.concatWith
|
||||
(\x y -> x <> "," <+> y)
|
||||
(map codegenPathSegmentPattern path)
|
||||
<+> "]"
|
||||
|
||||
codegenPathSegmentPattern :: PathSegment Param -> PP.Doc ann
|
||||
codegenPathSegmentPattern segment = case segment of
|
||||
StaticSegment literal ->
|
||||
"\"" <> PP.pretty literal <> "\""
|
||||
VariableSegment Param {name} ->
|
||||
toParamBinder name
|
||||
|
||||
codegenMethodGuard :: [(Text, PP.Doc ann)] -> PP.Doc ann
|
||||
codegenMethodGuard methodBodies =
|
||||
"case" <+> "Network.Wai.requestMethod" <+> "request" <+> "of" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep $
|
||||
[ "\"" <> PP.pretty method <> "\"" <+> "->" <> PP.line <> PP.indent 4 body
|
||||
| (method, body) <- methodBodies
|
||||
]
|
||||
++ [ "x" <+> "->" <> PP.line <> PP.indent 4 ("unsupportedMethod" <+> "x")
|
||||
]
|
||||
)
|
||||
|
||||
codegenRequestBodyGuard :: Maybe RequestBody -> PP.Doc ann -> PP.Doc ann
|
||||
codegenRequestBodyGuard requestBody continuation = case requestBody of
|
||||
Nothing -> continuation
|
||||
Just _body ->
|
||||
"do"
|
||||
<+> PP.align
|
||||
( "result" <+> "<-" <+> "Data.Attoparsec.ByteString.parseWith" <+> "(" <> "Network.Wai.getRequestBodyChunk" <+> "request" <> ")" <+> "Data.Aeson.Parser.json'" <+> "mempty" <> PP.line
|
||||
<> "case" <+> "Data.Attoparsec.ByteString.eitherResult" <+> "result" <+> "of"
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "Left" <+> "_err" <+> "->" <+> "undefined" <> PP.line
|
||||
<> "Right" <+> "bodyValue" <+> "->"
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "case" <+> "Data.Aeson.Types.parseEither" <+> "Data.Aeson.parseJSON" <+> "bodyValue" <+> "of" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "Left" <+> "_err" <+> "->" <+> "undefined" <> PP.line
|
||||
<> "Right" <+> "body" <+> "->"
|
||||
<> PP.line
|
||||
<> PP.indent 4 continuation
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
102
src/Tie/Codegen/Response.hs
Normal file
102
src/Tie/Codegen/Response.hs
Normal file
@ -0,0 +1,102 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Codegen.Response
|
||||
( codegenResponses,
|
||||
codegenResponseAuxFile,
|
||||
)
|
||||
where
|
||||
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
import qualified Prettyprinter.Render.Text as PP
|
||||
import Tie.Codegen.Schema (codegenFieldType, codegenParamSchema)
|
||||
import Tie.Name
|
||||
( Name,
|
||||
toApiDefaultResponseConstructorName,
|
||||
toApiMemberName,
|
||||
toApiResponseConstructorName,
|
||||
toApiResponseTypeName,
|
||||
toParamBinder,
|
||||
toParamName,
|
||||
)
|
||||
import Tie.Operation
|
||||
( Operation (..),
|
||||
Param (..),
|
||||
Path,
|
||||
PathSegment (..),
|
||||
RequestBody (..),
|
||||
Response (..),
|
||||
)
|
||||
import Tie.Resolve (Resolver)
|
||||
|
||||
-- | Generate code for the responses of an 'Operation'.
|
||||
codegenResponses :: Monad m => Resolver m -> Operation -> m (Doc ann)
|
||||
codegenResponses resolver Operation {..} = do
|
||||
let decl =
|
||||
"data" <+> toApiResponseTypeName name <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep $
|
||||
[ op <+> toApiResponseConstructorName name statusCode <+> codegenFieldType jsonResponseContent
|
||||
| (op, (statusCode, Response {jsonResponseContent})) <- zip ("=" : repeat "|") responses
|
||||
]
|
||||
++ [ "|" <+> toApiDefaultResponseConstructorName name
|
||||
<+> "Network.HTTP.Types.Status"
|
||||
<+> codegenFieldType jsonResponseContent
|
||||
| Just Response {jsonResponseContent} <- [defaultResponse]
|
||||
]
|
||||
)
|
||||
instances =
|
||||
codegenToResponses name responses defaultResponse
|
||||
|
||||
pure (PP.vsep [decl, mempty, instances])
|
||||
|
||||
codegenToResponses :: Name -> [(Int, Response)] -> Maybe Response -> Doc ann
|
||||
codegenToResponses operationName responses defaultResponse =
|
||||
let decl =
|
||||
"instance" <+> "ToResponse" <+> toApiResponseTypeName operationName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep $
|
||||
[ "toResponse" <+> "(" <> toApiResponseConstructorName operationName statusCode <+> "x" <> ")"
|
||||
<+> "="
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "Network.Wai.responseBuilder" <+> "(" <> "toEnum" <+> PP.pretty statusCode <> ")"
|
||||
<+> "[(Network.HTTP.Types.hContentType, \"application/json\")]"
|
||||
<+> "(" <> "Data.Aeson.fromEncoding"
|
||||
<+> "(" <> "Data.Aeson.toEncoding"
|
||||
<+> "x" <> ")" <> ")"
|
||||
)
|
||||
| (statusCode, _response) <- responses
|
||||
]
|
||||
++ [ "toResponse" <+> "(" <> toApiDefaultResponseConstructorName operationName <+> "status" <+> "x" <> ")"
|
||||
<+> "="
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "Network.Wai.responseBuilder" <+> "status"
|
||||
<+> "[(Network.HTTP.Types.hContentType, \"application/json\")]"
|
||||
<+> "(" <> "Data.Aeson.fromEncoding"
|
||||
<+> "(" <> "Data.Aeson.toEncoding"
|
||||
<+> "x" <> ")" <> ")"
|
||||
)
|
||||
| Just Response {jsonResponseContent} <- [defaultResponse]
|
||||
]
|
||||
)
|
||||
in decl
|
||||
|
||||
codegenResponseAuxFile :: Doc ann
|
||||
codegenResponseAuxFile =
|
||||
"class"
|
||||
<+> "ToResponse"
|
||||
<+> "a"
|
||||
<+> "where"
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "toResponse" <+> "::" <+> "a" <+> "->" <+> "Network.Wai.Response"
|
||||
)
|
324
src/Tie/Codegen/Schema.hs
Normal file
324
src/Tie/Codegen/Schema.hs
Normal file
@ -0,0 +1,324 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Codegen.Schema
|
||||
( codegenSchema,
|
||||
codegenParamSchema,
|
||||
codegenFieldType,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import qualified Data.HashSet as HashSet
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
import qualified Prettyprinter.Render.Text as PP
|
||||
import Tie.Name
|
||||
( inlineObjectTypeName,
|
||||
inlineVariantTypeName,
|
||||
toConstructorName,
|
||||
toDataTypeName,
|
||||
toFieldName,
|
||||
toFunctionName,
|
||||
toJsonFieldName,
|
||||
toOneOfConstructorName,
|
||||
toOneOfDataTypeName,
|
||||
)
|
||||
import Tie.Type
|
||||
( BasicType (..),
|
||||
Enumeration (..),
|
||||
Name,
|
||||
Named (..),
|
||||
ObjectType (..),
|
||||
Type (..),
|
||||
isArrayType,
|
||||
isBasicType,
|
||||
isEnumType,
|
||||
isObjectType,
|
||||
isOneOfType,
|
||||
namedType,
|
||||
normalizeObjectType,
|
||||
normalizeVariants,
|
||||
)
|
||||
import Prelude hiding (Type)
|
||||
|
||||
-- | Generate code for a parameter type.
|
||||
codegenParamSchema :: Monad m => Name -> Named Type -> m (Doc ann)
|
||||
codegenParamSchema _paramName typ = case typ of
|
||||
Named {} ->
|
||||
-- We are named, just defer to codegenFieldType
|
||||
pure (codegenFieldType typ)
|
||||
Unnamed typ
|
||||
| Just _enumeration <- isEnumType typ ->
|
||||
error "TODO enumeration params"
|
||||
| Just basicType <- isBasicType typ ->
|
||||
pure (codegenFieldType (Unnamed typ))
|
||||
| Just objectType <- isObjectType typ ->
|
||||
error "Invariant broken: ruled out by pathToPath"
|
||||
| otherwise ->
|
||||
undefined
|
||||
|
||||
-- | Generate code for a schema.
|
||||
codegenSchema :: Monad m => Name -> Type -> m (Doc ann)
|
||||
codegenSchema typName typ
|
||||
| Just Enumeration {alternatives, includeNull} <- isEnumType typ =
|
||||
pure (codegenEnumeration typName alternatives includeNull)
|
||||
| Just basicType <- isBasicType typ =
|
||||
pure (codegenBasicType typName basicType)
|
||||
| Just alternatives <- isOneOfType typ =
|
||||
codegenOneOfType typName alternatives
|
||||
| Just objectType <- isObjectType typ =
|
||||
codegenObjectType typName objectType
|
||||
| Just elemType <- isArrayType typ =
|
||||
codegenArrayType typName elemType
|
||||
| otherwise =
|
||||
undefined
|
||||
|
||||
-- | Generate code for basic, primitive types
|
||||
codegenBasicType :: Name -> BasicType -> Doc ann
|
||||
codegenBasicType typName basicType =
|
||||
let typ = case basicType of
|
||||
TyString {} ->
|
||||
"Data.Text.Text"
|
||||
TyEnum {} ->
|
||||
error "Impossible: Enumerations are handled by codegenEnumeration"
|
||||
TyNumber ->
|
||||
"GHC.Types.Double"
|
||||
TyInteger ->
|
||||
"GHC.Types.Int"
|
||||
TyBoolean ->
|
||||
"GHC.Types.Bool"
|
||||
in "type" <+> toDataTypeName typName <+> "=" <+> typ
|
||||
|
||||
codegenArrayType :: Monad m => Name -> Named Type -> m (Doc ann)
|
||||
codegenArrayType name typ = pure mempty
|
||||
|
||||
codegenOneOfType :: Monad m => Name -> [Named Type] -> m (Doc ann)
|
||||
codegenOneOfType typName variants = do
|
||||
-- Extract the inline dependencies, we will codegen them into the same file.
|
||||
(variants, inlineDependencies) <-
|
||||
normalizeVariants
|
||||
( \ith _variantType ->
|
||||
pure (inlineVariantTypeName typName ith)
|
||||
)
|
||||
variants
|
||||
|
||||
-- Generate code for inline dependencies inplace
|
||||
codeForInlineDependencies <-
|
||||
traverse (uncurry codegenSchema) inlineDependencies
|
||||
|
||||
let -- We derive the constructor names upfront. For unnamed types - which can still
|
||||
-- exists after normalization for e.g. basic types - we generate an inline variant
|
||||
-- type name.
|
||||
variantConstructors =
|
||||
[ (name, variant)
|
||||
| (ith, variant) <- zip [1 ..] variants,
|
||||
let name = case variant of
|
||||
Named variantName _ ->
|
||||
toOneOfConstructorName typName variantName
|
||||
Unnamed typ ->
|
||||
toOneOfConstructorName typName (inlineVariantTypeName typName ith)
|
||||
]
|
||||
|
||||
decl =
|
||||
"data" <+> toOneOfDataTypeName typName <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep
|
||||
[ op
|
||||
<+> variantName
|
||||
<+> codegenFieldType variantType
|
||||
| (op, (variantName, variantType)) <- zip ("=" : repeat "|") variantConstructors
|
||||
]
|
||||
)
|
||||
|
||||
toJson =
|
||||
"instance" <+> "Data.Aeson.ToJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep
|
||||
[ "toJSON" <+> "(" <> variantName <+> "x" <> ")" <+> "="
|
||||
<+> "Data.Aeson.toJSON"
|
||||
<+> "x"
|
||||
| (variantName, _) <- variantConstructors
|
||||
]
|
||||
)
|
||||
|
||||
fromJson =
|
||||
"instance" <+> "Data.Aeson.FromJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "parseJSON" <+> "x" <+> "=" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.concatWith
|
||||
(\x y -> x <+> "Control.Applicative.<|>" <> PP.line <> y)
|
||||
[ "(" <> variantName <+> "<$>" <+> "Data.Aeson.parseJSON" <+> "x" <> ")"
|
||||
| (variantName, variantType) <- variantConstructors
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
pure $
|
||||
PP.vsep
|
||||
( (map (<> PP.line) codeForInlineDependencies)
|
||||
++ [decl, mempty, toJson, mempty, fromJson]
|
||||
)
|
||||
|
||||
codegenObjectType :: Monad m => Name -> ObjectType (Named Type) -> m (Doc ann)
|
||||
codegenObjectType typName inputObjectType = do
|
||||
-- Extract the inline dependencies, we will codegen them into the same file.
|
||||
(ObjectType {..}, inlineDependencies) <-
|
||||
normalizeObjectType
|
||||
( \fieldName _inlineObjectType ->
|
||||
pure (inlineObjectTypeName typName fieldName)
|
||||
)
|
||||
inputObjectType
|
||||
|
||||
-- Generate code for inline dependencies inplace
|
||||
codeForInlineDependencies <-
|
||||
traverse (uncurry codegenSchema) inlineDependencies
|
||||
|
||||
-- Now generate for the object itself
|
||||
let orderedProperties =
|
||||
sortOn fst (HashMap.toList properties)
|
||||
|
||||
dataOrNewtype = case orderedProperties of
|
||||
[_] -> "newtype"
|
||||
_ -> "data"
|
||||
|
||||
decl =
|
||||
dataOrNewtype <+> toDataTypeName typName <+> "=" <+> toConstructorName typName <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "{" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.concatWith
|
||||
(\x y -> x <> "," <> PP.line <> y)
|
||||
[ toFieldName field <+> "::"
|
||||
<+> codegenRequiredOptionalFieldType
|
||||
(HashSet.member field requiredProperties)
|
||||
(codegenFieldType fieldType)
|
||||
| (field, fieldType) <- orderedProperties
|
||||
]
|
||||
)
|
||||
<> PP.line
|
||||
<> "}"
|
||||
)
|
||||
|
||||
toJson =
|
||||
"instance" <+> "Data.Aeson.ToJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "toJSON" <+> toConstructorName typName <+> "{..}" <+> "=" <+> "Data.Aeson.object" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "[" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.concatWith
|
||||
(\x y -> x <> "," <> PP.line <> y)
|
||||
[ "\"" <> toJsonFieldName field <> "\"" <+> "Data.Aeson..=" <+> toFieldName field
|
||||
| (field, _) <- orderedProperties
|
||||
]
|
||||
)
|
||||
<> PP.line
|
||||
<> "]"
|
||||
)
|
||||
)
|
||||
|
||||
fromOptOrReq field
|
||||
| HashSet.member field requiredProperties = "Data.Aeson..:"
|
||||
| otherwise = "Data.Aeson..:?"
|
||||
|
||||
fromJson =
|
||||
"instance" <+> "Data.Aeson.FromJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "parseJSON" <+> "=" <+> "Data.Aeson.withObject" <+> "\"" <> toDataTypeName typName <> "\"" <+> "$" <+> "\\" <> "o" <+> "->" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( toConstructorName typName <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep
|
||||
[ op <+> "o" <+> fromOptOrReq fieldName <+> "\"" <> toJsonFieldName fieldName <> "\""
|
||||
| (op, (fieldName, _)) <- zip ("<$>" : repeat "<*>") orderedProperties
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
in pure $
|
||||
PP.vsep
|
||||
( (map (<> PP.line) codeForInlineDependencies)
|
||||
++ [decl, mempty, toJson, mempty, fromJson]
|
||||
)
|
||||
|
||||
codegenRequiredOptionalFieldType :: Bool -> Doc ann -> Doc ann
|
||||
codegenRequiredOptionalFieldType True doc = doc
|
||||
codegenRequiredOptionalFieldType False doc = "Data.Maybe.Maybe" <+> "(" <> doc <> ")"
|
||||
|
||||
codegenFieldType :: Named Type -> Doc ann
|
||||
codegenFieldType namedType = case namedType of
|
||||
Named name _ -> toDataTypeName name
|
||||
Unnamed typ -> case typ of
|
||||
AllOf {} -> "error: allOf"
|
||||
AnyOf {} -> "error: anyOf"
|
||||
OneOf {} -> "error: oneOf"
|
||||
Not {} -> "error: not"
|
||||
Basic basicType -> case basicType of
|
||||
TyString {} -> "Data.Text.Text"
|
||||
TyEnum {} -> "error: Enum"
|
||||
TyNumber -> "GHC.Types.Double"
|
||||
TyInteger -> "GHC.Types.Int"
|
||||
TyBoolean -> "GHC.Types.Bool"
|
||||
Object objectType -> "Data.Aeson.Value"
|
||||
Array elemType -> "[" <+> codegenFieldType elemType <+> "]"
|
||||
|
||||
-- | Generate the Haskell code for enumeration types
|
||||
codegenEnumeration :: Name -> [Text] -> Bool -> Doc ann
|
||||
codegenEnumeration typName alternatives _includeNull =
|
||||
let dataDecl =
|
||||
"data"
|
||||
<+> toDataTypeName typName
|
||||
<> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "="
|
||||
<+> PP.concatWith
|
||||
(\x y -> x <> PP.line <> "|" <+> y)
|
||||
(map PP.pretty alternatives)
|
||||
<> PP.line
|
||||
<> "deriving"
|
||||
<+> "(" <> "Eq" <> ","
|
||||
<+> "Show" <> ")"
|
||||
)
|
||||
toJSON =
|
||||
"instance" <+> "Data.Aeson.ToJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "toJSON" <+> "x" <+> "=" <+> "case" <+> "x" <+> "of" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep
|
||||
[ PP.pretty alt <+> "->" <+> "\"" <+> PP.pretty alt <+> "\""
|
||||
| alt <- alternatives
|
||||
]
|
||||
)
|
||||
)
|
||||
fromJSON =
|
||||
"instance" <+> "Data.Aeson.FromJSON" <+> toDataTypeName typName <+> "where" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( "parseJSON" <+> "=" <+> "withText" <+> "\"" <+> toDataTypeName typName <+> "\"" <+> "$" <+> "\\" <+> "s" <+> "->" <> PP.line
|
||||
<> PP.indent
|
||||
4
|
||||
( PP.vsep
|
||||
[ "\"" <> PP.pretty alt <> "\"" <+> "->" <+> PP.pretty alt
|
||||
| alt <- alternatives
|
||||
]
|
||||
)
|
||||
)
|
||||
in PP.vsep [dataDecl, PP.line, toJSON, PP.line, fromJSON]
|
198
src/Tie/Name.hs
Normal file
198
src/Tie/Name.hs
Normal file
@ -0,0 +1,198 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Tie.Name
|
||||
( PackageName,
|
||||
ApiName,
|
||||
Name,
|
||||
fromText,
|
||||
cabalFileName,
|
||||
toDataTypeName,
|
||||
toOneOfDataTypeName,
|
||||
toOneOfConstructorName,
|
||||
toFunctionName,
|
||||
toConstructorName,
|
||||
toFieldName,
|
||||
toJsonFieldName,
|
||||
toParamName,
|
||||
toParamBinder,
|
||||
toApiTypeName,
|
||||
toSchemaHaskellFileName,
|
||||
toSchemaHaskellModuleName,
|
||||
toOperationHaskellFileName,
|
||||
toOperationHaskellModuleName,
|
||||
toResponseHaskellFileName,
|
||||
toResponseHaskellModuleName,
|
||||
apiHaskellModuleName,
|
||||
apiHaskellFileName,
|
||||
responseHaskellModuleName,
|
||||
responseHaskellFileName,
|
||||
inlineObjectTypeName,
|
||||
inlineVariantTypeName,
|
||||
toApiResponseTypeName,
|
||||
toApiResponseConstructorName,
|
||||
toApiDefaultResponseConstructorName,
|
||||
toApiMemberName,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Char (toLower, toUpper)
|
||||
import qualified Data.Text as Text
|
||||
import qualified Prettyprinter as PP
|
||||
|
||||
-- | Name of the API to generate code for
|
||||
type ApiName = Text
|
||||
|
||||
-- | Cabal package name
|
||||
type PackageName = Text
|
||||
|
||||
-- | Names identify things in the OpenApi universe. Name's are coming directly
|
||||
-- from the OpenApi spec.
|
||||
newtype Name = Name {unName :: Text}
|
||||
deriving (IsString, Eq, Ord, Show, Hashable)
|
||||
|
||||
fromText :: Text -> Name
|
||||
fromText = Name
|
||||
|
||||
cabalFileName :: PackageName -> FilePath
|
||||
cabalFileName packageName =
|
||||
Text.unpack packageName <> ".cabal"
|
||||
|
||||
apiHaskellModuleName :: ApiName -> Text
|
||||
apiHaskellModuleName apiName =
|
||||
apiName <> ".Api"
|
||||
|
||||
apiHaskellFileName :: ApiName -> FilePath
|
||||
apiHaskellFileName apiName =
|
||||
Text.unpack apiName <> "/Api.hs"
|
||||
|
||||
responseHaskellModuleName :: ApiName -> Text
|
||||
responseHaskellModuleName apiName =
|
||||
apiName <> ".Response"
|
||||
|
||||
responseHaskellFileName :: ApiName -> FilePath
|
||||
responseHaskellFileName apiName =
|
||||
Text.unpack apiName <> "/Response.hs"
|
||||
|
||||
toSchemaHaskellModuleName :: ApiName -> Name -> Text
|
||||
toSchemaHaskellModuleName apiName (Name name) =
|
||||
Text.pack $ Text.unpack apiName <> ".Schemas." <> capitalizeFirstLetter (Text.unpack name)
|
||||
|
||||
toSchemaHaskellFileName :: ApiName -> Name -> FilePath
|
||||
toSchemaHaskellFileName apiName (Name name) =
|
||||
Text.unpack apiName <> "/Schemas/" <> capitalizeFirstLetter (Text.unpack name) <> ".hs"
|
||||
|
||||
toOperationHaskellModuleName :: ApiName -> Name -> Text
|
||||
toOperationHaskellModuleName apiName (Name name) =
|
||||
Text.pack $ Text.unpack apiName <> ".Api." <> capitalizeFirstLetter (Text.unpack name)
|
||||
|
||||
toOperationHaskellFileName :: ApiName -> Name -> FilePath
|
||||
toOperationHaskellFileName apiName (Name name) =
|
||||
Text.unpack apiName <> "/Api/" <> capitalizeFirstLetter (Text.unpack name) <> ".hs"
|
||||
|
||||
toResponseHaskellModuleName :: ApiName -> Name -> Text
|
||||
toResponseHaskellModuleName apiName (Name name) =
|
||||
Text.pack $ Text.unpack apiName <> ".Response." <> capitalizeFirstLetter (Text.unpack name)
|
||||
|
||||
toResponseHaskellFileName :: ApiName -> Name -> FilePath
|
||||
toResponseHaskellFileName apiName (Name name) =
|
||||
Text.unpack apiName <> "/Response/" <> capitalizeFirstLetter (Text.unpack name) <> ".hs"
|
||||
|
||||
toApiTypeName :: Name -> PP.Doc ann
|
||||
toApiTypeName =
|
||||
toDataTypeName
|
||||
|
||||
toJsonFieldName :: Name -> PP.Doc ann
|
||||
toJsonFieldName = PP.pretty . unName
|
||||
|
||||
toDataTypeName :: Name -> PP.Doc ann
|
||||
toDataTypeName =
|
||||
PP.pretty . Text.pack . capitalizeFirstLetter . Text.unpack . unName
|
||||
|
||||
toOneOfDataTypeName :: Name -> PP.Doc ann
|
||||
toOneOfDataTypeName =
|
||||
PP.pretty . Text.pack . capitalizeFirstLetter . Text.unpack . unName
|
||||
|
||||
toOneOfConstructorName :: Name -> Name -> PP.Doc ann
|
||||
toOneOfConstructorName (Name oneOfType) (Name variant) =
|
||||
PP.pretty $
|
||||
Text.pack $
|
||||
escapeKeyword $
|
||||
capitalizeFirstLetter (Text.unpack oneOfType)
|
||||
<> capitalizeFirstLetter (Text.unpack variant)
|
||||
|
||||
toConstructorName :: Name -> PP.Doc ann
|
||||
toConstructorName = toDataTypeName
|
||||
|
||||
toFunctionName :: Name -> PP.Doc ann
|
||||
toFunctionName =
|
||||
PP.pretty . Text.pack . escapeKeyword . lowerFirstLetter . Text.unpack . unName
|
||||
|
||||
toFieldName :: Name -> PP.Doc ann
|
||||
toFieldName =
|
||||
PP.pretty . Text.pack . escapeKeyword . lowerFirstLetter . Text.unpack . unName
|
||||
|
||||
toParamName :: Name -> PP.Doc ann
|
||||
toParamName =
|
||||
PP.pretty . unName
|
||||
|
||||
toParamBinder :: Name -> PP.Doc ann
|
||||
toParamBinder =
|
||||
PP.pretty . Text.pack . escapeKeyword . lowerFirstLetter . Text.unpack . unName
|
||||
|
||||
toApiMemberName :: Name -> PP.Doc ann
|
||||
toApiMemberName =
|
||||
PP.pretty . Text.pack . escapeKeyword . lowerFirstLetter . Text.unpack . unName
|
||||
|
||||
toApiResponseTypeName :: Name -> PP.Doc ann
|
||||
toApiResponseTypeName =
|
||||
PP.pretty . Text.pack . escapeKeyword . (<> "Response") . capitalizeFirstLetter . Text.unpack . unName
|
||||
|
||||
toApiResponseConstructorName :: Name -> Int -> PP.Doc ann
|
||||
toApiResponseConstructorName name statusCode =
|
||||
PP.pretty . Text.pack . escapeKeyword . (<> show statusCode) . (<> "Response") . capitalizeFirstLetter . Text.unpack . unName $ name
|
||||
|
||||
toApiDefaultResponseConstructorName :: Name -> PP.Doc ann
|
||||
toApiDefaultResponseConstructorName name =
|
||||
PP.pretty . Text.pack . escapeKeyword . (<> "DefaultResponse") . capitalizeFirstLetter . Text.unpack . unName $ name
|
||||
|
||||
-- | Constructs a name for an object defined inline. Based on the containing data
|
||||
-- type as well as the field name.
|
||||
inlineObjectTypeName :: Name -> Name -> Name
|
||||
inlineObjectTypeName (Name parentType) (Name fieldName) =
|
||||
Name $
|
||||
Text.pack $
|
||||
escapeKeyword $
|
||||
capitalizeFirstLetter (Text.unpack parentType)
|
||||
<> capitalizeFirstLetter (Text.unpack fieldName)
|
||||
|
||||
-- | Construct a name for an inline type in a oneOf.
|
||||
inlineVariantTypeName :: Name -> Int -> Name
|
||||
inlineVariantTypeName (Name parentType) ith =
|
||||
Name $
|
||||
Text.pack $
|
||||
escapeKeyword $
|
||||
capitalizeFirstLetter (Text.unpack parentType) <> "OneOf" <> show ith
|
||||
|
||||
lowerFirstLetter :: String -> String
|
||||
lowerFirstLetter [] = []
|
||||
lowerFirstLetter (x : xs) = toLower x : xs
|
||||
|
||||
capitalizeFirstLetter :: String -> String
|
||||
capitalizeFirstLetter [] = []
|
||||
capitalizeFirstLetter (x : xs) = toUpper x : xs
|
||||
|
||||
escapeKeyword :: String -> String
|
||||
escapeKeyword input = case input of
|
||||
"type" -> "type'"
|
||||
"class" -> "class'"
|
||||
"where" -> "where'"
|
||||
"case" -> "case'"
|
||||
"of" -> "of'"
|
||||
"data" -> "data'"
|
||||
"import" -> "import'"
|
||||
"qualified" -> "qualified'"
|
||||
"as" -> "as'"
|
||||
"instance" -> "instance'"
|
||||
"module" -> "module'"
|
||||
_ -> input
|
330
src/Tie/Operation.hs
Normal file
330
src/Tie/Operation.hs
Normal file
@ -0,0 +1,330 @@
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveTraversable #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Operation
|
||||
( StatusCode,
|
||||
Param (..),
|
||||
RequestBody (..),
|
||||
Response (..),
|
||||
Operation (..),
|
||||
pathItemsToOperation,
|
||||
operationToOperation,
|
||||
|
||||
-- * Errors
|
||||
Errors (..),
|
||||
errors,
|
||||
|
||||
-- * Path
|
||||
Path,
|
||||
PathSegment (..),
|
||||
parsePath,
|
||||
|
||||
-- * Dependencies
|
||||
operationSchemaDependencies,
|
||||
operationResponseDependencies,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.HashMap.Strict.InsOrd as InsOrd
|
||||
import qualified Data.OpenApi as OpenApi
|
||||
import qualified Data.Text as Text
|
||||
import Tie.Name (Name, fromText)
|
||||
import Tie.Resolve (Resolver, resolve)
|
||||
import Tie.Type
|
||||
( Named,
|
||||
Type,
|
||||
isBasicType,
|
||||
namedType,
|
||||
namedTypeDependencies,
|
||||
schemaRefToType,
|
||||
)
|
||||
import Prelude hiding (Type)
|
||||
|
||||
-- | HTTP Status code type
|
||||
type StatusCode = Int
|
||||
|
||||
-- | Request body descriptor
|
||||
data RequestBody = RequestBody
|
||||
{ jsonRequestBodyContent :: Named Type
|
||||
}
|
||||
|
||||
-- | Response descriptor
|
||||
data Response = Response
|
||||
{ -- | JSON schema of the response
|
||||
jsonResponseContent :: Named Type
|
||||
}
|
||||
|
||||
-- | Internal representation of a path.
|
||||
data PathSegment variable
|
||||
= StaticSegment Text
|
||||
| VariableSegment variable
|
||||
deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
|
||||
|
||||
-- | Internal representation of a Path
|
||||
type Path = [PathSegment Param]
|
||||
|
||||
data ParamIn
|
||||
= InPath
|
||||
| InQuery
|
||||
| InHeader
|
||||
| InCookie
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
data Param = Param
|
||||
{ name :: Name,
|
||||
paramIn :: ParamIn,
|
||||
schema :: Named Type,
|
||||
required :: Bool
|
||||
}
|
||||
|
||||
-- | Internal representation for an 'OpenApi.Operation'.
|
||||
data Operation = Operation
|
||||
{ -- | Name of the operation. Used for identifiers.
|
||||
name :: Name,
|
||||
-- | Path
|
||||
path :: Path,
|
||||
-- | HTTP method for this operation (Get, Post, Put, Delete)
|
||||
method :: Text,
|
||||
-- | Type of the request body (if any) for this 'Operation'.
|
||||
requestBody :: Maybe RequestBody,
|
||||
-- | Default response.
|
||||
defaultResponse :: Maybe Response,
|
||||
-- | Responses
|
||||
responses :: [(StatusCode, Response)]
|
||||
}
|
||||
|
||||
data Errors m = Errors
|
||||
{ missingOperationId :: forall a. m a,
|
||||
unsupportedMediaType :: forall a. m a,
|
||||
requestBodyMissingSchema :: forall a. m a,
|
||||
unknownParameter :: forall a. Text -> m a,
|
||||
paramMissingSchema :: forall a. m a,
|
||||
paramNotInPath :: forall a. m a,
|
||||
paramNotBasicType :: forall a. m a
|
||||
}
|
||||
|
||||
errors :: Errors m
|
||||
errors =
|
||||
Errors
|
||||
{ missingOperationId =
|
||||
error "missing operation id",
|
||||
unsupportedMediaType =
|
||||
error "unsupported media type",
|
||||
requestBodyMissingSchema =
|
||||
error "request body missing media type",
|
||||
unknownParameter =
|
||||
error "unknown parameter",
|
||||
paramMissingSchema =
|
||||
error "param missing schema",
|
||||
paramNotInPath =
|
||||
error "param not 'in path' type",
|
||||
paramNotBasicType =
|
||||
error "only basic types are supported for parameters"
|
||||
}
|
||||
|
||||
-- | Returns the dependencies on schemas of an operation. Parameterized so
|
||||
-- that shallow vs. all transitive dependencies can be extracted.
|
||||
operationSchemaDependencies :: (Named Type -> [Name]) -> Operation -> [Name]
|
||||
operationSchemaDependencies getDependencies Operation {..} =
|
||||
concat $
|
||||
[ getDependencies jsonRequestBodyContent
|
||||
| Just RequestBody {jsonRequestBodyContent} <- [requestBody]
|
||||
]
|
||||
++ [ getDependencies jsonResponseContent
|
||||
| Just Response {jsonResponseContent} <- [defaultResponse]
|
||||
]
|
||||
++ [ getDependencies jsonResponseContent
|
||||
| (_, Response {jsonResponseContent}) <- responses
|
||||
]
|
||||
|
||||
-- | Dependencies in the Response.* modules.
|
||||
operationResponseDependencies :: Operation -> [Name]
|
||||
operationResponseDependencies Operation {name} = [name]
|
||||
|
||||
pathItemsToOperation ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
-- | Conversion error cases
|
||||
Errors m ->
|
||||
-- | URLs
|
||||
[(FilePath, OpenApi.PathItem)] ->
|
||||
m [Operation]
|
||||
pathItemsToOperation resolver errors@Errors {..} pathInfos = do
|
||||
items <- forM pathInfos $ \(path, OpenApi.PathItem {..}) -> do
|
||||
get <-
|
||||
forM _pathItemGet (operationToOperation resolver errors "GET" path _pathItemParameters)
|
||||
put <-
|
||||
forM _pathItemPut (operationToOperation resolver errors "PUT" path _pathItemParameters)
|
||||
post <-
|
||||
forM _pathItemPost (operationToOperation resolver errors "POST" path _pathItemParameters)
|
||||
delete <-
|
||||
forM _pathItemDelete (operationToOperation resolver errors "DELETE" path _pathItemParameters)
|
||||
options <-
|
||||
forM _pathItemOptions (operationToOperation resolver errors "OPTIONS" path _pathItemParameters)
|
||||
head <-
|
||||
forM _pathItemHead (operationToOperation resolver errors "HEAD" path _pathItemParameters)
|
||||
patch <-
|
||||
forM _pathItemPatch (operationToOperation resolver errors "PATCH" path _pathItemParameters)
|
||||
trace <-
|
||||
forM _pathItemTrace (operationToOperation resolver errors "TRACE" path _pathItemParameters)
|
||||
pure (catMaybes [get, put, post, delete, options, head, patch, trace])
|
||||
pure (concat items)
|
||||
|
||||
-- TODO name
|
||||
operationToOperation ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
-- | Conversion error cases
|
||||
Errors m ->
|
||||
-- | HTTP Method
|
||||
Text ->
|
||||
-- | Path
|
||||
FilePath ->
|
||||
-- | Params defined at the PathItem level
|
||||
[OpenApi.Referenced OpenApi.Param] ->
|
||||
OpenApi.Operation ->
|
||||
m Operation
|
||||
operationToOperation resolver errors@Errors {..} method path params OpenApi.Operation {..} = do
|
||||
operationId <-
|
||||
whenNothing _operationOperationId missingOperationId
|
||||
path <-
|
||||
pathToPath
|
||||
resolver
|
||||
errors
|
||||
path
|
||||
-- Operations override pathItem params
|
||||
(_operationParameters ++ params)
|
||||
requestBody <- forM _operationRequestBody $ \referencedRequestBody -> do
|
||||
requestBody <- resolve resolver referencedRequestBody
|
||||
requestBodyToRequestBody resolver errors requestBody
|
||||
defaultResponse <- forM (OpenApi._responsesDefault _operationResponses) $ \referencedResponse -> do
|
||||
response <- resolve resolver referencedResponse
|
||||
responseToResponse resolver errors response
|
||||
responses <- forM (InsOrd.toList (OpenApi._responsesResponses _operationResponses)) $ \(statusCode, referencedResponse) -> do
|
||||
response <- resolve resolver referencedResponse
|
||||
(,) <$> pure statusCode <*> responseToResponse resolver errors response
|
||||
pure
|
||||
Operation
|
||||
{ name = fromText operationId,
|
||||
..
|
||||
}
|
||||
|
||||
requestBodyToRequestBody ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
Errors m ->
|
||||
OpenApi.RequestBody ->
|
||||
m RequestBody
|
||||
requestBodyToRequestBody resolver Errors {..} requestBody = do
|
||||
-- TODO support form inputs as well
|
||||
OpenApi.MediaTypeObject {..} <-
|
||||
whenNothing
|
||||
(InsOrd.lookup "application/json" (OpenApi._requestBodyContent requestBody))
|
||||
unsupportedMediaType
|
||||
referencedSchema <-
|
||||
whenNothing
|
||||
_mediaTypeObjectSchema
|
||||
requestBodyMissingSchema
|
||||
type_ <-
|
||||
schemaRefToType resolver referencedSchema
|
||||
pure
|
||||
RequestBody
|
||||
{ jsonRequestBodyContent = type_
|
||||
}
|
||||
|
||||
responseToResponse ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
Errors m ->
|
||||
OpenApi.Response ->
|
||||
m Response
|
||||
responseToResponse resolver Errors {..} response = do
|
||||
OpenApi.MediaTypeObject {..} <-
|
||||
whenNothing
|
||||
(InsOrd.lookup "application/json" (OpenApi._responseContent response))
|
||||
unsupportedMediaType
|
||||
-- TODO take care of headers
|
||||
referencedSchema <-
|
||||
whenNothing
|
||||
_mediaTypeObjectSchema
|
||||
requestBodyMissingSchema
|
||||
type_ <-
|
||||
schemaRefToType resolver referencedSchema
|
||||
pure
|
||||
Response
|
||||
{ jsonResponseContent = type_
|
||||
}
|
||||
|
||||
parsePath :: FilePath -> [PathSegment Text]
|
||||
parsePath path =
|
||||
let toPathSegment s
|
||||
| Just s <- Text.stripPrefix "{" s,
|
||||
Just s <- Text.stripSuffix "}" s =
|
||||
VariableSegment s
|
||||
| otherwise =
|
||||
StaticSegment s
|
||||
in case Text.splitOn "/" (toText path) of
|
||||
-- leading / results in a leading empty string after split
|
||||
"" : segments ->
|
||||
map toPathSegment segments
|
||||
segments ->
|
||||
-- TODO this is probably an error
|
||||
map toPathSegment segments
|
||||
|
||||
paramToParam ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
Errors m ->
|
||||
OpenApi.Param ->
|
||||
m Param
|
||||
paramToParam resolver Errors {..} OpenApi.Param {..} = do
|
||||
schema <- whenNothing _paramSchema paramMissingSchema
|
||||
typ <- schemaRefToType resolver schema
|
||||
pure
|
||||
Param
|
||||
{ name = fromText _paramName,
|
||||
paramIn = case _paramIn of
|
||||
OpenApi.ParamQuery -> InQuery
|
||||
OpenApi.ParamHeader -> InHeader
|
||||
OpenApi.ParamPath -> InPath
|
||||
OpenApi.ParamCookie -> InCookie,
|
||||
required = fromMaybe False _paramRequired,
|
||||
schema = typ
|
||||
}
|
||||
|
||||
pathToPath ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
Errors m ->
|
||||
-- | URL Path
|
||||
FilePath ->
|
||||
-- | Available 'OpenApi.Param's
|
||||
[OpenApi.Referenced OpenApi.Param] ->
|
||||
m Path
|
||||
pathToPath resolver errors@Errors {..} textualPath availableParams = do
|
||||
let path = parsePath textualPath
|
||||
params <- traverse (resolve resolver) availableParams
|
||||
forM path $ \segment ->
|
||||
forM segment $ \paramName -> do
|
||||
param <-
|
||||
whenNothing
|
||||
( find
|
||||
(\x -> OpenApi._paramName x == paramName)
|
||||
params
|
||||
)
|
||||
(unknownParameter paramName)
|
||||
param <- paramToParam resolver errors param
|
||||
when
|
||||
(paramIn param /= InPath)
|
||||
paramNotInPath
|
||||
_ <-
|
||||
whenNothing
|
||||
(isBasicType (namedType (schema param)))
|
||||
paramNotBasicType
|
||||
pure param
|
71
src/Tie/Resolve.hs
Normal file
71
src/Tie/Resolve.hs
Normal file
@ -0,0 +1,71 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Resolve
|
||||
( Resolvable,
|
||||
Resolver,
|
||||
newResolver,
|
||||
resolve,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import qualified Data.HashMap.Strict.InsOrd as InsOrd
|
||||
import qualified Data.OpenApi as OpenApi
|
||||
import qualified Data.Text as Text
|
||||
|
||||
-- | Resolve an 'OpenApi.Reference' to the underlying component.
|
||||
newtype Resolver m = Resolver
|
||||
{ resolve :: forall a. Resolvable a => OpenApi.Referenced a -> m a
|
||||
}
|
||||
|
||||
newResolver :: Applicative m => OpenApi.Components -> (forall a. OpenApi.Reference -> m a) -> Resolver m
|
||||
newResolver components notFound =
|
||||
Resolver (resolveComponent components notFound)
|
||||
|
||||
type ComponentName = Text
|
||||
|
||||
resolveComponent ::
|
||||
(Applicative m, Resolvable a) =>
|
||||
-- | Inventory of components we can resolve to
|
||||
OpenApi.Components ->
|
||||
-- | What to do in case a 'OpenApi.Reference' is not found
|
||||
(OpenApi.Reference -> m a) ->
|
||||
-- | 'OpenApi.Reference' to resolve
|
||||
OpenApi.Referenced a ->
|
||||
m a
|
||||
resolveComponent components notFound = \referenced -> do
|
||||
let (componentType, resolveComponent) = resolvables
|
||||
case referenced of
|
||||
OpenApi.Inline a ->
|
||||
pure a
|
||||
OpenApi.Ref reference
|
||||
| Just a <-
|
||||
InsOrd.lookup
|
||||
(OpenApi.getReference reference)
|
||||
(resolveComponent components) ->
|
||||
pure a
|
||||
| otherwise ->
|
||||
notFound reference
|
||||
|
||||
-- | Helper class helping to dispatch from 'OpenApi.Referenced' to component type @a@.
|
||||
class Resolvable a where
|
||||
-- | Resolves the `OpenApi.Components` to the given corresponding `Definitions`.
|
||||
resolvables :: (Text.Text, OpenApi.Components -> OpenApi.Definitions a)
|
||||
|
||||
instance Resolvable OpenApi.Schema where
|
||||
resolvables = ("schemas", OpenApi._componentsSchemas)
|
||||
|
||||
instance Resolvable OpenApi.Response where
|
||||
resolvables = ("responses", OpenApi._componentsResponses)
|
||||
|
||||
instance Resolvable OpenApi.Param where
|
||||
resolvables = ("parameters", OpenApi._componentsParameters)
|
||||
|
||||
instance Resolvable OpenApi.Example where
|
||||
resolvables = ("examples", OpenApi._componentsExamples)
|
||||
|
||||
instance Resolvable OpenApi.RequestBody where
|
||||
resolvables = ("requestBodies", OpenApi._componentsRequestBodies)
|
410
src/Tie/Type.hs
Normal file
410
src/Tie/Type.hs
Normal file
@ -0,0 +1,410 @@
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveTraversable #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Tie.Type
|
||||
( StringFormat (..),
|
||||
BasicType (..),
|
||||
ObjectType (..),
|
||||
Type (..),
|
||||
Enumeration (..),
|
||||
Name,
|
||||
Named (..),
|
||||
namedType,
|
||||
|
||||
-- * Conversion from 'OpenApi.Schema' to 'Type'.
|
||||
schemaToType,
|
||||
schemaRefToType,
|
||||
|
||||
-- * Accessors and operators working with 'Type'
|
||||
isBasicType,
|
||||
isEnumType,
|
||||
isArrayType,
|
||||
isObjectType,
|
||||
isOneOfType,
|
||||
|
||||
-- * Normalize types
|
||||
normalizeObjectType,
|
||||
normalizeVariants,
|
||||
|
||||
-- * Dependencies
|
||||
namedTypeDependencies,
|
||||
transitiveDependencies,
|
||||
typeDependencies,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad.Writer (runWriterT, tell)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Foldable (foldr1)
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import qualified Data.HashMap.Strict.InsOrd as InsOrd
|
||||
import qualified Data.HashSet as HashSet
|
||||
import qualified Data.OpenApi as OpenApi
|
||||
import qualified Data.Text as Text
|
||||
import Tie.Name (Name, fromText)
|
||||
import Tie.Resolve (Resolver, resolve)
|
||||
import Prelude hiding (Type)
|
||||
|
||||
-- | Formats a String can have
|
||||
data StringFormat
|
||||
= -- | Default for when there is no format defined in the type declaration.
|
||||
FormatDefault
|
||||
| -- | Full-date notation as defined by RFC 3339, section 5.6, for example, 2017-07-21
|
||||
FormatDate
|
||||
| -- | The date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z
|
||||
FormatDateTime
|
||||
| -- | A hint to UIs to mask the input
|
||||
FormatPassword
|
||||
| -- | base64-encoded characters, for example, U3dhZ2dlciByb2Nrcw==
|
||||
FormatByte
|
||||
| -- | binary data, used to describe files
|
||||
FormatBinary
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Represents an OpenAPI enumeration.
|
||||
data Enumeration = Enumeration
|
||||
{ -- | The allowed values for this 'Enum'.
|
||||
alternatives :: [Text],
|
||||
-- | Whether 'null' is a valid 'Enum' value.
|
||||
includeNull :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Basic types OpenAPI data types.
|
||||
data BasicType
|
||||
= TyString {format :: StringFormat}
|
||||
| TyEnum Enumeration
|
||||
| TyNumber
|
||||
| TyInteger
|
||||
| TyBoolean
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | An object is a collection of property/value pairs.
|
||||
data ObjectType ty = ObjectType
|
||||
{ properties :: HashMap Name ty,
|
||||
requiredProperties :: HashSet Name,
|
||||
freeFormObjectType :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Our own version of 'OpenApi.Referenced'.
|
||||
data Named ty
|
||||
= Named Name ty
|
||||
| Unnamed ty
|
||||
deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
|
||||
|
||||
namedType :: Named ty -> ty
|
||||
namedType named = case named of
|
||||
Named _ ty -> ty
|
||||
Unnamed ty -> ty
|
||||
|
||||
-- | This is our internal representation for 'OpenApi.Schema'. From 'Type' we derive
|
||||
-- the Haskell data types as well as the serialization code.
|
||||
data Type
|
||||
= -- | OpenApi's allOf
|
||||
AllOf [Named Type]
|
||||
| -- | OpenApi's anyOf
|
||||
AnyOf [Named Type]
|
||||
| -- | OpenApi's oneOf
|
||||
OneOf [Named Type]
|
||||
| -- | OpenApi's not
|
||||
Not (Named Type)
|
||||
| -- | Basic, primitive types
|
||||
Basic BasicType
|
||||
| -- | Objects and it's properties
|
||||
Object (ObjectType (Named Type))
|
||||
| -- | Arrays of elements
|
||||
Array (Named Type)
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Casts a 'Type' to a 'BasicType' if possible.
|
||||
isBasicType :: Type -> Maybe BasicType
|
||||
isBasicType typ = case typ of
|
||||
AllOf [x] -> isBasicType (namedType x)
|
||||
AllOf {} -> Nothing
|
||||
AnyOf [x] -> isBasicType (namedType x)
|
||||
AnyOf {} -> Nothing
|
||||
OneOf [x] -> isBasicType (namedType x)
|
||||
OneOf {} -> Nothing
|
||||
Not {} -> Nothing
|
||||
Basic basicType -> Just basicType
|
||||
Object {} -> Nothing
|
||||
Array {} -> Nothing
|
||||
|
||||
-- | Casts a 'Type' to an 'Enumeration' if possible.
|
||||
isEnumType :: Type -> Maybe Enumeration
|
||||
isEnumType typ
|
||||
| Just (TyEnum enum) <- isBasicType typ =
|
||||
Just enum
|
||||
| otherwise =
|
||||
Nothing
|
||||
|
||||
schemaRefToType ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
OpenApi.Referenced OpenApi.Schema ->
|
||||
m (Named Type)
|
||||
schemaRefToType resolver referencedSchema = do
|
||||
schema <- resolve resolver referencedSchema
|
||||
case referencedSchema of
|
||||
OpenApi.Ref reference ->
|
||||
Named (fromText (OpenApi.getReference reference))
|
||||
<$> schemaToType resolver schema
|
||||
OpenApi.Inline schema -> do
|
||||
Unnamed <$> schemaToType resolver schema
|
||||
|
||||
-- | Converts an 'OpenApi.Schema' to our internal 'Type' representation.
|
||||
-- An optional 'ComponentName' indicates the name of component.
|
||||
schemaToType :: Monad m => Resolver m -> OpenApi.Schema -> m Type
|
||||
schemaToType resolver schema
|
||||
| Just allOfsRefs <- OpenApi._schemaAllOf schema = do
|
||||
AllOf <$> traverse (schemaRefToType resolver) allOfsRefs
|
||||
| Just oneOfsRefs <- OpenApi._schemaOneOf schema =
|
||||
OneOf <$> traverse (schemaRefToType resolver) oneOfsRefs
|
||||
| Just anyOfRefs <- OpenApi._schemaAnyOf schema =
|
||||
AnyOf <$> traverse (schemaRefToType resolver) anyOfRefs
|
||||
| Just notOfRef <- OpenApi._schemaNot schema =
|
||||
Not <$> schemaRefToType resolver notOfRef
|
||||
| Just schemaType <- OpenApi._schemaType schema =
|
||||
case schemaType of
|
||||
OpenApi.OpenApiString ->
|
||||
pure (Basic (schemaToStringyType schema))
|
||||
OpenApi.OpenApiNumber ->
|
||||
pure (Basic TyNumber)
|
||||
OpenApi.OpenApiInteger ->
|
||||
pure (Basic TyInteger)
|
||||
OpenApi.OpenApiBoolean ->
|
||||
pure (Basic TyBoolean)
|
||||
OpenApi.OpenApiArray
|
||||
| Just items <- OpenApi._schemaItems schema ->
|
||||
case items of
|
||||
OpenApi.OpenApiItemsObject itemsSchemaRef ->
|
||||
Array <$> schemaRefToType resolver itemsSchemaRef
|
||||
OpenApi.OpenApiItemsArray _itemsSchemaRefs ->
|
||||
undefined -- TODO find out what tuple schemas are
|
||||
| otherwise ->
|
||||
undefined -- TODO array type without items
|
||||
OpenApi.OpenApiNull ->
|
||||
undefined -- TODO need a BasicType for that
|
||||
OpenApi.OpenApiObject ->
|
||||
Object <$> schemaToObjectType resolver schema
|
||||
-- Heuristic: if the 'OpenApi.Schema' has properties attached
|
||||
-- treat it as object.
|
||||
| not (InsOrd.null (OpenApi._schemaProperties schema)) =
|
||||
Object <$> schemaToObjectType resolver schema
|
||||
-- It's an enum but without explicit "type: string"
|
||||
| Just _enum <- OpenApi._schemaEnum schema =
|
||||
pure (Basic (schemaToStringyType schema))
|
||||
| otherwise =
|
||||
traceShow schema undefined
|
||||
|
||||
-- | Resolves an 'OpenApi.Schema' to an 'ObjectType'. In case the the 'OpenApi.Schema' is an
|
||||
-- allOf-schema. This function doesn't do any additional type checking.
|
||||
schemaToObjectType ::
|
||||
Monad m =>
|
||||
Resolver m ->
|
||||
OpenApi.Schema ->
|
||||
m (ObjectType (Named Type))
|
||||
schemaToObjectType resolver schema = do
|
||||
properties <-
|
||||
traverse
|
||||
(schemaRefToType resolver)
|
||||
(InsOrd.toHashMap (OpenApi._schemaProperties schema))
|
||||
freeFormObjectType <- case OpenApi._schemaAdditionalProperties schema of
|
||||
Nothing -> pure False
|
||||
Just (OpenApi.AdditionalPropertiesAllowed allowed) -> pure allowed
|
||||
Just (OpenApi.AdditionalPropertiesSchema schema) -> undefined -- TODO what exactly is this?
|
||||
pure $
|
||||
ObjectType
|
||||
{ freeFormObjectType,
|
||||
properties = HashMap.mapKeys fromText properties,
|
||||
requiredProperties =
|
||||
HashSet.fromList (map fromText (OpenApi._schemaRequired schema))
|
||||
}
|
||||
|
||||
-- | Treat an 'OpenApi.Schema' as stringy. Accounts for enumerations
|
||||
-- as well. This function doesn't do any additional type checking.
|
||||
schemaToStringyType :: OpenApi.Schema -> BasicType
|
||||
schemaToStringyType schema
|
||||
| Just enum <- OpenApi._schemaEnum schema = do
|
||||
TyEnum $
|
||||
Enumeration
|
||||
{ alternatives = [alt | Aeson.String alt <- enum],
|
||||
includeNull = Aeson.Null `elem` enum
|
||||
}
|
||||
| otherwise =
|
||||
TyString
|
||||
{ format = FormatDefault -- TODO
|
||||
}
|
||||
|
||||
-- | Extracts the shallow dependencies of a 'Type' by traversing the 'Type' and
|
||||
-- until we hit a 'Named' type.
|
||||
typeDependencies :: (Named Type -> [Name]) -> Type -> [Name]
|
||||
typeDependencies getDependencies ty =
|
||||
-- For allOf, anyOf, oneOf look through all the dependencies - similar to what
|
||||
-- we do in 'isObjectType'.
|
||||
case ty of
|
||||
AllOf allOfs ->
|
||||
concatMap getDependencies allOfs
|
||||
AnyOf anyOfs ->
|
||||
concatMap getDependencies anyOfs
|
||||
OneOf oneOfs ->
|
||||
concatMap getDependencies oneOfs
|
||||
Not not ->
|
||||
typeDependencies getDependencies (namedType not)
|
||||
Basic {} ->
|
||||
[]
|
||||
Object objectType ->
|
||||
objectTypeDependencies getDependencies objectType
|
||||
Array elemType ->
|
||||
getDependencies elemType
|
||||
|
||||
-- | Dependencies of a 'Named Type'. This doesn't return transitive
|
||||
-- dependencies.
|
||||
namedTypeDependencies :: Named Type -> [Name]
|
||||
namedTypeDependencies named = case named of
|
||||
Named name _elem ->
|
||||
-- This is key: We don't want to descend into elem's type.
|
||||
[name]
|
||||
Unnamed elem ->
|
||||
typeDependencies namedTypeDependencies elem
|
||||
|
||||
-- | Extract all the transitive dependencies form a 'Named Type'.
|
||||
transitiveDependencies :: Named Type -> [Name]
|
||||
transitiveDependencies named = case named of
|
||||
Named name typ ->
|
||||
name : typeDependencies transitiveDependencies typ
|
||||
Unnamed typ ->
|
||||
typeDependencies transitiveDependencies typ
|
||||
|
||||
-- | Dependencies of an 'ObjectType'.
|
||||
objectTypeDependencies :: (Named Type -> [Name]) -> ObjectType (Named Type) -> [Name]
|
||||
objectTypeDependencies getDependencies objectType =
|
||||
concatMap getDependencies (toList (properties objectType))
|
||||
|
||||
-- | Casting a 'Type' to the set of types it could be.
|
||||
isOneOfType :: Type -> Maybe [Named Type]
|
||||
isOneOfType ty = case ty of
|
||||
OneOf oneOfs ->
|
||||
Just oneOfs
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
isArrayType :: Type -> Maybe (Named Type)
|
||||
isArrayType ty = case ty of
|
||||
Array elem -> Just elem
|
||||
_ -> Nothing
|
||||
|
||||
-- | Casting a 'Type' to an 'ObjectType', if possible. `isObjectType` looks through
|
||||
-- allOf, oneOf, anyOf to ensure
|
||||
isObjectType :: Type -> Maybe (ObjectType (Named Type))
|
||||
isObjectType ty = case ty of
|
||||
AllOf allOfs -> do
|
||||
-- We have the choice: we could use traverse instead and fail
|
||||
-- everything in case any subtype is not an Object. For now,
|
||||
-- we ignore "type errors" and collect everything we get. I
|
||||
-- found other code generators for OpenApi that behave that way.
|
||||
let objects = catMaybes (map (isObjectType . namedType) allOfs)
|
||||
pure (combine objects)
|
||||
AnyOf anyOfs -> do
|
||||
-- Look through all the objects, mark all of the properties optional
|
||||
let objects =
|
||||
[ object {requiredProperties = HashSet.empty}
|
||||
| object <- catMaybes (map (isObjectType . namedType) anyOfs)
|
||||
]
|
||||
pure (combine objects)
|
||||
OneOf oneOfs -> do
|
||||
-- Look through all the objects, mark all of the properties optional
|
||||
let objects =
|
||||
[ object {requiredProperties = HashSet.empty}
|
||||
| object <- catMaybes (map (isObjectType . namedType) oneOfs)
|
||||
]
|
||||
pure (combine objects)
|
||||
Not {} -> Nothing
|
||||
Basic {} -> Nothing
|
||||
Object obj -> Just obj
|
||||
Array {} -> Nothing
|
||||
where
|
||||
-- In principle this is Semigroup and Monoid instance. But it's too
|
||||
-- early in the design to rely on that.
|
||||
combine xs = case nonEmpty xs of
|
||||
Nothing -> emptyObject
|
||||
Just os -> foldr1 combineObjects os -- TODO make strict
|
||||
emptyObject =
|
||||
ObjectType
|
||||
{ properties = mempty,
|
||||
requiredProperties = mempty,
|
||||
freeFormObjectType = False
|
||||
}
|
||||
|
||||
-- Combine two ObjectTypes. Doesn't report common fields!
|
||||
combineObjects o1 o2 =
|
||||
ObjectType
|
||||
{ properties = properties o1 <> properties o2,
|
||||
requiredProperties = requiredProperties o1 <> requiredProperties o2,
|
||||
freeFormObjectType = freeFormObjectType o1 || freeFormObjectType o2
|
||||
}
|
||||
|
||||
-- | OpenApi allows defining objects "inline". This function extracts inline objects
|
||||
-- and assigns names to them.
|
||||
--
|
||||
-- Invariant: The returned 'ObjectType' doesn't contain unnamed dependencies.
|
||||
normalizeObjectType ::
|
||||
Monad m =>
|
||||
(Name -> ObjectType (Named Type) -> m Name) ->
|
||||
ObjectType (Named Type) ->
|
||||
m (ObjectType (Named Type), [(Name, Type)])
|
||||
normalizeObjectType assignName objectType@ObjectType {..} = do
|
||||
(properties, newTypes) <- runWriterT $
|
||||
flip HashMap.traverseWithKey properties $ \fieldName fieldType ->
|
||||
-- TODO we will probably have to handle enums here as well
|
||||
case fieldType of
|
||||
Unnamed typ
|
||||
| Just objectType <- isObjectType typ -> do
|
||||
name <- lift (assignName fieldName objectType)
|
||||
tell [(name, Object objectType)]
|
||||
pure (Named name (Object objectType))
|
||||
-- TODO we need to recurse, otherwise we only support
|
||||
-- one level deep arrays.
|
||||
| Array (Unnamed elemType) <- typ,
|
||||
Just objectType <- isObjectType elemType -> do
|
||||
name <- lift (assignName fieldName objectType)
|
||||
tell [(name, Object objectType)]
|
||||
pure (Named name (Object objectType))
|
||||
_ ->
|
||||
pure fieldType
|
||||
pure (objectType {properties}, newTypes)
|
||||
|
||||
-- | Walk through the variants of a oneOf type. This function extracts inline objects
|
||||
-- and assigns names to them.
|
||||
--
|
||||
-- Invariant: The returned variants don't contain unnamed dependencies.
|
||||
normalizeVariants ::
|
||||
Monad m =>
|
||||
(Int -> Type -> m Name) ->
|
||||
[Named Type] ->
|
||||
m ([Named Type], [(Name, Type)])
|
||||
normalizeVariants assignName variants = runWriterT $
|
||||
forM (zip [1 ..] variants) $ \(i, variant) -> do
|
||||
-- TODO we will probably have to handle enums here as well
|
||||
case variant of
|
||||
Unnamed typ
|
||||
| Just objectType <- isObjectType typ -> do
|
||||
let typ = Object objectType
|
||||
name <- lift (assignName i typ)
|
||||
tell [(name, typ)]
|
||||
pure (Named name typ)
|
||||
-- TODO we need to recurse, otherwise we only support
|
||||
-- one level deep arrays.
|
||||
| Array (Unnamed elemType) <- typ,
|
||||
Just objectType <- isObjectType elemType -> do
|
||||
let typ = Object objectType
|
||||
name <- lift (assignName i typ)
|
||||
tell [(name, typ)]
|
||||
pure (Named name typ)
|
||||
_ ->
|
||||
pure variant
|
57
src/Tie/Writer.hs
Normal file
57
src/Tie/Writer.hs
Normal file
@ -0,0 +1,57 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
module Tie.Writer
|
||||
( Writer,
|
||||
fileWriter,
|
||||
withTestWriter,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.ByteString.Builder (Builder, writeFile)
|
||||
import Data.Text.Lazy.Encoding (encodeUtf8Builder)
|
||||
import Prettyprinter (Doc, (<+>))
|
||||
import qualified Prettyprinter as PP
|
||||
import qualified Prettyprinter.Render.Text as PP
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath (takeDirectory, (</>))
|
||||
|
||||
-- | Abstraction for storing generated code on disk.
|
||||
type Writer m = forall ann. FilePath -> Doc ann -> m ()
|
||||
|
||||
-- | Renders a 'Doc' to a 'Builder' - ready to be written to disk.
|
||||
-- TODO move somewhere else
|
||||
render :: Doc ann -> Builder
|
||||
render =
|
||||
encodeUtf8Builder . PP.renderLazy . PP.layoutPretty PP.defaultLayoutOptions
|
||||
|
||||
-- | Renders 'Doc's to a file just as you would expect. Writes files relative
|
||||
-- to the given output directory.
|
||||
fileWriter :: MonadIO m => FilePath -> Writer m
|
||||
fileWriter outputDirectory path doc = liftIO $ do
|
||||
let fullPath = outputDirectory </> path
|
||||
createDirectoryIfMissing True (takeDirectory fullPath)
|
||||
Data.ByteString.Builder.writeFile fullPath (render doc)
|
||||
|
||||
-- | Collects all the FilePath and Doc pairs and returns them concatenated
|
||||
-- in one output
|
||||
withTestWriter :: MonadIO m => (Writer m -> m a) -> m (a, Builder)
|
||||
withTestWriter action = do
|
||||
ref <- liftIO (newIORef [])
|
||||
result <- action $ \file doc ->
|
||||
liftIO (modifyIORef' ref ((file, PP.unAnnotate doc) :))
|
||||
docs <- liftIO (readIORef ref)
|
||||
pure (result, renderOneBigFile (sortOn fst docs))
|
||||
where
|
||||
renderOneBigFile docs =
|
||||
render $
|
||||
PP.concatWith
|
||||
(\x y -> x <> PP.line <> "---------------------" <> PP.line <> y)
|
||||
[ PP.vsep
|
||||
[ PP.pretty (toText file),
|
||||
mempty,
|
||||
doc
|
||||
]
|
||||
| (file, doc) <- docs
|
||||
]
|
1
test/Main.hs
Normal file
1
test/Main.hs
Normal file
@ -0,0 +1 @@
|
||||
{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}
|
26
test/Test/Tie/Golden.hs
Normal file
26
test/Test/Tie/Golden.hs
Normal file
@ -0,0 +1,26 @@
|
||||
module Test.Tie.Golden (test_Golden_tests) where
|
||||
|
||||
import Data.ByteString.Builder (toLazyByteString)
|
||||
import Paths_tie (getDataDir)
|
||||
import System.FilePath (normalise, replaceExtension, (</>))
|
||||
import Test.Tasty (TestTree)
|
||||
import Test.Tasty.Golden (findByExtension, goldenVsStringDiff)
|
||||
import Tie (generate, withTestWriter)
|
||||
|
||||
test_Golden_tests :: IO [TestTree]
|
||||
test_Golden_tests = do
|
||||
dataDir <- getDataDir
|
||||
inputs <- findByExtension [".yaml"] (dataDir </> "test" </> "golden")
|
||||
pure
|
||||
[ goldenVsStringDiff
|
||||
("Test " <> input)
|
||||
(\ref new -> ["diff", "-u", ref, new])
|
||||
(replaceExtension input ".yaml.out")
|
||||
( do
|
||||
(_, output) <- withTestWriter $ \writer ->
|
||||
generate writer input
|
||||
pure (toLazyByteString output)
|
||||
)
|
||||
| input' <- inputs,
|
||||
let input = normalise input'
|
||||
]
|
16
test/Test/Tie/Operation.hs
Normal file
16
test/Test/Tie/Operation.hs
Normal file
@ -0,0 +1,16 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Test.Tie.Operation where
|
||||
|
||||
import Test.Hspec (Spec, context, it, shouldBe)
|
||||
import Test.Tasty.Hspec
|
||||
import Tie.Operation (PathSegment (..), parsePath)
|
||||
|
||||
spec_parsePath :: Spec
|
||||
spec_parsePath = do
|
||||
it "parses /users/create" $
|
||||
parsePath "/users/create" `shouldBe` [StaticSegment "users", StaticSegment "create"]
|
||||
it "parses /users/{id}" $
|
||||
parsePath "/users/{id}" `shouldBe` [StaticSegment "users", VariableSegment "id"]
|
||||
it "parses /users/{id}/address" $
|
||||
parsePath "/users/{id}/address" `shouldBe` [StaticSegment "users", VariableSegment "id", StaticSegment "address"]
|
47
test/golden/lists.yaml
Normal file
47
test/golden/lists.yaml
Normal file
@ -0,0 +1,47 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Scarf
|
||||
license:
|
||||
name: AllRightsReserved
|
||||
servers:
|
||||
- url: https://scarf.sh/api/v1
|
||||
paths:
|
||||
/packages:
|
||||
get:
|
||||
summary: List all packages
|
||||
operationId: listPackages
|
||||
tags:
|
||||
- packages
|
||||
responses:
|
||||
'200':
|
||||
description: An array of packages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Packages"
|
||||
/packages2:
|
||||
get:
|
||||
summary: List all packages
|
||||
operationId: listPackages2
|
||||
tags:
|
||||
- packages
|
||||
responses:
|
||||
'200':
|
||||
description: An inline array of packages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Package"
|
||||
components:
|
||||
schemas:
|
||||
Package:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
Packages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Package"
|
233
test/golden/lists.yaml.out
Normal file
233
test/golden/lists.yaml.out
Normal file
@ -0,0 +1,233 @@
|
||||
OpenAPI/Api.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Api where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Response
|
||||
|
||||
import OpenAPI.Schemas.Packages
|
||||
import OpenAPI.Schemas.Package
|
||||
|
||||
import OpenAPI.Response.ListPackages
|
||||
import OpenAPI.Response.ListPackages2
|
||||
|
||||
data Api m = Api {
|
||||
listPackages :: m ListPackagesResponse,
|
||||
listPackages2 :: m ListPackages2Response
|
||||
}
|
||||
|
||||
application :: (Control.Monad.IO.Class.MonadIO m) => (forall a . Network.Wai.Request -> m a -> IO a) -> Api m -> Network.Wai.Application -> Network.Wai.Application
|
||||
application run api notFound request respond =
|
||||
case Network.Wai.pathInfo request of
|
||||
[ "packages" ] ->
|
||||
case Network.Wai.requestMethod request of
|
||||
"GET" ->
|
||||
run request $ do
|
||||
response <- listPackages api
|
||||
Control.Monad.IO.Class.liftIO (respond (toResponse response))
|
||||
x ->
|
||||
unsupportedMethod x
|
||||
|
||||
[ "packages2" ] ->
|
||||
case Network.Wai.requestMethod request of
|
||||
"GET" ->
|
||||
run request $ do
|
||||
response <- listPackages2 api
|
||||
Control.Monad.IO.Class.liftIO (respond (toResponse response))
|
||||
x ->
|
||||
unsupportedMethod x
|
||||
|
||||
_ ->
|
||||
notFound request respond
|
||||
where
|
||||
unsupportedMethod _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 405) [] mempty)
|
||||
invalidRequest _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 401) [] mempty)
|
||||
---------------------
|
||||
OpenAPI/Response.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
class ToResponse a where
|
||||
toResponse :: a -> Network.Wai.Response
|
||||
---------------------
|
||||
OpenAPI/Response/ListPackages.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response.ListPackages where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Packages
|
||||
|
||||
data ListPackagesResponse
|
||||
= ListPackagesResponse200 Packages
|
||||
|
||||
instance ToResponse ListPackagesResponse where
|
||||
toResponse (ListPackagesResponse200 x) =
|
||||
Network.Wai.responseBuilder (toEnum 200) [(Network.HTTP.Types.hContentType, "application/json")] (Data.Aeson.fromEncoding (Data.Aeson.toEncoding x))
|
||||
---------------------
|
||||
OpenAPI/Response/ListPackages2.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response.ListPackages2 where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Package
|
||||
|
||||
data ListPackages2Response
|
||||
= ListPackages2Response200 [ Package ]
|
||||
|
||||
instance ToResponse ListPackages2Response where
|
||||
toResponse (ListPackages2Response200 x) =
|
||||
Network.Wai.responseBuilder (toEnum 200) [(Network.HTTP.Types.hContentType, "application/json")] (Data.Aeson.fromEncoding (Data.Aeson.toEncoding x))
|
||||
---------------------
|
||||
OpenAPI/Schemas/Package.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Package where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
|
||||
|
||||
newtype Package = Package
|
||||
{
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON Package where
|
||||
toJSON Package {..} = Data.Aeson.object
|
||||
[
|
||||
"name" Data.Aeson..= name
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON Package where
|
||||
parseJSON = Data.Aeson.withObject "Package" $ \o ->
|
||||
Package
|
||||
<$> o Data.Aeson..:? "name"
|
||||
---------------------
|
||||
OpenAPI/Schemas/Packages.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Packages where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Package
|
||||
|
||||
|
||||
---------------------
|
||||
open-api.cabal
|
||||
|
||||
cabal-version: 3.0
|
||||
name: open-api
|
||||
version: 0.1.0.0
|
||||
library
|
||||
build-depends:
|
||||
, aeson
|
||||
, attoparsec
|
||||
, base
|
||||
, ghc-prim
|
||||
, http-api-data
|
||||
, http-types
|
||||
, text
|
||||
, wai
|
||||
exposed-modules:
|
||||
OpenAPI.Api
|
||||
OpenAPI.Response
|
||||
OpenAPI.Response.ListPackages
|
||||
OpenAPI.Response.ListPackages2
|
||||
OpenAPI.Schemas.Package
|
||||
OpenAPI.Schemas.Packages
|
32
test/golden/oneof.yaml
Normal file
32
test/golden/oneof.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Scarf
|
||||
license:
|
||||
name: AllRightsReserved
|
||||
servers:
|
||||
- url: https://scarf.sh/api/v1
|
||||
paths:
|
||||
/packages:
|
||||
get:
|
||||
summary: List all packages
|
||||
operationId: listPackages
|
||||
tags:
|
||||
- packages
|
||||
responses:
|
||||
'200':
|
||||
description: An array of packages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Packages"
|
||||
components:
|
||||
schemas:
|
||||
Package:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
Packages:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Package"
|
||||
- $ref: "#/components/schemas/Package"
|
201
test/golden/oneof.yaml.out
Normal file
201
test/golden/oneof.yaml.out
Normal file
@ -0,0 +1,201 @@
|
||||
OpenAPI/Api.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Api where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Response
|
||||
|
||||
import OpenAPI.Schemas.Packages
|
||||
|
||||
import OpenAPI.Response.ListPackages
|
||||
|
||||
data Api m = Api {
|
||||
listPackages :: m ListPackagesResponse
|
||||
}
|
||||
|
||||
application :: (Control.Monad.IO.Class.MonadIO m) => (forall a . Network.Wai.Request -> m a -> IO a) -> Api m -> Network.Wai.Application -> Network.Wai.Application
|
||||
application run api notFound request respond =
|
||||
case Network.Wai.pathInfo request of
|
||||
[ "packages" ] ->
|
||||
case Network.Wai.requestMethod request of
|
||||
"GET" ->
|
||||
run request $ do
|
||||
response <- listPackages api
|
||||
Control.Monad.IO.Class.liftIO (respond (toResponse response))
|
||||
x ->
|
||||
unsupportedMethod x
|
||||
|
||||
_ ->
|
||||
notFound request respond
|
||||
where
|
||||
unsupportedMethod _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 405) [] mempty)
|
||||
invalidRequest _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 401) [] mempty)
|
||||
---------------------
|
||||
OpenAPI/Response.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
class ToResponse a where
|
||||
toResponse :: a -> Network.Wai.Response
|
||||
---------------------
|
||||
OpenAPI/Response/ListPackages.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response.ListPackages where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Packages
|
||||
|
||||
data ListPackagesResponse
|
||||
= ListPackagesResponse200 Packages
|
||||
|
||||
instance ToResponse ListPackagesResponse where
|
||||
toResponse (ListPackagesResponse200 x) =
|
||||
Network.Wai.responseBuilder (toEnum 200) [(Network.HTTP.Types.hContentType, "application/json")] (Data.Aeson.fromEncoding (Data.Aeson.toEncoding x))
|
||||
---------------------
|
||||
OpenAPI/Schemas/Package.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Package where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
|
||||
|
||||
newtype Package = Package
|
||||
{
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON Package where
|
||||
toJSON Package {..} = Data.Aeson.object
|
||||
[
|
||||
"name" Data.Aeson..= name
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON Package where
|
||||
parseJSON = Data.Aeson.withObject "Package" $ \o ->
|
||||
Package
|
||||
<$> o Data.Aeson..:? "name"
|
||||
---------------------
|
||||
OpenAPI/Schemas/Packages.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Packages where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Package
|
||||
|
||||
data Packages
|
||||
= PackagesPackage Package
|
||||
| PackagesPackage Package
|
||||
|
||||
instance Data.Aeson.ToJSON Packages where
|
||||
toJSON (PackagesPackage x) = Data.Aeson.toJSON x
|
||||
toJSON (PackagesPackage x) = Data.Aeson.toJSON x
|
||||
|
||||
instance Data.Aeson.FromJSON Packages where
|
||||
parseJSON x =
|
||||
(PackagesPackage <$> Data.Aeson.parseJSON x) Control.Applicative.<|>
|
||||
(PackagesPackage <$> Data.Aeson.parseJSON x)
|
||||
---------------------
|
||||
open-api.cabal
|
||||
|
||||
cabal-version: 3.0
|
||||
name: open-api
|
||||
version: 0.1.0.0
|
||||
library
|
||||
build-depends:
|
||||
, aeson
|
||||
, attoparsec
|
||||
, base
|
||||
, ghc-prim
|
||||
, http-api-data
|
||||
, http-types
|
||||
, text
|
||||
, wai
|
||||
exposed-modules:
|
||||
OpenAPI.Api
|
||||
OpenAPI.Response
|
||||
OpenAPI.Response.ListPackages
|
||||
OpenAPI.Schemas.Package
|
||||
OpenAPI.Schemas.Packages
|
145
test/golden/test1.yaml
Normal file
145
test/golden/test1.yaml
Normal file
@ -0,0 +1,145 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: simple_violation_bool
|
||||
license:
|
||||
name: MIT
|
||||
description: |
|
||||
Simple violation in simple_violation_bool module
|
||||
servers:
|
||||
- url: https://swagger.io/specification/
|
||||
paths:
|
||||
/users/{id}/create/{name}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
required: true
|
||||
- in: path
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
post:
|
||||
operationId: getUsers
|
||||
summary: Adds a new user
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema: # Request body contents
|
||||
$ref: "#/components/schemas/NISE"
|
||||
responses:
|
||||
'200':
|
||||
description: xxxx
|
||||
content:
|
||||
application/json:
|
||||
schema: # Request body contents
|
||||
$ref: "#/components/schemas/Vehicle"
|
||||
components:
|
||||
schemas:
|
||||
Vehicle:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
Car:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/Vehicle"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
enum:
|
||||
- car
|
||||
has_4_wheel_drive:
|
||||
type: boolean
|
||||
|
||||
Plane:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/Vehicle"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
enum:
|
||||
- plane
|
||||
car:
|
||||
$ref: "#/components/schemas/Car"
|
||||
has_reactor:
|
||||
type: boolean
|
||||
nb_passengers:
|
||||
type: integer
|
||||
|
||||
Error:
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
message:
|
||||
type: string
|
||||
|
||||
PackageId:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Plane"
|
||||
- $ref: "#/components/schemas/Car"
|
||||
- type: integer
|
||||
- properties:
|
||||
name:
|
||||
type: string
|
||||
|
||||
NISE:
|
||||
type: object
|
||||
title: The Root Schema
|
||||
required:
|
||||
- description
|
||||
- id
|
||||
- name
|
||||
- ports
|
||||
properties:
|
||||
description:
|
||||
$ref: "#/components/schemas/PackageId"
|
||||
id:
|
||||
type: integer
|
||||
title: The Id Schema
|
||||
default: 0
|
||||
format: int64
|
||||
schema:
|
||||
$ref: './violation_schema.yaml#/NISE'
|
||||
name:
|
||||
type: string
|
||||
title: The Name Schema
|
||||
schema:
|
||||
$ref: './violation_schema.yaml#/NISE'
|
||||
ports:
|
||||
type: array
|
||||
title: The Ports Schema
|
||||
schema:
|
||||
$ref: './violation_schema.yaml#/NISE'
|
||||
items:
|
||||
type: integer
|
||||
title: The items Schema
|
||||
default: 0
|
||||
schema:
|
||||
$ref: './violation_schema.yaml#/NISE'
|
||||
value:
|
||||
type: object
|
||||
title: The Value Schema
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
schema:
|
||||
$ref: './violation_schema.yaml#/NISE'
|
||||
|
||||
|
427
test/golden/test1.yaml.out
Normal file
427
test/golden/test1.yaml.out
Normal file
@ -0,0 +1,427 @@
|
||||
OpenAPI/Api.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Api where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Response
|
||||
|
||||
import OpenAPI.Schemas.NISE
|
||||
import OpenAPI.Schemas.Vehicle
|
||||
|
||||
import OpenAPI.Response.GetUsers
|
||||
|
||||
data Api m = Api {
|
||||
getUsers :: GHC.Types.Int -> Data.Text.Text -> NISE -> m GetUsersResponse
|
||||
}
|
||||
|
||||
application :: (Control.Monad.IO.Class.MonadIO m) => (forall a . Network.Wai.Request -> m a -> IO a) -> Api m -> Network.Wai.Application -> Network.Wai.Application
|
||||
application run api notFound request respond =
|
||||
case Network.Wai.pathInfo request of
|
||||
[ "users", id, "create", name ] ->
|
||||
case Web.HttpApiData.parseUrlPiece id of
|
||||
Left _ -> invalidRequest "id"
|
||||
Right id ->
|
||||
case Web.HttpApiData.parseUrlPiece name of
|
||||
Left _ -> invalidRequest "name"
|
||||
Right name ->
|
||||
case Network.Wai.requestMethod request of
|
||||
"POST" ->
|
||||
do result <- Data.Attoparsec.ByteString.parseWith (Network.Wai.getRequestBodyChunk request) Data.Aeson.Parser.json' mempty
|
||||
case Data.Attoparsec.ByteString.eitherResult result of
|
||||
Left _err -> undefined
|
||||
Right bodyValue ->
|
||||
case Data.Aeson.Types.parseEither Data.Aeson.parseJSON bodyValue of
|
||||
Left _err -> undefined
|
||||
Right body ->
|
||||
run request $ do
|
||||
response <- getUsers api id name body
|
||||
Control.Monad.IO.Class.liftIO (respond (toResponse response))
|
||||
x ->
|
||||
unsupportedMethod x
|
||||
|
||||
_ ->
|
||||
notFound request respond
|
||||
where
|
||||
unsupportedMethod _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 405) [] mempty)
|
||||
invalidRequest _ =
|
||||
respond (Network.Wai.responseBuilder (toEnum 401) [] mempty)
|
||||
---------------------
|
||||
OpenAPI/Response.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
class ToResponse a where
|
||||
toResponse :: a -> Network.Wai.Response
|
||||
---------------------
|
||||
OpenAPI/Response/GetUsers.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Response.GetUsers where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.NISE
|
||||
import OpenAPI.Schemas.Vehicle
|
||||
|
||||
data GetUsersResponse
|
||||
= GetUsersResponse200 Vehicle
|
||||
|
||||
instance ToResponse GetUsersResponse where
|
||||
toResponse (GetUsersResponse200 x) =
|
||||
Network.Wai.responseBuilder (toEnum 200) [(Network.HTTP.Types.hContentType, "application/json")] (Data.Aeson.fromEncoding (Data.Aeson.toEncoding x))
|
||||
---------------------
|
||||
OpenAPI/Schemas/Car.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Car where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Vehicle
|
||||
|
||||
data Car = Car
|
||||
{
|
||||
has_4_wheel_drive :: Data.Maybe.Maybe (GHC.Types.Bool),
|
||||
id :: GHC.Types.Int,
|
||||
model :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
type' :: Data.Text.Text
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON Car where
|
||||
toJSON Car {..} = Data.Aeson.object
|
||||
[
|
||||
"has_4_wheel_drive" Data.Aeson..= has_4_wheel_drive,
|
||||
"id" Data.Aeson..= id,
|
||||
"model" Data.Aeson..= model,
|
||||
"name" Data.Aeson..= name,
|
||||
"type" Data.Aeson..= type'
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON Car where
|
||||
parseJSON = Data.Aeson.withObject "Car" $ \o ->
|
||||
Car
|
||||
<$> o Data.Aeson..:? "has_4_wheel_drive"
|
||||
<*> o Data.Aeson..: "id"
|
||||
<*> o Data.Aeson..:? "model"
|
||||
<*> o Data.Aeson..:? "name"
|
||||
<*> o Data.Aeson..: "type"
|
||||
---------------------
|
||||
OpenAPI/Schemas/NISE.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.NISE where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.PackageId
|
||||
|
||||
newtype NISEValue = NISEValue
|
||||
{
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON NISEValue where
|
||||
toJSON NISEValue {..} = Data.Aeson.object
|
||||
[
|
||||
"name" Data.Aeson..= name
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON NISEValue where
|
||||
parseJSON = Data.Aeson.withObject "NISEValue" $ \o ->
|
||||
NISEValue
|
||||
<$> o Data.Aeson..:? "name"
|
||||
|
||||
data NISE = NISE
|
||||
{
|
||||
description :: PackageId,
|
||||
id :: GHC.Types.Int,
|
||||
name :: Data.Text.Text,
|
||||
ports :: [ GHC.Types.Int ],
|
||||
value :: Data.Maybe.Maybe (NISEValue)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON NISE where
|
||||
toJSON NISE {..} = Data.Aeson.object
|
||||
[
|
||||
"description" Data.Aeson..= description,
|
||||
"id" Data.Aeson..= id,
|
||||
"name" Data.Aeson..= name,
|
||||
"ports" Data.Aeson..= ports,
|
||||
"value" Data.Aeson..= value
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON NISE where
|
||||
parseJSON = Data.Aeson.withObject "NISE" $ \o ->
|
||||
NISE
|
||||
<$> o Data.Aeson..: "description"
|
||||
<*> o Data.Aeson..: "id"
|
||||
<*> o Data.Aeson..: "name"
|
||||
<*> o Data.Aeson..: "ports"
|
||||
<*> o Data.Aeson..:? "value"
|
||||
---------------------
|
||||
OpenAPI/Schemas/PackageId.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.PackageId where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Car
|
||||
import OpenAPI.Schemas.Plane
|
||||
|
||||
newtype PackageIdOneOf4 = PackageIdOneOf4
|
||||
{
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON PackageIdOneOf4 where
|
||||
toJSON PackageIdOneOf4 {..} = Data.Aeson.object
|
||||
[
|
||||
"name" Data.Aeson..= name
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON PackageIdOneOf4 where
|
||||
parseJSON = Data.Aeson.withObject "PackageIdOneOf4" $ \o ->
|
||||
PackageIdOneOf4
|
||||
<$> o Data.Aeson..:? "name"
|
||||
|
||||
data PackageId
|
||||
= PackageIdPlane Plane
|
||||
| PackageIdCar Car
|
||||
| PackageIdPackageIdOneOf3 GHC.Types.Int
|
||||
| PackageIdPackageIdOneOf4 PackageIdOneOf4
|
||||
|
||||
instance Data.Aeson.ToJSON PackageId where
|
||||
toJSON (PackageIdPlane x) = Data.Aeson.toJSON x
|
||||
toJSON (PackageIdCar x) = Data.Aeson.toJSON x
|
||||
toJSON (PackageIdPackageIdOneOf3 x) = Data.Aeson.toJSON x
|
||||
toJSON (PackageIdPackageIdOneOf4 x) = Data.Aeson.toJSON x
|
||||
|
||||
instance Data.Aeson.FromJSON PackageId where
|
||||
parseJSON x =
|
||||
(PackageIdPlane <$> Data.Aeson.parseJSON x) Control.Applicative.<|>
|
||||
(PackageIdCar <$> Data.Aeson.parseJSON x) Control.Applicative.<|>
|
||||
(PackageIdPackageIdOneOf3 <$> Data.Aeson.parseJSON x) Control.Applicative.<|>
|
||||
(PackageIdPackageIdOneOf4 <$> Data.Aeson.parseJSON x)
|
||||
---------------------
|
||||
OpenAPI/Schemas/Plane.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Plane where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
import OpenAPI.Schemas.Car
|
||||
import OpenAPI.Schemas.Vehicle
|
||||
|
||||
data Plane = Plane
|
||||
{
|
||||
car :: Data.Maybe.Maybe (Car),
|
||||
has_reactor :: Data.Maybe.Maybe (GHC.Types.Bool),
|
||||
id :: Data.Maybe.Maybe (GHC.Types.Int),
|
||||
model :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
nb_passengers :: Data.Maybe.Maybe (GHC.Types.Int),
|
||||
type' :: Data.Maybe.Maybe (Data.Text.Text)
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON Plane where
|
||||
toJSON Plane {..} = Data.Aeson.object
|
||||
[
|
||||
"car" Data.Aeson..= car,
|
||||
"has_reactor" Data.Aeson..= has_reactor,
|
||||
"id" Data.Aeson..= id,
|
||||
"model" Data.Aeson..= model,
|
||||
"name" Data.Aeson..= name,
|
||||
"nb_passengers" Data.Aeson..= nb_passengers,
|
||||
"type" Data.Aeson..= type'
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON Plane where
|
||||
parseJSON = Data.Aeson.withObject "Plane" $ \o ->
|
||||
Plane
|
||||
<$> o Data.Aeson..:? "car"
|
||||
<*> o Data.Aeson..:? "has_reactor"
|
||||
<*> o Data.Aeson..:? "id"
|
||||
<*> o Data.Aeson..:? "model"
|
||||
<*> o Data.Aeson..:? "name"
|
||||
<*> o Data.Aeson..:? "nb_passengers"
|
||||
<*> o Data.Aeson..:? "type"
|
||||
---------------------
|
||||
OpenAPI/Schemas/Vehicle.hs
|
||||
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module OpenAPI.Schemas.Vehicle where
|
||||
|
||||
import qualified Control.Applicative
|
||||
import qualified Control.Monad.IO.Class
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Parser
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Data.Attoparsec.ByteString
|
||||
import qualified Data.Maybe
|
||||
import qualified Data.Text
|
||||
import qualified GHC.Types
|
||||
import qualified Network.HTTP.Types
|
||||
import qualified Network.Wai
|
||||
import qualified Web.HttpApiData
|
||||
|
||||
|
||||
|
||||
data Vehicle = Vehicle
|
||||
{
|
||||
id :: GHC.Types.Int,
|
||||
model :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
name :: Data.Maybe.Maybe (Data.Text.Text),
|
||||
type' :: Data.Text.Text
|
||||
}
|
||||
|
||||
instance Data.Aeson.ToJSON Vehicle where
|
||||
toJSON Vehicle {..} = Data.Aeson.object
|
||||
[
|
||||
"id" Data.Aeson..= id,
|
||||
"model" Data.Aeson..= model,
|
||||
"name" Data.Aeson..= name,
|
||||
"type" Data.Aeson..= type'
|
||||
]
|
||||
|
||||
instance Data.Aeson.FromJSON Vehicle where
|
||||
parseJSON = Data.Aeson.withObject "Vehicle" $ \o ->
|
||||
Vehicle
|
||||
<$> o Data.Aeson..: "id"
|
||||
<*> o Data.Aeson..:? "model"
|
||||
<*> o Data.Aeson..:? "name"
|
||||
<*> o Data.Aeson..: "type"
|
||||
---------------------
|
||||
open-api.cabal
|
||||
|
||||
cabal-version: 3.0
|
||||
name: open-api
|
||||
version: 0.1.0.0
|
||||
library
|
||||
build-depends:
|
||||
, aeson
|
||||
, attoparsec
|
||||
, base
|
||||
, ghc-prim
|
||||
, http-api-data
|
||||
, http-types
|
||||
, text
|
||||
, wai
|
||||
exposed-modules:
|
||||
OpenAPI.Api
|
||||
OpenAPI.Response
|
||||
OpenAPI.Response.GetUsers
|
||||
OpenAPI.Schemas.Car
|
||||
OpenAPI.Schemas.NISE
|
||||
OpenAPI.Schemas.PackageId
|
||||
OpenAPI.Schemas.Plane
|
||||
OpenAPI.Schemas.Vehicle
|
113
tie.cabal
Normal file
113
tie.cabal
Normal file
@ -0,0 +1,113 @@
|
||||
cabal-version: 3.6
|
||||
name: tie
|
||||
version: 0.1.0.0
|
||||
|
||||
-- A short (one-line) description of the package.
|
||||
-- synopsis:
|
||||
|
||||
-- A longer description of the package.
|
||||
-- description:
|
||||
|
||||
-- A URL where users can report bugs.
|
||||
-- bug-reports:
|
||||
|
||||
-- The license under which the package is released.
|
||||
-- license:
|
||||
|
||||
-- The package author(s).
|
||||
-- author:
|
||||
|
||||
-- An email address to which users can send suggestions, bug reports, and patches.
|
||||
-- maintainer:
|
||||
|
||||
-- A copyright notice.
|
||||
-- copyright:
|
||||
-- category:
|
||||
extra-source-files: CHANGELOG.md
|
||||
|
||||
data-files:
|
||||
test/golden/**/*.yaml
|
||||
test/golden/**/*.out
|
||||
|
||||
library
|
||||
exposed-modules: Tie
|
||||
Tie.Resolve
|
||||
Tie.Type
|
||||
Tie.Operation
|
||||
Tie.Codegen.Imports
|
||||
Tie.Codegen.Operation
|
||||
Tie.Codegen.Response
|
||||
Tie.Codegen.Schema
|
||||
Tie.Name
|
||||
Tie.Codegen.Cabal
|
||||
Tie.Writer
|
||||
|
||||
-- Modules included in this library but not exported.
|
||||
-- other-modules:
|
||||
|
||||
-- LANGUAGE extensions used by modules in this package.
|
||||
-- other-extensions:
|
||||
build-depends: aeson,
|
||||
base,
|
||||
bytestring >= 0.11.2.0,
|
||||
containers,
|
||||
text,
|
||||
directory,
|
||||
filepath,
|
||||
lens,
|
||||
mtl,
|
||||
openapi3,
|
||||
unordered-containers,
|
||||
insert-ordered-containers,
|
||||
prettyprinter,
|
||||
yaml,
|
||||
relude
|
||||
|
||||
mixins:
|
||||
base hiding (Prelude),
|
||||
relude (Relude as Prelude),
|
||||
relude
|
||||
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite tie-tests
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Main.hs
|
||||
|
||||
autogen-modules:
|
||||
Paths_tie
|
||||
|
||||
other-modules:
|
||||
Paths_tie
|
||||
|
||||
other-modules:
|
||||
Test.Tie.Golden
|
||||
Test.Tie.Operation
|
||||
|
||||
build-depends:
|
||||
, aeson
|
||||
, base
|
||||
, bytestring
|
||||
, containers
|
||||
, filepath
|
||||
, hspec
|
||||
, tasty-hspec
|
||||
, tasty
|
||||
, tasty-golden
|
||||
, openapi3
|
||||
, tasty-hunit
|
||||
, tie
|
||||
, text
|
||||
, prettyprinter
|
||||
, relude
|
||||
, insert-ordered-containers
|
||||
, yaml
|
||||
|
||||
mixins:
|
||||
base hiding (Prelude),
|
||||
relude (Relude as Prelude),
|
||||
relude
|
||||
|
||||
build-tool-depends: tasty-discover:tasty-discover
|
||||
hs-source-dirs: test
|
Loading…
Reference in New Issue
Block a user