servant-openapi-hs 5.0.0 → 5.1.0
raw patch · 5 files changed
+90/−56 lines, 5 filesdep ~openapi-hsPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: openapi-hs
API changes (from Hackage documentation)
- Servant.OpenApi.Internal: instance (GHC.Internal.TypeLits.KnownSymbol desc, Data.OpenApi.Internal.Schema.ToSchema a) => Servant.OpenApi.Internal.IsSwaggerResponse (Servant.API.MultiVerb.Respond s desc a)
- Servant.OpenApi.Internal: instance (GHC.Internal.TypeLits.KnownSymbol desc, Data.OpenApi.Internal.Schema.ToSchema a, Servant.API.ContentTypes.Accept ct) => Servant.OpenApi.Internal.IsSwaggerResponse (Servant.API.MultiVerb.RespondAs ct s desc a)
- Servant.OpenApi.Internal: instance (Servant.OpenApi.Internal.AllToResponseHeader hs, Servant.OpenApi.Internal.IsSwaggerResponse r) => Servant.OpenApi.Internal.IsSwaggerResponse (Servant.API.MultiVerb.WithHeaders hs a r)
- Servant.OpenApi.Internal: instance (Servant.OpenApi.Internal.IsSwaggerResponse a, GHC.Internal.TypeNats.KnownNat (Servant.OpenApi.Internal.MultiVerbStatus a), Servant.OpenApi.Internal.IsSwaggerResponseList as) => Servant.OpenApi.Internal.IsSwaggerResponseList (a : as)
- Servant.OpenApi.Internal: instance (Servant.OpenApi.Internal.OpenApiMethod method, Servant.OpenApi.Internal.IsSwaggerResponseList as) => Servant.OpenApi.Internal.HasOpenApi (Servant.API.MultiVerb.MultiVerb method '() as r)
- Servant.OpenApi.Internal: instance (Servant.OpenApi.Internal.OpenApiMethod method, Servant.OpenApi.Internal.IsSwaggerResponseList as, Servant.API.ContentTypes.AllMime cs) => Servant.OpenApi.Internal.HasOpenApi (Servant.API.MultiVerb.MultiVerb method cs as r)
- Servant.OpenApi.Internal: instance GHC.Internal.TypeLits.KnownSymbol desc => Servant.OpenApi.Internal.IsSwaggerResponse (Servant.API.MultiVerb.RespondEmpty s desc)
- Servant.OpenApi.Internal: instance Servant.OpenApi.Internal.IsSwaggerResponseList '[]
+ Servant.OpenApi.Internal: instance (GHC.Internal.TypeLits.KnownSymbol desc, Data.OpenApi.Internal.Schema.ToSchema a) => Servant.OpenApi.Internal.IsSwaggerResponse '() (Servant.API.MultiVerb.Respond s desc a)
+ Servant.OpenApi.Internal: instance (GHC.Internal.TypeLits.KnownSymbol desc, Data.OpenApi.Internal.Schema.ToSchema a, Servant.API.ContentTypes.AllMime cs) => Servant.OpenApi.Internal.IsSwaggerResponse cs (Servant.API.MultiVerb.Respond s desc a)
+ Servant.OpenApi.Internal: instance forall k (cs :: k) a (as :: [*]). (Servant.OpenApi.Internal.IsSwaggerResponse cs a, GHC.Internal.TypeNats.KnownNat (Servant.OpenApi.Internal.MultiVerbStatus a), Servant.OpenApi.Internal.IsSwaggerResponseList cs as) => Servant.OpenApi.Internal.IsSwaggerResponseList cs (a : as)
+ Servant.OpenApi.Internal: instance forall k (cs :: k). Servant.OpenApi.Internal.IsSwaggerResponseList cs '[]
+ Servant.OpenApi.Internal: instance forall k (desc :: GHC.Types.Symbol) (cs :: k) (s :: GHC.Internal.TypeNats.Nat). GHC.Internal.TypeLits.KnownSymbol desc => Servant.OpenApi.Internal.IsSwaggerResponse cs (Servant.API.MultiVerb.RespondEmpty s desc)
+ Servant.OpenApi.Internal: instance forall k (desc :: GHC.Types.Symbol) a ct (cs :: k) (s :: GHC.Internal.TypeNats.Nat). (GHC.Internal.TypeLits.KnownSymbol desc, Data.OpenApi.Internal.Schema.ToSchema a, Servant.API.ContentTypes.Accept ct) => Servant.OpenApi.Internal.IsSwaggerResponse cs (Servant.API.MultiVerb.RespondAs ct s desc a)
+ Servant.OpenApi.Internal: instance forall k (hs :: [*]) (cs :: k) r a. (Servant.OpenApi.Internal.AllToResponseHeader hs, Servant.OpenApi.Internal.IsSwaggerResponse cs r) => Servant.OpenApi.Internal.IsSwaggerResponse cs (Servant.API.MultiVerb.WithHeaders hs a r)
+ Servant.OpenApi.Internal: instance forall k (method :: Network.HTTP.Types.Method.StdMethod) (cs :: k) (as :: [*]) r. (Servant.OpenApi.Internal.OpenApiMethod method, Servant.OpenApi.Internal.IsSwaggerResponseList cs as) => Servant.OpenApi.Internal.HasOpenApi (Servant.API.MultiVerb.MultiVerb method cs as r)
- Servant.OpenApi.Internal: class IsSwaggerResponse (a :: k)
+ Servant.OpenApi.Internal: class IsSwaggerResponse (cs :: k1) (a :: k)
- Servant.OpenApi.Internal: class IsSwaggerResponseList (as :: [Type])
+ Servant.OpenApi.Internal: class IsSwaggerResponseList (cs :: k) (as :: [Type])
- Servant.OpenApi.Internal: responseListSwagger :: IsSwaggerResponseList as => DeclareDefinition (InsOrdHashMap HttpStatusCode Response)
+ Servant.OpenApi.Internal: responseListSwagger :: IsSwaggerResponseList cs as => DeclareDefinition (InsOrdHashMap HttpStatusCode Response)
- Servant.OpenApi.Internal: responseSwagger :: IsSwaggerResponse a => DeclareDefinition Response
+ Servant.OpenApi.Internal: responseSwagger :: IsSwaggerResponse cs a => DeclareDefinition Response
Files
- CHANGELOG.md +19/−0
- README.md +17/−0
- servant-openapi-hs.cabal +4/−4
- src/Servant/OpenApi/Internal.hs +23/−51
- test/Servant/OpenApiSpec.hs +27/−1
CHANGELOG.md view
@@ -1,3 +1,22 @@+5.1.0+-----++* Breaking: `MultiVerb` now documents each response alternative under its own+ content types. `RespondAs ct` keeps `ct`, `Respond` inherits the endpoint's+ media-type list, and `RespondEmpty` stays bodyless. Previously every+ alternative was rendered against a JSON default and then had the endpoint's+ content-type list reapplied wholesale, which discarded a `RespondAs`+ content type. Alternatives sharing a status code now retain every media type+ they declared instead of collapsing to one.+* Breaking: `Servant.OpenApi.Internal`'s `IsSwaggerResponse` and+ `IsSwaggerResponseList` take the enclosing `MultiVerb` content-type slot as an+ additional poly-kinded parameter, and the separate `'()` and `[Type]`+ `HasOpenApi (MultiVerb …)` instances are replaced by a single instance.+* Breaking: narrow the `openapi-hs` bound to `>=5.0 && <5.1`. Under PVP the+ breaking axis is `A.B`, so the previous `<6` promised compatibility with an+ `openapi-hs` 5.1 that the policy explicitly permits to break this package.+* Document the package's PVP versioning policy in the README.+ 5.0.0 -----
README.md view
@@ -119,6 +119,23 @@ nix run nixpkgs#vacuum-go -- lint -d openapi.json ``` +## Versioning++This package follows the Haskell+[Package Versioning Policy (PVP)](https://pvp.haskell.org/). Versions have the+form `A.B.C`, optionally followed by further components, where **`A.B` together+is the major version**: it changes only when the public API breaks. A release+that only adds to the API bumps `C`, and one that does not touch the API bumps a+fourth component `D`. The PVP-recommended bound is therefore:++```cabal+build-depends: servant-openapi-hs >=5.0 && <5.1+```++Releases up to and including `5.0.0` numbered themselves like SemVer; `4.1.0` in+particular was additive-only and would be `4.0.1` under the policy above. From+`5.0.0` onward the policy holds.+ ## License `servant-openapi-hs` retains the original **BSD-3-Clause** license of the upstream
servant-openapi-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: servant-openapi-hs-version: 5.0.0+version: 5.1.0 synopsis: Generate an OpenAPI 3.1 specification for your servant API. description: [OpenAPI](https://spec.openapis.org/oas/v3.1.0) is a language-agnostic format@@ -65,7 +65,7 @@ , hspec >=2.6.0 && <2.12 , http-media >=0.7.1.3 && <0.9 , lens >=5.3.3 && <5.4- , openapi-hs >=5.0 && <6+ , openapi-hs >=5.0 && <5.1 , QuickCheck >=2.9 && <2.17 , servant >=0.17 && <0.21 , singleton-bool >=0.1.4 && <0.2@@ -84,7 +84,7 @@ , base , bytestring , lens- , openapi-hs >=5.0 && <6+ , openapi-hs >=5.0 && <5.1 , servant , servant-openapi-hs , text@@ -105,7 +105,7 @@ , hspec >=2.6.0 && <2.12 , lens , lens-aeson >=1.0.2 && <1.3- , openapi-hs >=5.0 && <6+ , openapi-hs >=5.0 && <5.1 , QuickCheck , servant , servant-openapi-hs
src/Servant/OpenApi/Internal.hs view
@@ -32,7 +32,6 @@ import Data.HashMap.Strict.InsOrd.Compat (InsOrdHashMap) import Data.HashMap.Strict.InsOrd.Compat qualified as InsOrdHashMap import Data.Kind (Type)-import Data.Maybe (listToMaybe) import Data.OpenApi hiding (Header, contentType) import Data.OpenApi qualified as OpenApi import Data.OpenApi.Declare@@ -575,17 +574,17 @@ -- | Produce the OpenAPI 'Response' object for one MultiVerb response -- alternative.-class IsSwaggerResponse a where+class IsSwaggerResponse (cs :: k) a where responseSwagger :: DeclareDefinition Response instance- (AllToResponseHeader hs, IsSwaggerResponse r) =>- IsSwaggerResponse (WithHeaders hs a r)+ (AllToResponseHeader hs, IsSwaggerResponse cs r) =>+ IsSwaggerResponse cs (WithHeaders hs a r) where responseSwagger = fmap (headers .~ fmap Inline (toAllResponseHeaders (Proxy @hs)))- (responseSwagger @r)+ (responseSwagger @_ @cs @r) -- | Build a 'Response' carrying a single schema, exposed under the given list -- of content types.@@ -606,23 +605,26 @@ cs = allMime (Proxy @cs) instance+ (KnownSymbol desc, ToSchema a, AllMime cs) =>+ IsSwaggerResponse (cs :: [Type]) (Respond s desc a)+ where+ responseSwagger = simpleResponseSwagger @a @cs @desc++instance (KnownSymbol desc, ToSchema a) =>- IsSwaggerResponse (Respond s desc a)+ IsSwaggerResponse '() (Respond s desc a) where- -- Default the schema's content type to JSON: openapi needs *some* media type- -- to hang the schema on, and the enclosing MultiVerb instance reapplies the- -- real content-type list afterwards. responseSwagger = simpleResponseSwagger @a @'[JSON] @desc instance (KnownSymbol desc, ToSchema a, Accept ct) =>- IsSwaggerResponse (RespondAs (ct :: Type) s desc a)+ IsSwaggerResponse cs (RespondAs (ct :: Type) s desc a) where responseSwagger = simpleResponseSwagger @a @'[ct] @desc instance (KnownSymbol desc) =>- IsSwaggerResponse (RespondEmpty s desc)+ IsSwaggerResponse cs (RespondEmpty s desc) where responseSwagger = pure $@@ -631,25 +633,25 @@ -- | Fold a MultiVerb's list of response alternatives into a -- status-code-keyed map of 'Response' objects, merging alternatives that -- share a status code.-class IsSwaggerResponseList (as :: [Type]) where+class IsSwaggerResponseList (cs :: k) (as :: [Type]) where responseListSwagger :: DeclareDefinition (InsOrdHashMap HttpStatusCode Response) -instance IsSwaggerResponseList '[] where+instance IsSwaggerResponseList cs '[] where responseListSwagger = pure mempty instance- ( IsSwaggerResponse a+ ( IsSwaggerResponse cs a , KnownNat (MultiVerbStatus a)- , IsSwaggerResponseList as+ , IsSwaggerResponseList cs as ) =>- IsSwaggerResponseList (a ': as)+ IsSwaggerResponseList cs (a ': as) where responseListSwagger = InsOrdHashMap.insertWith combineResponseSwagger (fromIntegral (natVal (Proxy @(MultiVerbStatus a))))- <$> responseSwagger @a- <*> responseListSwagger @as+ <$> responseSwagger @_ @cs @a+ <*> responseListSwagger @_ @cs @as -- | Merge two responses that share a status code: concatenate descriptions and -- union their content maps.@@ -682,11 +684,9 @@ <>~ (s2 ^. properties . ix "label" . _Inline . enum_ . _Just) | otherwise = s1 --- | MultiVerb whose request-mime-types slot is @'()@ (no content types given):--- the response schemas keep their JSON default. instance- (OpenApiMethod method, IsSwaggerResponseList as) =>- HasOpenApi (MultiVerb method '() as r)+ (OpenApiMethod method, IsSwaggerResponseList cs as) =>+ HasOpenApi (MultiVerb method cs as r) where toOpenApi _ = mempty@@ -696,34 +696,6 @@ (mempty & responses . responses .~ refResps)) where method = openApiMethod (Proxy @method)- (schemaDefs, resps) = runDeclare (responseListSwagger @as) mempty+ (schemaDefs, resps) = runDeclare (responseListSwagger @_ @cs @as) mempty refResps = Inline <$> resps---- | MultiVerb with an explicit content-type list @cs@: reapply @cs@ to every--- response's single schema.-instance- (OpenApiMethod method, IsSwaggerResponseList as, AllMime cs) =>- HasOpenApi (MultiVerb method (cs :: [Type]) as r)- where- toOpenApi _ =- mempty- & components . schemas <>~ schemaDefs- & paths . at "/" ?~- (mempty & method ?~- (mempty & responses . responses .~ refResps))- where- method = openApiMethod (Proxy @method)- cs = allMime (Proxy @cs)- (schemaDefs, resps) = runDeclare (responseListSwagger @as) mempty- -- Each alternative already holds a single (JSON-defaulted) schema; swap in- -- the MultiVerb's own content types, reusing that one schema for each.- addMime :: Response -> Response- addMime resp =- resp- & content %~- ( foldMap (\c -> InsOrdHashMap.fromList $ (,c) <$> cs)- . listToMaybe- . toList- )- refResps = Inline . addMime <$> resps #endif
test/Servant/OpenApiSpec.hs view
@@ -70,6 +70,26 @@ codes "201" `shouldSatisfy` has _Just codes "202" `shouldSatisfy` has _Just codes "204" `shouldSatisfy` has _Just+ it "MultiVerb documents each alternative under its own content type" $ do+ let doc = toJSON coverageOpenApi+ keys k = doc ^.. key "paths" . key "/cov" . key "get" . key "responses"+ . key k . key "content" . members . asIndex+ keys "200" `shouldMatchList` ["application/json", "application/json;charset=utf-8"]+ keys "201" `shouldMatchList` ["text/plain;charset=utf-8"]+ keys "202" `shouldMatchList` ["application/json", "application/json;charset=utf-8"]+ keys "204" `shouldMatchList` []+ (doc ^? key "paths" . key "/cov" . key "get" . key "responses"+ . key "202" . key "headers" . key "X-Trace")+ `shouldSatisfy` has _Just+ it "MultiVerb preserves every media type when alternatives share a status" $ do+ let doc = toJSON coverageOpenApi+ keys = doc ^.. key "paths" . key "/shared" . key "get" . key "responses"+ . key "200" . key "content" . members . asIndex+ keys `shouldMatchList`+ [ "application/json"+ , "application/json;charset=utf-8"+ , "text/plain;charset=utf-8"+ ] #endif -- Layer 1: every generated document round-trips through openapi-hs's@@ -731,8 +751,14 @@ , WithHeaders '[Servant.API.Header "X-Trace" Text] Int (Respond 202 "Accepted" Int) ] +type SharedContentResponses =+ '[ RespondAs PlainText 200 "Plain text form" String+ , RespondAs JSON 200 "JSON form" Int+ ]+ type CoverageAPI =- "cov" :> MultiVerb 'GET '[JSON] CoverageResponses ()+ ("cov" :> MultiVerb 'GET '[JSON] CoverageResponses ())+ :<|> ("shared" :> MultiVerb 'GET '[JSON] SharedContentResponses ()) coverageOpenApi :: OpenApi coverageOpenApi = toOpenApi (Proxy :: Proxy CoverageAPI)