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
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### 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
|
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ dependencies:
|
|||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- stack test
|
- 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 -- cabal update
|
||||||
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
|
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
|
||||||
- stack exec -- packdeps *.cabal || true
|
- stack exec -- packdeps *.cabal || true
|
||||||
|
|||||||
+5
-2
@@ -13,6 +13,7 @@ import PostgREST.DbStructure
|
|||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.String (IsString (..))
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -47,9 +48,11 @@ main = do
|
|||||||
hSetBuffering stderr NoBuffering
|
hSetBuffering stderr NoBuffering
|
||||||
|
|
||||||
conf <- readOptions
|
conf <- readOptions
|
||||||
let port = configPort conf
|
let host = configHost conf
|
||||||
|
port = configPort conf
|
||||||
pgSettings = cs (configDatabase conf)
|
pgSettings = cs (configDatabase conf)
|
||||||
appSettings = setPort port
|
appSettings = setHost (fromString host)
|
||||||
|
. setPort port
|
||||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||||
$ defaultSettings
|
$ defaultSettings
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -44,8 +44,8 @@ executable postgrest
|
|||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
, microlens >= 0.4.2 && < 0.5
|
, lens >=3.8 && < 5.0
|
||||||
, microlens-aeson >= 2.1.1 && < 2.2
|
, lens-aeson >= 1.0.0.0 && < 1.1.0.0
|
||||||
, mtl
|
, mtl
|
||||||
, optparse-applicative >= 0.11 && < 0.13
|
, optparse-applicative >= 0.11 && < 0.13
|
||||||
, parsec
|
, parsec
|
||||||
@@ -65,6 +65,8 @@ executable postgrest
|
|||||||
, wai-extra
|
, wai-extra
|
||||||
, wai-middleware-static >= 0.6.0
|
, wai-middleware-static >= 0.6.0
|
||||||
, warp >= 3.1.0
|
, warp >= 3.1.0
|
||||||
|
, insert-ordered-containers >= 0.1.0.1
|
||||||
|
, swagger2 >= 2.1
|
||||||
, HTTP
|
, HTTP
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
if !os(windows)
|
if !os(windows)
|
||||||
@@ -89,8 +91,8 @@ library
|
|||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
, microlens
|
, lens
|
||||||
, microlens-aeson
|
, lens-aeson
|
||||||
, mtl
|
, mtl
|
||||||
, optparse-applicative
|
, optparse-applicative
|
||||||
, parsec
|
, parsec
|
||||||
@@ -109,6 +111,8 @@ library
|
|||||||
, wai-extra
|
, wai-extra
|
||||||
, wai-middleware-static >= 0.6.0
|
, wai-middleware-static >= 0.6.0
|
||||||
, warp >= 3.1.0
|
, warp >= 3.1.0
|
||||||
|
, insert-ordered-containers >= 0.1.0.1
|
||||||
|
, swagger2 >= 2.1
|
||||||
|
|
||||||
Other-Modules: Paths_postgrest
|
Other-Modules: Paths_postgrest
|
||||||
Exposed-Modules: PostgREST.App
|
Exposed-Modules: PostgREST.App
|
||||||
@@ -122,6 +126,7 @@ library
|
|||||||
, PostgREST.RangeQuery
|
, PostgREST.RangeQuery
|
||||||
, PostgREST.ApiRequest
|
, PostgREST.ApiRequest
|
||||||
, PostgREST.Types
|
, PostgREST.Types
|
||||||
|
, PostgREST.OpenAPI
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
|
|
||||||
Test-Suite spec
|
Test-Suite spec
|
||||||
@@ -163,8 +168,8 @@ Test-Suite spec
|
|||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
, microlens
|
, lens
|
||||||
, microlens-aeson
|
, lens-aeson
|
||||||
, monad-control
|
, monad-control
|
||||||
, mtl
|
, mtl
|
||||||
, optparse-applicative
|
, optparse-applicative
|
||||||
@@ -186,5 +191,9 @@ Test-Suite spec
|
|||||||
, wai-extra
|
, wai-extra
|
||||||
, wai-middleware-static
|
, wai-middleware-static
|
||||||
, warp
|
, warp
|
||||||
|
, insert-ordered-containers
|
||||||
|
, hjsonpointer
|
||||||
|
, hjsonschema
|
||||||
|
, swagger2
|
||||||
, HTTP
|
, HTTP
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ data Target = TargetIdent QualifiedIdentifier
|
|||||||
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
|
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
|
||||||
-- | Enumeration of currently supported content types for
|
-- | Enumeration of currently supported content types for
|
||||||
-- route responses and upload payloads
|
-- route responses and upload payloads
|
||||||
data ContentType = ApplicationJSON | TextCSV deriving Eq
|
data ContentType = ApplicationJSON | TextCSV | OpenAPI deriving Eq
|
||||||
instance Show ContentType where
|
instance Show ContentType where
|
||||||
show ApplicationJSON = "application/json; charset=utf-8"
|
show ApplicationJSON = "application/json; charset=utf-8"
|
||||||
show TextCSV = "text/csv; 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
|
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"
|
Nothing -> PayloadParseError "All lines must have same number of fields"
|
||||||
Just json -> PayloadJSON json)
|
Just json -> PayloadJSON json)
|
||||||
(CSV.decodeByName reqBody)
|
(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
|
-- This is a Left value because form-urlencoded is not a content
|
||||||
-- type which we ever use for responses, only something we handle
|
-- type which we ever use for responses, only something we handle
|
||||||
-- just this once for requests
|
-- just this once for requests
|
||||||
@@ -208,11 +211,13 @@ pickContentType :: Maybe BS.ByteString -> Either BS.ByteString ContentType
|
|||||||
pickContentType accept
|
pickContentType accept
|
||||||
| isNothing accept || has ctAll || has ctJson = Right ApplicationJSON
|
| isNothing accept || has ctAll || has ctJson = Right ApplicationJSON
|
||||||
| has ctCsv = Right TextCSV
|
| has ctCsv = Right TextCSV
|
||||||
|
| has ctOpenAPI = Right OpenAPI
|
||||||
| otherwise = Left accept'
|
| otherwise = Left accept'
|
||||||
where
|
where
|
||||||
ctAll = "*/*"
|
ctAll = "*/*"
|
||||||
ctCsv = "text/csv"
|
ctCsv = "text/csv"
|
||||||
ctJson = "application/json"
|
ctJson = "application/json"
|
||||||
|
ctOpenAPI = "application/openapi+json"
|
||||||
Just accept' = accept
|
Just accept' = accept
|
||||||
findInAccept = flip find $ parseHttpAccept accept'
|
findInAccept = flip find $ parseHttpAccept accept'
|
||||||
has = isJust . findInAccept . BS.isPrefixOf
|
has = isJust . findInAccept . BS.isPrefixOf
|
||||||
|
|||||||
+22
-5
@@ -60,6 +60,7 @@ import PostgREST.QueryBuilder ( callProc
|
|||||||
, ResultsWithCount
|
, ResultsWithCount
|
||||||
)
|
)
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
|
import PostgREST.OpenAPI
|
||||||
|
|
||||||
import Prelude
|
import Prelude
|
||||||
|
|
||||||
@@ -180,9 +181,6 @@ app dbStructure conf apiRequest =
|
|||||||
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
||||||
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
||||||
body = encode (TableOptions cols pkeys)
|
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
|
acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
|
||||||
return $ responseLBS status200 [jsonH, allOrigins, acceptH] $ cs body
|
return $ responseLBS status200 [jsonH, allOrigins, acceptH] $ cs body
|
||||||
|
|
||||||
@@ -206,8 +204,13 @@ app dbStructure conf apiRequest =
|
|||||||
else return notFound
|
else return notFound
|
||||||
|
|
||||||
(ActionRead, TargetRoot, Nothing) -> do
|
(ActionRead, TargetRoot, Nothing) -> do
|
||||||
body <- encode <$> H.query schema accessibleTables
|
let encodeApi ti = encodeOpenAPI ti host port
|
||||||
return $ responseLBS status200 [jsonH] $ cs body
|
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 [] ""
|
(ActionInappropriate, _, _) -> return $ responseLBS status405 [] ""
|
||||||
|
|
||||||
@@ -220,8 +223,19 @@ app dbStructure conf apiRequest =
|
|||||||
(_, _, _) -> return notFound
|
(_, _, _) -> return notFound
|
||||||
|
|
||||||
where
|
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 [] ""
|
notFound = responseLBS status404 [] ""
|
||||||
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
|
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
|
allPrKeys = dbPrimaryKeys dbStructure
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
schema = cs $ configSchema conf
|
schema = cs $ configSchema conf
|
||||||
@@ -273,6 +287,9 @@ contentRangeH frm to total =
|
|||||||
jsonH :: Header
|
jsonH :: Header
|
||||||
jsonH = (hContentType, "application/json; charset=utf-8")
|
jsonH = (hContentType, "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
openapiH :: Header
|
||||||
|
openapiH = (hContentType, "application/openapi+json; charset=utf-8")
|
||||||
|
|
||||||
formatRelationError :: Text -> Text
|
formatRelationError :: Text -> Text
|
||||||
formatRelationError = formatGeneralError
|
formatRelationError = formatGeneralError
|
||||||
"could not find foreign keys between these entities"
|
"could not find foreign keys between these entities"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ module PostgREST.Auth (
|
|||||||
, tokenJWT
|
, tokenJWT
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Lens.Micro
|
import Control.Lens
|
||||||
import Lens.Micro.Aeson
|
|
||||||
import Data.Aeson (Value (..), parseJSON, toJSON)
|
import Data.Aeson (Value (..), parseJSON, toJSON)
|
||||||
|
import Data.Aeson.Lens
|
||||||
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
|
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ data AppConfig = AppConfig {
|
|||||||
configDatabase :: String
|
configDatabase :: String
|
||||||
, configAnonRole :: String
|
, configAnonRole :: String
|
||||||
, configSchema :: String
|
, configSchema :: String
|
||||||
|
, configHost :: String
|
||||||
, configPort :: Int
|
, configPort :: Int
|
||||||
, configJwtSecret :: Secret
|
, configJwtSecret :: Secret
|
||||||
, configPool :: Int
|
, 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")
|
<$> 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 "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 "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)
|
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
|
||||||
<*> (secret . cs <$>
|
<*> (secret . cs <$>
|
||||||
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
|
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 Data.Aeson (Value (..))
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.Maybe (fromMaybe, listToMaybe)
|
||||||
import Data.Text
|
import Data.Text
|
||||||
import qualified Hasql.Transaction as H
|
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.Gzip (def, gzip)
|
||||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
import Network.Wai.Middleware.Static (only, staticPolicy)
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest(..), pickContentType)
|
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..), pickContentType)
|
||||||
import PostgREST.Auth (claimsToSQL)
|
import PostgREST.Auth (claimsToSQL)
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (errResponse)
|
import PostgREST.Error (errResponse)
|
||||||
@@ -40,10 +41,14 @@ runWithClaims conf eClaims app req =
|
|||||||
|
|
||||||
unsupportedAccept :: Application -> Application
|
unsupportedAccept :: Application -> Application
|
||||||
unsupportedAccept app req respond =
|
unsupportedAccept app req respond =
|
||||||
case accept of
|
case (isTargetRoot, accept) of
|
||||||
Left _ -> respond $ errResponse status415 "Unsupported Accept header, try: application/json"
|
(_, Left _) -> unsupportedAcceptRespond
|
||||||
Right _ -> app req respond
|
(False, Right OpenAPI) -> unsupportedAcceptRespond
|
||||||
|
(_, Right _) -> app req respond
|
||||||
where accept = pickContentType $ lookup hAccept $ requestHeaders req
|
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 :: Application -> Application
|
||||||
defaultMiddle =
|
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
|
- wai-cors-0.2.5
|
||||||
- cryptohash-sha256-0.11.100.0
|
- cryptohash-sha256-0.11.100.0
|
||||||
- hackage-security-0.5.2.1
|
- 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:
|
ghc-options:
|
||||||
postgrest: -O2 -Werror -Wall -fwarn-identities
|
postgrest: -O2 -Werror -Wall -fwarn-identities
|
||||||
|
|
||||||
packages:
|
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.HTTP.Types
|
||||||
import Network.Wai (Application)
|
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 :: SpecWith Application
|
||||||
spec = do
|
spec = do
|
||||||
@@ -61,6 +65,33 @@ spec = do
|
|||||||
] |]
|
] |]
|
||||||
{matchStatus = 200}
|
{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
|
describe "Table info" $ do
|
||||||
it "The structure of complex views is correctly detected" $
|
it "The structure of complex views is correctly detected" $
|
||||||
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
|
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
|
||||||
|
|||||||
+3
-3
@@ -19,15 +19,15 @@ testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_t
|
|||||||
|
|
||||||
testCfg :: AppConfig
|
testCfg :: AppConfig
|
||||||
testCfg =
|
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
|
||||||
testUnicodeCfg =
|
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
|
||||||
testLtdRowsCfg =
|
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 :: IO ()
|
||||||
setupDb = do
|
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