Merge pull request #639 from hudayou/swagger2
Provide a swagger2 spec for the dynamic API
This commit is contained in:
@@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs
|
||||
- Ability to set addresses to listen on - @hudayou
|
||||
|
||||
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ dependencies:
|
||||
test:
|
||||
override:
|
||||
- stack test
|
||||
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@"
|
||||
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$@"
|
||||
- stack exec -- cabal update
|
||||
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
|
||||
- stack exec -- packdeps *.cabal || true
|
||||
|
||||
+5
-2
@@ -13,6 +13,7 @@ import PostgREST.DbStructure
|
||||
import Control.Monad
|
||||
import Data.Monoid ((<>))
|
||||
import Data.String.Conversions (cs)
|
||||
import Data.String (IsString (..))
|
||||
import qualified Hasql.Query as H
|
||||
import qualified Hasql.Session as H
|
||||
import qualified Hasql.Decoders as HD
|
||||
@@ -47,9 +48,11 @@ main = do
|
||||
hSetBuffering stderr NoBuffering
|
||||
|
||||
conf <- readOptions
|
||||
let port = configPort conf
|
||||
let host = configHost conf
|
||||
port = configPort conf
|
||||
pgSettings = cs (configDatabase conf)
|
||||
appSettings = setPort port
|
||||
appSettings = setHost (fromString host)
|
||||
. setPort port
|
||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||
$ defaultSettings
|
||||
|
||||
|
||||
+15
-6
@@ -44,8 +44,8 @@ executable postgrest
|
||||
, http-types
|
||||
, interpolatedstring-perl6
|
||||
, jwt
|
||||
, microlens >= 0.4.2 && < 0.5
|
||||
, microlens-aeson >= 2.1.1 && < 2.2
|
||||
, lens >=3.8 && < 5.0
|
||||
, lens-aeson >= 1.0.0.0 && < 1.1.0.0
|
||||
, mtl
|
||||
, optparse-applicative >= 0.11 && < 0.13
|
||||
, parsec
|
||||
@@ -65,6 +65,8 @@ executable postgrest
|
||||
, wai-extra
|
||||
, wai-middleware-static >= 0.6.0
|
||||
, warp >= 3.1.0
|
||||
, insert-ordered-containers >= 0.1.0.1
|
||||
, swagger2 >= 2.1
|
||||
, HTTP
|
||||
, Ranged-sets
|
||||
if !os(windows)
|
||||
@@ -89,8 +91,8 @@ library
|
||||
, http-types
|
||||
, interpolatedstring-perl6
|
||||
, jwt
|
||||
, microlens
|
||||
, microlens-aeson
|
||||
, lens
|
||||
, lens-aeson
|
||||
, mtl
|
||||
, optparse-applicative
|
||||
, parsec
|
||||
@@ -109,6 +111,8 @@ library
|
||||
, wai-extra
|
||||
, wai-middleware-static >= 0.6.0
|
||||
, warp >= 3.1.0
|
||||
, insert-ordered-containers >= 0.1.0.1
|
||||
, swagger2 >= 2.1
|
||||
|
||||
Other-Modules: Paths_postgrest
|
||||
Exposed-Modules: PostgREST.App
|
||||
@@ -122,6 +126,7 @@ library
|
||||
, PostgREST.RangeQuery
|
||||
, PostgREST.ApiRequest
|
||||
, PostgREST.Types
|
||||
, PostgREST.OpenAPI
|
||||
hs-source-dirs: src
|
||||
|
||||
Test-Suite spec
|
||||
@@ -163,8 +168,8 @@ Test-Suite spec
|
||||
, http-types
|
||||
, interpolatedstring-perl6
|
||||
, jwt
|
||||
, microlens
|
||||
, microlens-aeson
|
||||
, lens
|
||||
, lens-aeson
|
||||
, monad-control
|
||||
, mtl
|
||||
, optparse-applicative
|
||||
@@ -186,5 +191,9 @@ Test-Suite spec
|
||||
, wai-extra
|
||||
, wai-middleware-static
|
||||
, warp
|
||||
, insert-ordered-containers
|
||||
, hjsonpointer
|
||||
, hjsonschema
|
||||
, swagger2
|
||||
, HTTP
|
||||
, Ranged-sets
|
||||
|
||||
@@ -45,10 +45,11 @@ data Target = TargetIdent QualifiedIdentifier
|
||||
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
|
||||
-- | Enumeration of currently supported content types for
|
||||
-- route responses and upload payloads
|
||||
data ContentType = ApplicationJSON | TextCSV deriving Eq
|
||||
data ContentType = ApplicationJSON | TextCSV | OpenAPI deriving Eq
|
||||
instance Show ContentType where
|
||||
show ApplicationJSON = "application/json; charset=utf-8"
|
||||
show TextCSV = "text/csv; charset=utf-8"
|
||||
show OpenAPI = "application/openapi+json; charset=utf-8"
|
||||
|
||||
{-|
|
||||
Describes what the user wants to do. This data type is a
|
||||
@@ -123,6 +124,8 @@ userApiRequest schema req reqBody =
|
||||
Nothing -> PayloadParseError "All lines must have same number of fields"
|
||||
Just json -> PayloadJSON json)
|
||||
(CSV.decodeByName reqBody)
|
||||
Right oa@OpenAPI ->
|
||||
PayloadParseError $ "Content-type not acceptable: " <> cs (show oa)
|
||||
-- This is a Left value because form-urlencoded is not a content
|
||||
-- type which we ever use for responses, only something we handle
|
||||
-- just this once for requests
|
||||
@@ -208,11 +211,13 @@ pickContentType :: Maybe BS.ByteString -> Either BS.ByteString ContentType
|
||||
pickContentType accept
|
||||
| isNothing accept || has ctAll || has ctJson = Right ApplicationJSON
|
||||
| has ctCsv = Right TextCSV
|
||||
| has ctOpenAPI = Right OpenAPI
|
||||
| otherwise = Left accept'
|
||||
where
|
||||
ctAll = "*/*"
|
||||
ctCsv = "text/csv"
|
||||
ctJson = "application/json"
|
||||
ctOpenAPI = "application/openapi+json"
|
||||
Just accept' = accept
|
||||
findInAccept = flip find $ parseHttpAccept accept'
|
||||
has = isJust . findInAccept . BS.isPrefixOf
|
||||
|
||||
+22
-5
@@ -60,6 +60,7 @@ import PostgREST.QueryBuilder ( callProc
|
||||
, ResultsWithCount
|
||||
)
|
||||
import PostgREST.Types
|
||||
import PostgREST.OpenAPI
|
||||
|
||||
import Prelude
|
||||
|
||||
@@ -180,9 +181,6 @@ app dbStructure conf apiRequest =
|
||||
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
||||
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
||||
body = encode (TableOptions cols pkeys)
|
||||
filterCol :: Schema -> TableName -> Column -> Bool
|
||||
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
|
||||
filterCol _ _ _ = False
|
||||
acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
|
||||
return $ responseLBS status200 [jsonH, allOrigins, acceptH] $ cs body
|
||||
|
||||
@@ -206,8 +204,13 @@ app dbStructure conf apiRequest =
|
||||
else return notFound
|
||||
|
||||
(ActionRead, TargetRoot, Nothing) -> do
|
||||
body <- encode <$> H.query schema accessibleTables
|
||||
return $ responseLBS status200 [jsonH] $ cs body
|
||||
let encodeApi ti = encodeOpenAPI ti host port
|
||||
host = configHost conf
|
||||
port = toInteger $ configPort conf
|
||||
encodeFn = if contentType == OpenAPI then encodeApi . toTableInfo else encode
|
||||
header = if contentType == OpenAPI then openapiH else jsonH
|
||||
body <- encodeFn <$> H.query schema accessibleTables
|
||||
return $ responseLBS status200 [header] $ cs body
|
||||
|
||||
(ActionInappropriate, _, _) -> return $ responseLBS status405 [] ""
|
||||
|
||||
@@ -220,8 +223,19 @@ app dbStructure conf apiRequest =
|
||||
(_, _, _) -> return notFound
|
||||
|
||||
where
|
||||
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
|
||||
toTableInfo = map (\t ->
|
||||
let tSchema = tableSchema t
|
||||
tTable = tableName t
|
||||
cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
||||
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
||||
in
|
||||
(t, cols, pkeys))
|
||||
notFound = responseLBS status404 [] ""
|
||||
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
|
||||
filterCol :: Schema -> TableName -> Column -> Bool
|
||||
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
|
||||
filterCol _ _ _ = False
|
||||
allPrKeys = dbPrimaryKeys dbStructure
|
||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||
schema = cs $ configSchema conf
|
||||
@@ -273,6 +287,9 @@ contentRangeH frm to total =
|
||||
jsonH :: Header
|
||||
jsonH = (hContentType, "application/json; charset=utf-8")
|
||||
|
||||
openapiH :: Header
|
||||
openapiH = (hContentType, "application/openapi+json; charset=utf-8")
|
||||
|
||||
formatRelationError :: Text -> Text
|
||||
formatRelationError = formatGeneralError
|
||||
"could not find foreign keys between these entities"
|
||||
|
||||
@@ -18,9 +18,9 @@ module PostgREST.Auth (
|
||||
, tokenJWT
|
||||
) where
|
||||
|
||||
import Lens.Micro
|
||||
import Lens.Micro.Aeson
|
||||
import Control.Lens
|
||||
import Data.Aeson (Value (..), parseJSON, toJSON)
|
||||
import Data.Aeson.Lens
|
||||
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.Vector as V
|
||||
|
||||
@@ -39,6 +39,7 @@ data AppConfig = AppConfig {
|
||||
configDatabase :: String
|
||||
, configAnonRole :: String
|
||||
, configSchema :: String
|
||||
, configHost :: String
|
||||
, configPort :: Int
|
||||
, configJwtSecret :: Secret
|
||||
, configPool :: Int
|
||||
@@ -51,6 +52,7 @@ argParser = AppConfig
|
||||
<$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL")
|
||||
<*> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE")
|
||||
<*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault)
|
||||
<*> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault)
|
||||
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
||||
<*> (secret . cs <$>
|
||||
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
|
||||
|
||||
@@ -6,6 +6,7 @@ module PostgREST.Middleware where
|
||||
import Data.Aeson (Value (..))
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.String.Conversions (cs)
|
||||
import Data.Maybe (fromMaybe, listToMaybe)
|
||||
import Data.Text
|
||||
import qualified Hasql.Transaction as H
|
||||
|
||||
@@ -17,7 +18,7 @@ import Network.Wai.Middleware.Cors (cors)
|
||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
||||
|
||||
import PostgREST.ApiRequest (ApiRequest(..), pickContentType)
|
||||
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..), pickContentType)
|
||||
import PostgREST.Auth (claimsToSQL)
|
||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||
import PostgREST.Error (errResponse)
|
||||
@@ -40,10 +41,14 @@ runWithClaims conf eClaims app req =
|
||||
|
||||
unsupportedAccept :: Application -> Application
|
||||
unsupportedAccept app req respond =
|
||||
case accept of
|
||||
Left _ -> respond $ errResponse status415 "Unsupported Accept header, try: application/json"
|
||||
Right _ -> app req respond
|
||||
case (isTargetRoot, accept) of
|
||||
(_, Left _) -> unsupportedAcceptRespond
|
||||
(False, Right OpenAPI) -> unsupportedAcceptRespond
|
||||
(_, Right _) -> app req respond
|
||||
where accept = pickContentType $ lookup hAccept $ requestHeaders req
|
||||
path = pathInfo req
|
||||
isTargetRoot = fromMaybe True $ (== "") <$> listToMaybe path
|
||||
unsupportedAcceptRespond = respond $ errResponse status415 "Unsupported Accept header, try: application/json"
|
||||
|
||||
defaultMiddle :: Application -> Application
|
||||
defaultMiddle =
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module PostgREST.OpenAPI (
|
||||
encodeOpenAPI
|
||||
) where
|
||||
|
||||
import Control.Lens
|
||||
import Data.Aeson (decode, encode)
|
||||
import Data.ByteString.Lazy (ByteString)
|
||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (Text, unpack, pack, concat, intercalate)
|
||||
import qualified Data.Set as Set
|
||||
|
||||
import Prelude hiding (concat)
|
||||
|
||||
import Data.Swagger
|
||||
|
||||
import PostgREST.ApiRequest (ContentType(..))
|
||||
import PostgREST.Config (prettyVersion)
|
||||
import PostgREST.QueryBuilder (operators)
|
||||
import PostgREST.Types (Table(..), Column(..))
|
||||
|
||||
makeMimeList :: [ContentType] -> MimeList
|
||||
makeMimeList cs = MimeList $ map (fromString . show) cs
|
||||
|
||||
toSwaggerType :: Text -> SwaggerType t
|
||||
toSwaggerType "text" = SwaggerString
|
||||
toSwaggerType "integer" = SwaggerInteger
|
||||
toSwaggerType "boolean" = SwaggerBoolean
|
||||
toSwaggerType "numeric" = SwaggerNumber
|
||||
toSwaggerType _ = SwaggerString
|
||||
|
||||
makeProperty :: Column -> (Text, Referenced Schema)
|
||||
makeProperty c = (colName c, Inline u)
|
||||
where
|
||||
r = mempty :: Schema
|
||||
s = if null $ colEnum c
|
||||
then r
|
||||
else r & enum_ .~ decode (encode (colEnum c))
|
||||
t = s & type_ .~ toSwaggerType (colType c)
|
||||
u = t & format ?~ colType c
|
||||
|
||||
makeProperties :: [Column] -> InsOrdHashMap Text (Referenced Schema)
|
||||
makeProperties cs = fromList $ map makeProperty cs
|
||||
|
||||
makeDefinition :: (Table, [Column], [Text]) -> (Text, Schema)
|
||||
makeDefinition (t, cs, _) =
|
||||
let tn = tableName t in
|
||||
(tn, (mempty :: Schema)
|
||||
& type_ .~ SwaggerObject
|
||||
& properties .~ makeProperties cs)
|
||||
|
||||
makeDefinitions :: [(Table, [Column], [Text])] -> InsOrdHashMap Text Schema
|
||||
makeDefinitions ti = fromList $ map makeDefinition ti
|
||||
|
||||
makeOperatorPattern :: Text
|
||||
makeOperatorPattern =
|
||||
intercalate "|"
|
||||
[ concat ["^", x, y, "[.]"] |
|
||||
x <- ["not[.]", ""],
|
||||
y <- map fst operators ]
|
||||
|
||||
makeRowFilter :: Column -> Param
|
||||
makeRowFilter c =
|
||||
(mempty :: Param)
|
||||
& name .~ colName c
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString
|
||||
& format ?~ colType c
|
||||
& pattern ?~ makeOperatorPattern)
|
||||
|
||||
makeRowFilters :: [Column] -> [Param]
|
||||
makeRowFilters = map makeRowFilter
|
||||
|
||||
makeOrderItems :: [Column] -> [Text]
|
||||
makeOrderItems cs =
|
||||
[ concat [x, y, z] |
|
||||
x <- map colName cs,
|
||||
y <- [".asc", ".desc", ""],
|
||||
z <- [".nullsfirst", ".nulllast", ""]
|
||||
]
|
||||
|
||||
makeRangeParams :: [Param]
|
||||
makeRangeParams =
|
||||
[ (mempty :: Param)
|
||||
& name .~ "Range"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString)
|
||||
, (mempty :: Param)
|
||||
& name .~ "Range-Unit"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString
|
||||
& default_ .~ decode "\"items\"")
|
||||
, (mempty :: Param)
|
||||
& name .~ "offset"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString)
|
||||
, (mempty :: Param)
|
||||
& name .~ "limit"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString)
|
||||
]
|
||||
|
||||
makePreferParam :: [Text] -> Param
|
||||
makePreferParam ts =
|
||||
(mempty :: Param)
|
||||
& name .~ "Prefer"
|
||||
& description ?~ "Preference"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString
|
||||
& enum_ .~ decode (encode ts))
|
||||
|
||||
makeSelectParam :: Param
|
||||
makeSelectParam =
|
||||
(mempty :: Param)
|
||||
& name .~ "select"
|
||||
& description ?~ "Filtering Columns"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString)
|
||||
|
||||
makeGetParams :: [Column] -> [Param]
|
||||
makeGetParams [] =
|
||||
makeRangeParams ++
|
||||
[ makeSelectParam
|
||||
, makePreferParam ["plurality=singular", "count=none"]
|
||||
]
|
||||
makeGetParams cs =
|
||||
makeRangeParams ++
|
||||
[ makeSelectParam
|
||||
, (mempty :: Param)
|
||||
& name .~ "order"
|
||||
& description ?~ "Ordering"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString
|
||||
& enum_ .~ decode (encode $ makeOrderItems cs))
|
||||
, makePreferParam ["plurality=singular", "count=none"]
|
||||
]
|
||||
|
||||
makeReturnPreferenceParam :: Param
|
||||
makeReturnPreferenceParam =
|
||||
makePreferParam ["return=representation", "return=minimal", "return=none"]
|
||||
|
||||
makePostParams :: Text -> [Param]
|
||||
makePostParams tn =
|
||||
[ makeReturnPreferenceParam
|
||||
, (mempty :: Param)
|
||||
& name .~ "body"
|
||||
& description ?~ tn
|
||||
& required ?~ False
|
||||
& schema .~ ParamBody (Ref (Reference tn))
|
||||
]
|
||||
|
||||
makeDeleteParams :: [Param]
|
||||
makeDeleteParams =
|
||||
[ makeReturnPreferenceParam ]
|
||||
|
||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
||||
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||
where
|
||||
tOp = (mempty :: Operation)
|
||||
& tags .~ Set.fromList [tn]
|
||||
& produces ?~ makeMimeList [ApplicationJSON, TextCSV]
|
||||
& at 200 ?~ "OK"
|
||||
getOp = tOp
|
||||
& parameters .~ map Inline (makeGetParams cs ++ rs)
|
||||
& at 206 ?~ "Partial Content"
|
||||
postOp = tOp
|
||||
& consumes ?~ makeMimeList [ApplicationJSON, TextCSV]
|
||||
& parameters .~ map Inline (makePostParams tn)
|
||||
& at 201 ?~ "Created"
|
||||
patchOp = tOp
|
||||
& consumes ?~ makeMimeList [ApplicationJSON, TextCSV]
|
||||
& parameters .~ map Inline (makePostParams tn ++ rs)
|
||||
& at 204 ?~ "No Content"
|
||||
deletOp = tOp
|
||||
& parameters .~ map Inline (makeDeleteParams ++ rs)
|
||||
pr = (mempty :: PathItem) & get ?~ getOp
|
||||
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
|
||||
p False = pr
|
||||
p True = pw
|
||||
rs = makeRowFilters cs
|
||||
tn = tableName t
|
||||
|
||||
makeRootPathItem :: (FilePath, PathItem)
|
||||
makeRootPathItem = ("/", p)
|
||||
where
|
||||
getOp = (mempty :: Operation)
|
||||
& tags .~ Set.fromList ["/"]
|
||||
& produces ?~ makeMimeList [ApplicationJSON, OpenAPI]
|
||||
& at 200 ?~ "OK"
|
||||
pr = (mempty :: PathItem) & get ?~ getOp
|
||||
p = pr
|
||||
|
||||
makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
||||
makePathItems ti = fromList $ makeRootPathItem : map makePathItem ti
|
||||
|
||||
escapeHostName :: String -> String
|
||||
escapeHostName "*" = "0.0.0.0"
|
||||
escapeHostName "*4" = "0.0.0.0"
|
||||
escapeHostName "!4" = "0.0.0.0"
|
||||
escapeHostName "*6" = "0.0.0.0"
|
||||
escapeHostName "!6" = "0.0.0.0"
|
||||
escapeHostName h = h
|
||||
|
||||
postgrestSpec:: [(Table, [Column], [Text])] -> String -> Integer -> Swagger
|
||||
postgrestSpec ti h p = (mempty :: Swagger)
|
||||
& basePath ?~ "/"
|
||||
& schemes ?~ [Http]
|
||||
& info .~ ((mempty :: Info)
|
||||
& version .~ pack prettyVersion
|
||||
& title .~ "PostgREST API"
|
||||
& description ?~ "This is a dynamic API generated by PostgREST")
|
||||
& host .~ h'
|
||||
& definitions .~ makeDefinitions ti
|
||||
& paths .~ makePathItems ti
|
||||
where
|
||||
h' = Just $ Host (escapeHostName h) (Just (fromInteger p))
|
||||
|
||||
encodeOpenAPI :: [(Table, [Column], [Text])] -> String -> Integer -> ByteString
|
||||
encodeOpenAPI ti h p = encode $ postgrestSpec ti h p
|
||||
+9
-1
@@ -16,9 +16,17 @@ extra-deps:
|
||||
- wai-cors-0.2.5
|
||||
- cryptohash-sha256-0.11.100.0
|
||||
- hackage-security-0.5.2.1
|
||||
|
||||
- unordered-containers-0.2.7.1
|
||||
- insert-ordered-containers-0.1.0.1
|
||||
- swagger2-2.1
|
||||
# - hjsonschema-0.10.0.2
|
||||
- hjsonpointer-0.3.0.1
|
||||
ghc-options:
|
||||
postgrest: -O2 -Werror -Wall -fwarn-identities
|
||||
|
||||
packages:
|
||||
- .
|
||||
- location:
|
||||
git: https://github.com/seagreen/hjsonschema
|
||||
commit: 075da33626d9d5cf20645a998b86518222d6b2d6
|
||||
extra-dep: true
|
||||
|
||||
@@ -8,7 +8,11 @@ import SpecHelper
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai (Application)
|
||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||
import Network.Wai.Test (SResponse(simpleStatus, simpleHeaders, simpleBody))
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Aeson (decode)
|
||||
import qualified Data.JsonSchema.Draft4 as D4
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec = do
|
||||
@@ -61,6 +65,33 @@ spec = do
|
||||
] |]
|
||||
{matchStatus = 200}
|
||||
|
||||
it "returns a valid openapi spec" $ do
|
||||
r <- request methodGet "/" [("Accept", "application/openapi+json")] ""
|
||||
liftIO $
|
||||
let respStatus = simpleStatus r in
|
||||
respStatus `shouldSatisfy`
|
||||
\s -> s == Status { statusCode = 200, statusMessage="OK" }
|
||||
liftIO $
|
||||
let respHeaders = simpleHeaders r in
|
||||
respHeaders `shouldSatisfy`
|
||||
\hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs
|
||||
liftIO $
|
||||
let respBody = simpleBody r
|
||||
schema :: D4.Schema
|
||||
schema = D4.emptySchema { D4._schemaRef = Just "openapi.json" }
|
||||
schemaContext :: D4.SchemaWithURI D4.Schema
|
||||
schemaContext = D4.SchemaWithURI
|
||||
{ D4._swSchema = schema
|
||||
, D4._swURI = Just "test/fixtures/openapi.json"
|
||||
}
|
||||
in
|
||||
D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right ()
|
||||
|
||||
it "should respond to openapi request on none root path with 415" $
|
||||
request methodGet "/none_root_path"
|
||||
(acceptHdrs "application/openapi+json") ""
|
||||
`shouldRespondWith` 415
|
||||
|
||||
describe "Table info" $ do
|
||||
it "The structure of complex views is correctly detected" $
|
||||
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
|
||||
|
||||
+3
-3
@@ -19,15 +19,15 @@ testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_t
|
||||
|
||||
testCfg :: AppConfig
|
||||
testCfg =
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "test" "localhost" 3000 (secret "safe") 10 Nothing True
|
||||
|
||||
testUnicodeCfg :: AppConfig
|
||||
testUnicodeCfg =
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "تست" 3000 (secret "safe") 10 Nothing True
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "تست" "localhost" 3000 (secret "safe") 10 Nothing True
|
||||
|
||||
testLtdRowsCfg :: AppConfig
|
||||
testLtdRowsCfg =
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 2) True
|
||||
AppConfig testDbConn "postgrest_test_anonymous" "test" "localhost" 3000 (secret "safe") 10 (Just 2) True
|
||||
|
||||
setupDb :: IO ()
|
||||
setupDb = do
|
||||
|
||||
Vendored
+151
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"id": "draft04.json",
|
||||
"$schema": "draft04.json",
|
||||
"description": "Core schema meta-schema",
|
||||
"definitions": {
|
||||
"schemaArray": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#" }
|
||||
},
|
||||
"positiveInteger": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"positiveIntegerDefault0": {
|
||||
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
|
||||
},
|
||||
"simpleTypes": {
|
||||
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
|
||||
},
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"multipleOf": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"exclusiveMinimum": true
|
||||
},
|
||||
"maximum": {
|
||||
"type": "number"
|
||||
},
|
||||
"exclusiveMaximum": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"minimum": {
|
||||
"type": "number"
|
||||
},
|
||||
"exclusiveMinimum": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"maxLength": { "$ref": "#/definitions/positiveInteger" },
|
||||
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"format": "regex"
|
||||
},
|
||||
"additionalItems": {
|
||||
"anyOf": [
|
||||
{ "type": "boolean" },
|
||||
{ "$ref": "#" }
|
||||
],
|
||||
"default": {}
|
||||
},
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#" },
|
||||
{ "$ref": "#/definitions/schemaArray" }
|
||||
],
|
||||
"default": {}
|
||||
},
|
||||
"maxItems": { "$ref": "#/definitions/positiveInteger" },
|
||||
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||
"uniqueItems": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
|
||||
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
|
||||
"required": { "$ref": "#/definitions/stringArray" },
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{ "type": "boolean" },
|
||||
{ "$ref": "#" }
|
||||
],
|
||||
"default": {}
|
||||
},
|
||||
"definitions": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#" },
|
||||
"default": {}
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#" },
|
||||
"default": {}
|
||||
},
|
||||
"patternProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#" },
|
||||
"default": {}
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#" },
|
||||
{ "$ref": "#/definitions/stringArray" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"enum": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"type": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/definitions/simpleTypes" },
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/simpleTypes" },
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"format": { "type": "string" },
|
||||
"allOf": { "$ref": "#/definitions/schemaArray" },
|
||||
"anyOf": { "$ref": "#/definitions/schemaArray" },
|
||||
"oneOf": { "$ref": "#/definitions/schemaArray" },
|
||||
"not": { "$ref": "#" }
|
||||
},
|
||||
"dependencies": {
|
||||
"exclusiveMaximum": [ "maximum" ],
|
||||
"exclusiveMinimum": [ "minimum" ]
|
||||
},
|
||||
"default": {}
|
||||
}
|
||||
Vendored
+1607
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user