Merge pull request #639 from hudayou/swagger2

Provide a swagger2 spec for the dynamic API
This commit is contained in:
Joe Nelson
2016-06-20 15:12:58 -07:00
committed by GitHub
15 changed files with 2107 additions and 26 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -1
View File
@@ -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
View File
@@ -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"
+2 -2
View File
@@ -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
+2
View File
@@ -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))
+9 -4
View File
@@ -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 =
+241
View File
@@ -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
View File
@@ -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
+32 -1
View File
@@ -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
View File
@@ -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
+151
View File
@@ -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": {}
}
+1607
View File
File diff suppressed because it is too large Load Diff