openapi3 3.2.2 → 3.2.3
raw patch · 12 files changed
+158/−50 lines, 12 filesdep −networkdep ~aesondep ~basedep ~base-compat-batteries
Dependencies removed: network
Dependency ranges changed: aeson, base, base-compat-batteries, hspec, mtl, template-haskell, transformers
Files
- CHANGELOG.md +9/−0
- openapi3.cabal +11/−11
- src/Data/OpenApi.hs +7/−5
- src/Data/OpenApi/Declare.hs +0/−1
- src/Data/OpenApi/Internal.hs +54/−3
- src/Data/OpenApi/Internal/Schema.hs +9/−3
- src/Data/OpenApi/Internal/Schema/Validation.hs +3/−0
- src/Data/OpenApi/Operation.hs +5/−4
- src/Data/OpenApi/Optics.hs +3/−2
- test/Data/OpenApi/CommonTestTypes.hs +7/−14
- test/Data/OpenApiSpec.hs +49/−6
- test/doctests.hs +1/−1
CHANGELOG.md view
@@ -1,6 +1,15 @@ Unreleased ---------- +3.2.3+-----++- Fix generation of `allOf` / `oneOf` schemas when variants are not objects [#49](https://github.com/biocad/openapi3/pull/49).+- Support parsing OpenAPI 3.0.1, 3.0.2, 3.0.3 versions [#68](https://github.com/biocad/openapi3/pull/68).+- Support GHC-9.4 [#61](https://github.com/biocad/openapi3/pull/61).+- Output `scopes` in schema even if empty [#65](https://github.com/biocad/openapi3/pull/65).+- Fix `mtl-2.3` compatibility [#60](https://github.com/biocad/openapi3/pull/60).+ 3.2.2 -----
openapi3.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: openapi3-version: 3.2.2+version: 3.2.3 synopsis: OpenAPI 3.0 data model category: Web, Swagger, OpenApi@@ -61,21 +61,21 @@ -- GHC boot libraries build-depends:- base >=4.11.1.0 && <4.17+ base >=4.11.1.0 && <4.18 , bytestring >=0.10.8.2 && <0.12 , containers >=0.5.11.0 && <0.7- , template-haskell >=2.13.0.0 && <2.19+ , template-haskell >=2.13.0.0 && <2.20 , time >=1.8.0.2 && <1.14- , transformers >=0.5.5.0 && <0.6+ , transformers >=0.5.5.0 && <0.7 build-depends:- mtl >=2.2.2 && <2.3+ mtl >=2.2.2 && <2.4 , text >=1.2.3.1 && <2.1 -- other dependencies build-depends: base-compat-batteries >=0.11.1 && <0.13- , aeson >=1.4.2.0 && <1.6 || >=2.0.1.0 && < 2.1+ , aeson >=1.4.2.0 && <1.6 || >=2.0.1.0 && < 2.2 , aeson-pretty >=0.8.7 && <0.9 -- cookie 0.4.3 is needed by GHC 7.8 due to time>=1.4 constraint , cookie >=0.4.3 && <0.5@@ -83,14 +83,13 @@ , hashable >=1.2.7.0 && <1.5 , http-media >=0.8.0.0 && <0.9 , insert-ordered-containers >=0.2.3 && <0.3- , lens >=4.16.1 && <5.2- , network >=2.6.3.5 && <3.2+ , lens >=4.16.1 && <5.3 , optics-core >=0.2 && <0.5 , optics-th >=0.2 && <0.5 , scientific >=0.3.6.2 && <0.4 , unordered-containers >=0.2.9.0 && <0.3 , uuid-types >=1.0.3 && <1.1- , vector >=0.12.0.1 && <0.13+ , vector >=0.12.0.1 && <0.14 , QuickCheck >=2.10.1 && <2.15 default-language: Haskell2010@@ -122,14 +121,14 @@ -- test-suite only dependencies build-depends:- hspec >=2.5.5 && <2.10+ hspec >=2.5.5 && <2.11 , HUnit >=1.6.0.0 && <1.7 , quickcheck-instances >=0.3.19 && <0.14 , utf8-string >=1.0.1.1 && <1.1 -- https://github.com/haskell/cabal/issues/3708 build-tool-depends:- hspec-discover:hspec-discover >=2.5.5 && <2.10+ hspec-discover:hspec-discover >=2.5.5 && <2.11 other-modules: SpecCommon@@ -148,6 +147,7 @@ hs-source-dirs: test main-is: doctests.hs type: exitcode-stdio-1.0+ build-depends: base, openapi3 executable example hs-source-dirs: examples
src/Data/OpenApi.hs view
@@ -137,10 +137,13 @@ -- >>> import Data.Proxy -- >>> import GHC.Generics -- >>> import qualified Data.ByteString.Lazy.Char8 as BSL+-- >>> import Data.OpenApi.Internal+-- >>> import Data.OpenApi.Internal.Schema+-- >>> import qualified Data.HashMap.Strict.InsOrd as IOHM -- >>> import Data.OpenApi.Internal.Utils+-- >>> import Data.OpenApi.Lens -- >>> :set -XDeriveGeneric -- >>> :set -XOverloadedStrings--- >>> :set -XOverloadedLists -- >>> :set -fno-warn-missing-methods -- $howto@@ -211,9 +214,9 @@ -- -- >>> :{ -- BSL.putStrLn $ encodePretty $ (mempty :: OpenApi)--- & components . schemas .~ [ ("User", mempty & type_ ?~ OpenApiString) ]+-- & components . schemas .~ IOHM.fromList [ ("User", mempty & type_ ?~ OpenApiString) ] -- & paths .~--- [ ("/user", mempty & get ?~ (mempty+-- IOHM.fromList [ ("/user", mempty & get ?~ (mempty -- & at 200 ?~ ("OK" & _Inline.content.at "application/json" ?~ (mempty & schema ?~ Ref (Reference "User"))) -- & at 404 ?~ "User info not found")) ] -- :}@@ -409,8 +412,7 @@ -- ], -- "type": "object" -- }--- ],--- "type": "object"+-- ] -- } -- $manipulation
src/Data/OpenApi/Declare.hs view
@@ -16,7 +16,6 @@ import Control.Monad import Control.Monad.Cont (ContT)-import Control.Monad.List (ListT) import Control.Monad.Reader (ReaderT) import Control.Monad.Trans import Control.Monad.Trans.Except (ExceptT)
src/Data/OpenApi/Internal.hs view
@@ -43,7 +43,6 @@ import GHC.Generics (Generic) import Network.HTTP.Media (MediaType, mainType, parameters, parseAccept, subType, (//), (/:))-import Network.Socket (HostName, PortNumber) import Text.Read (readMaybe) import Data.HashMap.Strict.InsOrd (InsOrdHashMap)@@ -56,6 +55,9 @@ sopSwaggerGenericToJSON, sopSwaggerGenericToJSONWithOpts) import Data.OpenApi.Internal.Utils import Generics.SOP.TH (deriveGeneric)+import Data.Version+import Control.Monad (unless)+import Text.ParserCombinators.ReadP (readP_to_S) -- $setup -- >>> :seti -XDataKinds@@ -99,8 +101,19 @@ -- | Additional external documentation. , _openApiExternalDocs :: Maybe ExternalDocs++ , -- | The spec of OpenApi this spec adheres to. Must be between 'lowerOpenApiSpecVersion' and 'upperOpenApiSpecVersion'+ _openApiOpenapi :: OpenApiSpecVersion } deriving (Eq, Show, Generic, Data, Typeable) +-- | This is the lower version of the OpenApi Spec this library can parse or produce+lowerOpenApiSpecVersion :: Version+lowerOpenApiSpecVersion = makeVersion [3, 0, 0]++-- | This is the upper version of the OpenApi Spec this library can parse or produce+upperOpenApiSpecVersion :: Version+upperOpenApiSpecVersion = makeVersion [3, 0, 3]+ -- | The object provides metadata about the API. -- The metadata MAY be used by the clients if needed, -- and MAY be presented in editing or documentation generation tools for convenience.@@ -962,6 +975,8 @@ | AdditionalPropertiesSchema (Referenced Schema) deriving (Eq, Show, Data, Typeable) +newtype OpenApiSpecVersion = OpenApiSpecVersion {getVersion :: Version} deriving (Eq, Show, Generic, Data, Typeable)+ ------------------------------------------------------------------------------- -- Generic instances -------------------------------------------------------------------------------@@ -984,11 +999,19 @@ deriveGeneric ''Example deriveGeneric ''Encoding deriveGeneric ''Link+deriveGeneric ''OpenApiSpecVersion -- ======================================================================= -- Monoid instances -- ======================================================================= +instance Semigroup OpenApiSpecVersion where+ (<>) (OpenApiSpecVersion a) (OpenApiSpecVersion b) = OpenApiSpecVersion $ max a b + +instance Monoid OpenApiSpecVersion where+ mempty = OpenApiSpecVersion (makeVersion [3,0,0])+ mappend = (<>)+ instance Semigroup OpenApi where (<>) = genericMappend instance Monoid OpenApi where@@ -1126,6 +1149,7 @@ instance SwaggerMonoid Operation instance (Eq a, Hashable a) => SwaggerMonoid (InsOrdHashSet a) instance SwaggerMonoid SecurityDefinitions+instance SwaggerMonoid OpenApiSpecVersion instance SwaggerMonoid MimeList deriving instance SwaggerMonoid URL@@ -1258,6 +1282,9 @@ -- Manual ToJSON instances -- ======================================================================= +instance ToJSON OpenApiSpecVersion where + toJSON (OpenApiSpecVersion v)= toJSON . showVersion $ v+ instance ToJSON MediaType where toJSON = toJSON . show toEncoding = toEncoding . show@@ -1266,7 +1293,10 @@ toJSONKey = JSON.toJSONKeyText (Text.pack . show) instance (Eq p, ToJSON p, AesonDefaultValue p) => ToJSON (OAuth2Flow p) where- toJSON = sopSwaggerGenericToJSON+ toJSON a = sopSwaggerGenericToJSON a &+ if InsOrdHashMap.null (_oAuth2Scopes a)+ then (<+> object ["scopes" .= object []])+ else id toEncoding = sopSwaggerGenericToEncoding instance ToJSON OAuth2Flows where@@ -1422,6 +1452,22 @@ -- Manual FromJSON instances -- ======================================================================= +instance FromJSON OpenApiSpecVersion where+ parseJSON = withText "OpenApiSpecVersion" $ \str ->+ let validatedVersion :: Either String Version+ validatedVersion = do+ parsedVersion <- readVersion str + unless ((parsedVersion >= lowerOpenApiSpecVersion) && (parsedVersion <= upperOpenApiSpecVersion)) $+ Left ("The provided version " <> showVersion parsedVersion <> " is out of the allowed range >=" <> showVersion lowerOpenApiSpecVersion <> " && <=" <> showVersion upperOpenApiSpecVersion)+ return parsedVersion+ in + either fail (return . OpenApiSpecVersion) validatedVersion+ where+ readVersion :: Text -> Either String Version+ readVersion v = case readP_to_S parseVersion (Text.unpack v) of + [] -> Left $ "Failed to parse as a version string " <> Text.unpack v+ solutions -> Right (fst . last $ solutions)+ instance FromJSON MediaType where parseJSON = withText "MediaType" $ \str -> maybe (fail $ "Invalid media type literal " <> Text.unpack str) pure $ parseAccept $ encodeUtf8 str@@ -1591,8 +1637,10 @@ swaggerAesonOptions _ = mkSwaggerAesonOptions "securityScheme" & saoSubObject ?~ "type" instance HasSwaggerAesonOptions Schema where swaggerAesonOptions _ = mkSwaggerAesonOptions "schema" & saoSubObject ?~ "paramSchema"+instance HasSwaggerAesonOptions OpenApiSpecVersion where+ swaggerAesonOptions _ = mkSwaggerAesonOptions "openapi" instance HasSwaggerAesonOptions OpenApi where- swaggerAesonOptions _ = mkSwaggerAesonOptions "swagger" & saoAdditionalPairs .~ [("openapi", "3.0.0")]+ swaggerAesonOptions _ = mkSwaggerAesonOptions "swagger" instance HasSwaggerAesonOptions Example where swaggerAesonOptions _ = mkSwaggerAesonOptions "example" instance HasSwaggerAesonOptions Encoding where@@ -1601,6 +1649,9 @@ instance HasSwaggerAesonOptions Link where swaggerAesonOptions _ = mkSwaggerAesonOptions "link" +instance AesonDefaultValue Version where + defaultValue = Just (makeVersion [3,0,0])+instance AesonDefaultValue OpenApiSpecVersion instance AesonDefaultValue Server instance AesonDefaultValue Components instance AesonDefaultValue OAuth2ImplicitFlow
src/Data/OpenApi/Internal/Schema.hs view
@@ -28,7 +28,7 @@ import Data.Data.Lens (template) import Control.Monad-import Control.Monad.Writer+import Control.Monad.Writer hiding (First, Last) import Data.Aeson (Object (..), SumEncoding (..), ToJSON (..), ToJSONKey (..), ToJSONKeyFunction (..), Value (..)) import Data.Char@@ -50,6 +50,7 @@ import Data.Scientific (Scientific) import Data.Fixed (Fixed, HasResolution, Pico) import Data.Set (Set)+import Data.Semigroup import qualified Data.Text as T import qualified Data.Text.Lazy as TL import Data.Time@@ -92,6 +93,13 @@ rename :: Maybe T.Text -> NamedSchema -> NamedSchema rename name (NamedSchema _ schema) = NamedSchema name schema +-- $setup+-- >>> import Data.Aeson.Types (toJSONKeyText)+-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL+-- >>> import Data.OpenApi.Internal+-- >>> import Data.OpenApi.Internal.Utils (encodePretty)+-- >>> import Data.OpenApi.Lens (name, schema)+ -- | Convert a type into @'Schema'@. -- -- An example type and instance:@@ -1026,7 +1034,6 @@ | otherwise = do (schemas, _) <- runWriterT declareSumSchema return $ unnamed $ mempty- & type_ ?~ OpenApiObject & oneOf ?~ (snd <$> schemas) where declareSumSchema = gsumToSchema opts proxy@@ -1071,7 +1078,6 @@ -- In the remaining cases we combine "tag" object and "contents" object using allOf. _ -> Inline $ mempty- & type_ ?~ OpenApiObject & allOf ?~ [Inline $ mempty & type_ ?~ OpenApiObject & required .~ (T.pack tagField : if isRecord then [] else [T.pack contentsField])
src/Data/OpenApi/Internal/Schema/Validation.hs view
@@ -59,6 +59,9 @@ import Data.OpenApi.Internal.Utils import Data.OpenApi.Lens +-- $setup+-- >>> import Data.OpenApi.Internal.Schema.Validation+ -- | Validate @'ToJSON'@ instance matches @'ToSchema'@ for a given value. -- This can be used with QuickCheck to ensure those instances are coherent: --
src/Data/OpenApi/Operation.hs view
@@ -55,12 +55,13 @@ -- >>> import Data.Proxy -- >>> import Data.Time -- >>> import qualified Data.ByteString.Lazy.Char8 as BSL+-- >>> import qualified Data.HashMap.Strict.InsOrd as IOHM -- >>> import Data.OpenApi.Internal.Utils -- | Prepend path piece to all operations of the spec. -- Leading and trailing slashes are trimmed/added automatically. ----- >>> let api = (mempty :: OpenApi) & paths .~ [("/info", mempty)]+-- >>> let api = (mempty :: OpenApi) & paths .~ IOHM.fromList [("/info", mempty)] -- >>> BSL.putStrLn $ encodePretty $ prependPath "user/{user_id}" api ^. paths -- { -- "/user/{user_id}/info": {}@@ -83,8 +84,8 @@ -- by both path and method. -- -- >>> let ok = (mempty :: Operation) & at 200 ?~ "OK"--- >>> let api = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ ok & post ?~ ok)]--- >>> let sub = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ mempty)]+-- >>> let api = (mempty :: OpenApi) & paths .~ IOHM.fromList [("/user", mempty & get ?~ ok & post ?~ ok)]+-- >>> let sub = (mempty :: OpenApi) & paths .~ IOHM.fromList [("/user", mempty & get ?~ mempty)] -- >>> BSL.putStrLn $ encodePretty api -- { -- "components": {},@@ -215,7 +216,7 @@ -- -- Example: ----- >>> let api = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ mempty)]+-- >>> let api = (mempty :: OpenApi) & paths .~ IOHM.fromList [("/user", mempty & get ?~ mempty)] -- >>> let res = declareResponse "application/json" (Proxy :: Proxy Day) -- >>> BSL.putStrLn $ encodePretty $ api & setResponse 200 res -- {
src/Data/OpenApi/Optics.hs view
@@ -18,14 +18,15 @@ -- >>> import Optics.Core -- >>> :set -XOverloadedLabels -- >>> import qualified Data.ByteString.Lazy.Char8 as BSL+-- >>> import qualified Data.HashMap.Strict.InsOrd as IOHM -- -- Example from the "Data.OpenApi" module using @optics@: -- -- >>> :{ -- BSL.putStrLn $ encodePretty $ (mempty :: OpenApi)--- & #components % #schemas .~ [ ("User", mempty & #type ?~ OpenApiString) ]+-- & #components % #schemas .~ IOHM.fromList [ ("User", mempty & #type ?~ OpenApiString) ] -- & #paths .~--- [ ("/user", mempty & #get ?~ (mempty+-- IOHM.fromList [ ("/user", mempty & #get ?~ (mempty -- & at 200 ?~ ("OK" & #_Inline % #content % at "application/json" ?~ (mempty & #schema ?~ Ref (Reference "User"))) -- & at 404 ?~ "User info not found")) ] -- :}
test/Data/OpenApi/CommonTestTypes.hs view
@@ -312,8 +312,7 @@ } } }- ],- "type": "object"+ ] } |]@@ -398,8 +397,7 @@ } } }- ],- "type": "object"+ ] } |] @@ -455,8 +453,7 @@ } } }- ],- "type": "object"+ ] } |] @@ -705,8 +702,7 @@ } } }- ],- "type": "object"+ ] } |] @@ -789,8 +785,7 @@ } } }- ],- "type": "object"+ ] } |] @@ -929,8 +924,7 @@ "required": ["tag", "contents"], "type": "object" }- ],- "type": "object"+ ] }, "Noun": { "properties": {@@ -969,8 +963,7 @@ "required": ["tag", "contents"], "type": "object" }- ],- "type": "object"+ ] }, "Omitted": { "properties": {
test/Data/OpenApiSpec.hs view
@@ -36,9 +36,13 @@ describe "Responses Definition Object" $ responsesDefinitionExample <=> responsesDefinitionExampleJSON describe "Security Definitions Object" $ securityDefinitionsExample <=> securityDefinitionsExampleJSON describe "OAuth2 Security Definitions with merged Scope" $ oAuth2SecurityDefinitionsExample <=> oAuth2SecurityDefinitionsExampleJSON+ describe "OAuth2 Security Definitions with empty Scope" $ oAuth2SecurityDefinitionsEmptyExample <=> oAuth2SecurityDefinitionsEmptyExampleJSON describe "Composition Schema Example" $ compositionSchemaExample <=> compositionSchemaExampleJSON describe "Swagger Object" $ do- context "Example with no paths" $ emptyPathsFieldExample <=> emptyPathsFieldExampleJSON+ context "Example with no paths" $ do + emptyPathsFieldExample <=> emptyPathsFieldExampleJSON+ it "fails to parse a spec with a wrong Openapi spec version" $ do+ (fromJSON wrongVersionExampleJSON :: Result OpenApi) `shouldBe` Error "The provided version 3.0.4 is out of the allowed range >=3.0.0 && <=3.0.3" context "Todo Example" $ swaggerExample <=> swaggerExampleJSON context "PetStore Example" $ do it "decodes successfully" $ do@@ -47,7 +51,7 @@ (toJSON :: OpenApi -> Value) <$> fromJSON petstoreExampleJSON `shouldBe` Success petstoreExampleJSON context "Security schemes" $ do it "merged correctly" $ do- let merged = oAuth2SecurityDefinitionsReadOpenApi <> oAuth2SecurityDefinitionsWriteOpenApi+ let merged = oAuth2SecurityDefinitionsReadOpenApi <> oAuth2SecurityDefinitionsWriteOpenApi <> oAuth2SecurityDefinitionsEmptyOpenApi merged `shouldBe` oAuth2SecurityDefinitionsOpenApi main :: IO ()@@ -508,10 +512,22 @@ , _securitySchemeDescription = Nothing }) ] +oAuth2SecurityDefinitionsEmptyExample :: SecurityDefinitions+oAuth2SecurityDefinitionsEmptyExample = SecurityDefinitions+ [ ("petstore_auth", SecurityScheme+ { _securitySchemeType = SecuritySchemeOAuth2 (mempty & implicit ?~ OAuth2Flow+ { _oAuth2Params = OAuth2ImplicitFlow "http://swagger.io/api/oauth/dialog"+ , _oAath2RefreshUrl = Nothing+ , _oAuth2Scopes = []+ } )+ , _securitySchemeDescription = Nothing })+ ]+ oAuth2SecurityDefinitionsExample :: SecurityDefinitions oAuth2SecurityDefinitionsExample = oAuth2SecurityDefinitionsWriteExample <>- oAuth2SecurityDefinitionsReadExample+ oAuth2SecurityDefinitionsReadExample <>+ oAuth2SecurityDefinitionsEmptyExample oAuth2SecurityDefinitionsExampleJSON :: Value oAuth2SecurityDefinitionsExampleJSON = [aesonQQ|@@ -531,6 +547,21 @@ } |] +oAuth2SecurityDefinitionsEmptyExampleJSON :: Value+oAuth2SecurityDefinitionsEmptyExampleJSON = [aesonQQ|+{+ "petstore_auth": {+ "type": "oauth2",+ "flows": {+ "implicit": {+ "scopes": {},+ "authorizationUrl": "http://swagger.io/api/oauth/dialog"+ }+ }+ }+}+|]+ oAuth2SecurityDefinitionsReadOpenApi :: OpenApi oAuth2SecurityDefinitionsReadOpenApi = mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsReadExample@@ -539,6 +570,10 @@ oAuth2SecurityDefinitionsWriteOpenApi = mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsWriteExample +oAuth2SecurityDefinitionsEmptyOpenApi :: OpenApi+oAuth2SecurityDefinitionsEmptyOpenApi =+ mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsEmptyExample+ oAuth2SecurityDefinitionsOpenApi :: OpenApi oAuth2SecurityDefinitionsOpenApi = mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsExample@@ -550,6 +585,16 @@ emptyPathsFieldExample :: OpenApi emptyPathsFieldExample = mempty +wrongVersionExampleJSON :: Value+wrongVersionExampleJSON = [aesonQQ|+{+ "openapi": "3.0.4",+ "info": {"version": "", "title": ""},+ "paths": {},+ "components": {}+}+|]+ emptyPathsFieldExampleJSON :: Value emptyPathsFieldExampleJSON = [aesonQQ| {@@ -663,7 +708,7 @@ petstoreExampleJSON :: Value petstoreExampleJSON = [aesonQQ| {- "openapi": "3.0.0",+ "openapi": "3.0.3", "info": { "version": "1.0.0", "title": "Swagger Petstore",@@ -933,7 +978,6 @@ compositionSchemaExample :: Schema compositionSchemaExample = mempty- & type_ ?~ OpenApiObject & Data.OpenApi.allOf ?~ [ Ref (Reference "Other") , Inline (mempty@@ -946,7 +990,6 @@ compositionSchemaExampleJSON :: Value compositionSchemaExampleJSON = [aesonQQ| {- "type": "object", "allOf": [ { "$ref": "#/components/schemas/Other"
test/doctests.hs view
@@ -9,4 +9,4 @@ traverse_ putStrLn args doctest args where- args = flags ++ pkgs ++ module_sources+ args = flags ++ ["-package-db=/nix/store/pl15d7r0mhfpygks2s8cal3jsjivwcl8-ghc-shell-for-openapi3-ghc-8.10.7-env/lib/ghc-8.10.7/package.conf.d"] ++ pkgs ++ module_sources