servant-openapi-hs 4.1.0 → 5.0.0
raw patch · 16 files changed
+614/−488 lines, 16 filesdep −insert-ordered-containersdep ~bytestringdep ~lensdep ~openapi-hsPVP ok
version bump matches the API change (PVP)
Dependencies removed: insert-ordered-containers
Dependency ranges changed: bytestring, lens, openapi-hs
API changes (from Hackage documentation)
Files
- CHANGELOG.md +16/−0
- README.md +7/−6
- app/GenOpenApi.hs +4/−105
- app/GenOpenApi/Spec.hs +112/−0
- servant-openapi-hs.cabal +76/−67
- src/Servant/OpenApi.hs +23/−22
- src/Servant/OpenApi/Internal.hs +224/−165
- src/Servant/OpenApi/Internal/Orphans.hs +5/−6
- src/Servant/OpenApi/Internal/Test.hs +77/−60
- src/Servant/OpenApi/Internal/TypeLevel.hs +9/−8
- src/Servant/OpenApi/Internal/TypeLevel/Every.hs +26/−24
- src/Servant/OpenApi/Internal/TypeLevel/TMap.hs +13/−13
- src/Servant/OpenApi/Test.hs +6/−5
- src/Servant/OpenApi/TypeLevel.hs +7/−7
- test/Servant/OpenApiSpec.hs +8/−0
- test/golden/openapi-hs-4.1-gen-openapi.json +1/−0
CHANGELOG.md view
@@ -1,3 +1,19 @@+5.0.0+-----++* Breaking: require `openapi-hs >=5.0 && <6`, resolved from its published+ Hackage release. `openapi-hs` 5 vendors the insertion-ordered map and set+ types it previously took from `insert-ordered-containers`, so this package no+ longer depends on `insert-ordered-containers` directly.+* Breaking: code that imports `Servant.OpenApi.Internal` may need to replace+ `Data.HashMap.Strict.InsOrd` with `Data.HashMap.Strict.InsOrd.Compat`. The+ stable `Servant.OpenApi` generation API and generated schemas are unchanged.+* Guard generated output with a byte-for-byte golden captured under+ `openapi-hs-4.1.0` (`test/golden/openapi-hs-4.1-gen-openapi.json`). The+ document `gen-openapi` produces under `openapi-hs-5.0.0` is identical to it,+ and still lints with 0 errors under [`vacuum`](https://quobix.com/vacuum/).+* Raise the `lens` lower bound to `>=5.3.3`.+ 4.1.0 -----
README.md view
@@ -1,5 +1,6 @@ # servant-openapi-hs +[](https://hackage.haskell.org/package/servant-openapi-hs) [](/LICENSE) Generate an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) specification for@@ -34,15 +35,15 @@ ## Installation -> **Pre-release.** The first Hackage release is still in preparation. Until it-> is published, depend on this repository directly (see-> [Building from source](#building-from-source) below); the instructions in this-> section describe the package once it is on Hackage.+`servant-openapi-hs` is available on+[Hackage](https://hackage.haskell.org/package/servant-openapi-hs). The `5.0`+series builds against `openapi-hs` 5; see the [changelog](CHANGELOG.md) for the+OpenAPI 3.1 port, `MultiVerb` support, and the `openapi-hs` 5 upgrade. Add `servant-openapi-hs` to your package's `build-depends`: ```cabal-build-depends: servant-openapi-hs+build-depends: servant-openapi-hs >= 5.0 && < 6 ``` Its OpenAPI 3.1 data model comes from@@ -58,7 +59,7 @@ Requires GHC **9.12.4** or **9.14.1**. <a id="building-from-source"></a>-> **Building from source.** Until the first Hackage release, depend on this+> **Building from source.** To test unreleased changes, depend on this > repository directly by adding a `source-repository-package` stanza for > `servant-openapi-hs` to your `cabal.project`: >
app/GenOpenApi.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} -- | Emit a representative API's OpenAPI 3.1 document as JSON to stdout.@@ -7,111 +6,11 @@ -- -- > cabal run gen-openapi > openapi.json -- > nix run nixpkgs#vacuum-go -- lint -d openapi.json------ The document is deliberately a complete OpenAPI 3.1 contract — it carries--- @info@ (title/version/description), a @server@, @tags@, and a unique--- @operationId@ per operation — so an external linter has a realistic document--- to validate rather than the bare skeleton @toOpenApi@ produces by default.------ On servant >= 0.20.3 the API also includes a 'MultiVerb' endpoint, so the--- external linter exercises MultiVerb rendering (multiple status codes, an--- empty-body response, and a response carrying headers) and catches any future--- regression that produces a structurally invalid OpenAPI 3.1 document. module Main (main) where -import Control.Lens-import Data.Aeson (ToJSON, encode)-import qualified Data.ByteString.Lazy.Char8 as BL-import Data.Char (isAlphaNum, toUpper)-import Data.OpenApi (ToSchema)-import qualified Data.OpenApi as O-import Data.Proxy (Proxy (..))-import qualified Data.Text as T-import Data.Text (Text)-import GHC.Generics (Generic)-import Servant.API-#if MIN_VERSION_servant(0,20,3)-import Servant.API.MultiVerb (MultiVerb, Respond, RespondEmpty, WithHeaders)-#endif-import Servant.OpenApi (toOpenApi)---- A small but representative Todo-style CRUD API: a response record with a--- nested optional field, a request body, a path capture, and a no-content--- delete.--data Todo = Todo- { todoId :: Int- , title :: Text- , completed :: Bool- , notes :: Maybe Text- } deriving (Generic)--instance ToJSON Todo-instance ToSchema Todo--data NewTodo = NewTodo- { newTitle :: Text- , newNotes :: Maybe Text- } deriving (Generic)--instance ToJSON NewTodo-instance ToSchema NewTodo--#if MIN_VERSION_servant(0,20,3)--- | Responses for the MultiVerb status endpoint: an empty-body 404, a plain--- 200 carrying the todo, and a 203 whose response also carries a header — so--- the linted document exercises MultiVerb's multi-status, empty-body, and--- header-carrying rendering paths.-type TodoStatusResponses =- '[ RespondEmpty 404 "No todo with that id"- , Respond 200 "The todo's current state" Todo- , WithHeaders '[Header "X-Revision" Int] Todo- (Respond 203 "Non-authoritative todo state" Todo)- ]-#endif--type TodoAPI =- "todos" :> Get '[JSON] [Todo]- :<|> "todos" :> ReqBody '[JSON] NewTodo :> Post '[JSON] Todo- :<|> "todos" :> Capture "id" Int :> Get '[JSON] Todo- :<|> "todos" :> Capture "id" Int :> ReqBody '[JSON] NewTodo :> Put '[JSON] Todo- :<|> "todos" :> Capture "id" Int :> Delete '[JSON] NoContent-#if MIN_VERSION_servant(0,20,3)- :<|> "todos" :> "status" :> Capture "id" Int- :> MultiVerb 'GET '[JSON] TodoStatusResponses ()-#endif---- | The generated bare document enriched into a complete OpenAPI 3.1 contract.-spec :: O.OpenApi-spec = toOpenApi (Proxy :: Proxy TodoAPI)- & O.info . O.title .~ "Todo API"- & O.info . O.version .~ "1.0.0"- & O.info . O.description ?~ "A small, representative Todo CRUD API."- & O.servers .~ ["https://api.example.com"]- & O.applyTags [O.Tag "todos" (Just "Operations on todo items") Nothing]- & withOperationIds---- | Assign a unique @operationId@ to every operation, derived from its HTTP--- method and path (e.g. @GET \/todos\/{id}@ → @getTodosId@). Operations whose--- method is absent on a path are left untouched.-withOperationIds :: O.OpenApi -> O.OpenApi-withOperationIds = O.paths %~ imap setForPath- where- setForPath path =- (O.get . _Just . O.operationId %~ orSet ("get" <> key))- . (O.post . _Just . O.operationId %~ orSet ("create" <> key))- . (O.put . _Just . O.operationId %~ orSet ("update" <> key))- . (O.delete . _Just . O.operationId %~ orSet ("delete" <> key))- where key = camel path- orSet v = Just . maybe v id---- | Turn a path like @"\/todos\/{id}"@ into @"TodosId"@.-camel :: FilePath -> Text-camel = T.pack . concatMap capitalize . words . map keepAlnum- where- keepAlnum c = if isAlphaNum c then c else ' '- capitalize [] = []- capitalize (c:cs) = toUpper c : cs+import Data.Aeson (encode)+import Data.ByteString.Lazy.Char8 qualified as BL+import GenOpenApi.Spec qualified as GenOpenApi main :: IO ()-main = BL.putStrLn (encode spec)+main = BL.putStrLn (encode GenOpenApi.spec)
+ app/GenOpenApi/Spec.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++-- | A representative API's complete OpenAPI 3.1 document.+--+-- The document carries @info@ (title/version/description), a @server@, @tags@,+-- and a unique @operationId@ per operation so both the executable and tests+-- exercise a realistic generated contract.+--+-- On servant >= 0.20.3 the API also includes a 'MultiVerb' endpoint, so the+-- contract covers multiple status codes, an empty-body response, and a+-- response carrying headers.+module GenOpenApi.Spec (spec) where++import Control.Lens+import Data.Aeson (ToJSON)+import Data.Char (isAlphaNum, toUpper)+import Data.OpenApi (ToSchema)+import Data.OpenApi qualified as O+import Data.Proxy (Proxy (..))+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics (Generic)+import Servant.API+#if MIN_VERSION_servant(0,20,3)+import Servant.API.MultiVerb (MultiVerb, Respond, RespondEmpty, WithHeaders)+#endif+import Servant.OpenApi (toOpenApi)++-- A small but representative Todo-style CRUD API: a response record with a+-- nested optional field, a request body, a path capture, and a no-content+-- delete.++data Todo = Todo+ { todoId :: Int,+ title :: Text,+ completed :: Bool,+ notes :: Maybe Text+ }+ deriving (Generic)++instance ToJSON Todo++instance ToSchema Todo++data NewTodo = NewTodo+ { newTitle :: Text,+ newNotes :: Maybe Text+ }+ deriving (Generic)++instance ToJSON NewTodo++instance ToSchema NewTodo++#if MIN_VERSION_servant(0,20,3)+-- | Responses for the MultiVerb status endpoint: an empty-body 404, a plain+-- 200 carrying the todo, and a 203 whose response also carries a header — so+-- the linted document exercises MultiVerb's multi-status, empty-body, and+-- header-carrying rendering paths.+type TodoStatusResponses =+ '[ RespondEmpty 404 "No todo with that id"+ , Respond 200 "The todo's current state" Todo+ , WithHeaders '[Header "X-Revision" Int] Todo+ (Respond 203 "Non-authoritative todo state" Todo)+ ]+#endif++type TodoAPI =+ "todos" :> Get '[JSON] [Todo]+ :<|> "todos" :> ReqBody '[JSON] NewTodo :> Post '[JSON] Todo+ :<|> "todos" :> Capture "id" Int :> Get '[JSON] Todo+ :<|> "todos" :> Capture "id" Int :> ReqBody '[JSON] NewTodo :> Put '[JSON] Todo+ :<|> "todos" :> Capture "id" Int :> Delete '[JSON] NoContent+#if MIN_VERSION_servant(0,20,3)+ :<|> "todos" :> "status" :> Capture "id" Int+ :> MultiVerb 'GET '[JSON] TodoStatusResponses ()+#endif++-- | The generated bare document enriched into a complete OpenAPI 3.1 contract.+spec :: O.OpenApi+spec =+ toOpenApi (Proxy :: Proxy TodoAPI)+ & O.info . O.title .~ "Todo API"+ & O.info . O.version .~ "1.0.0"+ & O.info . O.description ?~ "A small, representative Todo CRUD API."+ & O.servers .~ ["https://api.example.com"]+ & O.applyTags [O.Tag "todos" (Just "Operations on todo items") Nothing]+ & withOperationIds++-- | Assign a unique @operationId@ to every operation, derived from its HTTP+-- method and path (e.g. @GET \/todos\/{id}@ → @getTodosId@). Operations whose+-- method is absent on a path are left untouched.+withOperationIds :: O.OpenApi -> O.OpenApi+withOperationIds = O.paths %~ imap setForPath+ where+ setForPath path =+ (O.get . _Just . O.operationId %~ orSet ("get" <> key))+ . (O.post . _Just . O.operationId %~ orSet ("create" <> key))+ . (O.put . _Just . O.operationId %~ orSet ("update" <> key))+ . (O.delete . _Just . O.operationId %~ orSet ("delete" <> key))+ where+ key = camel path+ orSet v = Just . maybe v id++-- | Turn a path like @"\/todos\/{id}"@ into @"TodosId"@.+camel :: FilePath -> Text+camel = T.pack . concatMap capitalize . words . map keepAlnum+ where+ keepAlnum c = if isAlphaNum c then c else ' '+ capitalize [] = []+ capitalize (c : cs) = toUpper c : cs
servant-openapi-hs.cabal view
@@ -1,7 +1,7 @@-cabal-version: 3.0-name: servant-openapi-hs-version: 4.1.0-synopsis: Generate an OpenAPI 3.1 specification for your servant API.+cabal-version: 3.0+name: servant-openapi-hs+version: 5.0.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 for describing and documenting HTTP APIs in JSON or YAML. This library@@ -19,34 +19,31 @@ This package is a fork of [@servant-openapi3@](https://github.com/biocad/servant-openapi3) that targets OpenAPI 3.1 via [@openapi-hs@](https://github.com/shinzui/openapi-hs).-homepage: https://github.com/shinzui/servant-openapi-hs-bug-reports: https://github.com/shinzui/servant-openapi-hs/issues-license: BSD-3-Clause-license-file: LICENSE-author: David Johnson, Nickolay Kudasov, Maxim Koltsov-maintainer: nadeem@gmail.com-copyright: (c) 2015-2020, Servant contributors-category: Web, Servant, OpenApi-build-type: Simple-tested-with:- GHC ==9.12.4 || ==9.14.1 +homepage: https://github.com/shinzui/servant-openapi-hs+bug-reports: https://github.com/shinzui/servant-openapi-hs/issues+license: BSD-3-Clause+license-file: LICENSE+author: David Johnson, Nickolay Kudasov, Maxim Koltsov+maintainer: nadeem@gmail.com+copyright: (c) 2015-2020, Servant contributors+category: Web, Servant, OpenApi+build-type: Simple+tested-with: GHC ==9.12.4 || ==9.14.1 extra-doc-files:- README.md- , CHANGELOG.md+ CHANGELOG.md+ README.md +extra-source-files: test/golden/*.json+ source-repository head type: git location: https://github.com/shinzui/servant-openapi-hs.git library- ghc-options: -Wall+ ghc-options: -Wall exposed-modules: Servant.OpenApi- Servant.OpenApi.Test- Servant.OpenApi.TypeLevel-- -- Internal modules Servant.OpenApi.Internal Servant.OpenApi.Internal.Orphans Servant.OpenApi.Internal.Test@@ -54,60 +51,72 @@ Servant.OpenApi.Internal.TypeLevel.API Servant.OpenApi.Internal.TypeLevel.Every Servant.OpenApi.Internal.TypeLevel.TMap- hs-source-dirs: src- build-depends: aeson >=2.0.1.0 && <2.3- , aeson-pretty >=0.8.7 && <0.9- , base >=4.21 && <4.23- , base-compat >=0.10.5 && <0.15- , bytestring >=0.10.8.1 && <0.13- , http-media >=0.7.1.3 && <0.9- , insert-ordered-containers >=0.2.1.0 && <0.3- , lens >=4.17 && <5.4- , servant >=0.17 && <0.21- , singleton-bool >=0.1.4 && <0.2- , openapi-hs >=4.0 && <5- , text >=1.2.3.0 && <3- , unordered-containers >=0.2.9.0 && <0.3+ Servant.OpenApi.Test+ Servant.OpenApi.TypeLevel - , hspec >=2.6.0 && <2.12- , QuickCheck >=2.9 && <2.17- default-language: GHC2024+ -- Internal modules+ hs-source-dirs: src+ build-depends:+ , aeson >=2.0.1.0 && <2.3+ , aeson-pretty >=0.8.7 && <0.9+ , base >=4.21 && <4.23+ , base-compat >=0.10.5 && <0.15+ , bytestring >=0.10.8.1 && <0.13+ , 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+ , QuickCheck >=2.9 && <2.17+ , servant >=0.17 && <0.21+ , singleton-bool >=0.1.4 && <0.2+ , text >=1.2.3.0 && <3+ , unordered-containers >=0.2.9.0 && <0.3 + default-language: GHC2024+ executable gen-openapi ghc-options: -Wall hs-source-dirs: app main-is: GenOpenApi.hs- build-depends: base- , aeson- , bytestring- , lens- , openapi-hs- , servant- , servant-openapi-hs- , text+ other-modules: GenOpenApi.Spec+ build-depends:+ , aeson+ , base+ , bytestring+ , lens+ , openapi-hs >=5.0 && <6+ , servant+ , servant-openapi-hs+ , text+ default-language: GHC2024 test-suite spec- ghc-options: -Wall- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: Spec.hs+ ghc-options: -Wall+ type: exitcode-stdio-1.0+ hs-source-dirs: test app+ main-is: Spec.hs build-tool-depends: hspec-discover:hspec-discover >=2.6.0 && <2.12- build-depends: base- , base-compat- , aeson- , hspec >=2.6.0 && <2.12- , QuickCheck- , lens- , lens-aeson >=1.0.2 && <1.3- , servant- , servant-openapi-hs- , openapi-hs >= 4.0- , text- , template-haskell- , utf8-string >=1.0.1.1 && <1.1- , time- , vector+ build-depends:+ , aeson+ , base+ , base-compat+ , bytestring+ , hspec >=2.6.0 && <2.12+ , lens+ , lens-aeson >=1.0.2 && <1.3+ , openapi-hs >=5.0 && <6+ , QuickCheck+ , servant+ , servant-openapi-hs+ , template-haskell+ , text+ , time+ , utf8-string >=1.0.1.1 && <1.1+ , vector+ other-modules:+ GenOpenApi.Spec Servant.OpenApiSpec- default-language: GHC2024++ default-language: GHC2024
src/Servant/OpenApi.hs view
@@ -15,36 +15,37 @@ -- Additional utilities can also take advantage of the resulting files, such as testing tools. -- -- For more information see the <https://spec.openapis.org/oas/v3.1.0 OpenAPI 3.1 specification>.-module Servant.OpenApi (- -- * How to use this library- -- $howto+module Servant.OpenApi+ ( -- * How to use this library+ -- $howto - -- ** Generate @OpenApi@- -- $generate+ -- ** Generate @OpenApi@+ -- $generate - -- ** Annotate- -- $annotate+ -- ** Annotate+ -- $annotate - -- ** Test- -- $test+ -- ** Test+ -- $test - -- ** Serve- -- $serve+ -- ** Serve+ -- $serve - -- * @'HasOpenApi'@ class- HasOpenApi(..),+ -- * @'HasOpenApi'@ class+ HasOpenApi (..), - -- * Manipulation- subOperations,+ -- * Manipulation+ subOperations, - -- * Testing- validateEveryToJSON,- validateEveryToJSONWithPatternChecker,-) where+ -- * Testing+ validateEveryToJSON,+ validateEveryToJSONWithPatternChecker,+ )+where -import Servant.OpenApi.Internal-import Servant.OpenApi.Test-import Servant.OpenApi.Internal.Orphans ()+import Servant.OpenApi.Internal+import Servant.OpenApi.Internal.Orphans ()+import Servant.OpenApi.Test -- $setup -- >>> import Control.Applicative
src/Servant/OpenApi/Internal.hs view
@@ -1,56 +1,57 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} #if __GLASGOW_HASKELL__ >= 806 {-# LANGUAGE UndecidableInstances #-} #endif {-# OPTIONS_GHC -Wno-orphans #-}+ -- | Internal implementation of the 'HasOpenApi' class and the machinery that -- derives an OpenAPI 3.1 document from a servant API type. Not subject to the -- PVP; import "Servant.OpenApi" instead. module Servant.OpenApi.Internal where -import Prelude () import Prelude.Compat+import Prelude () #if MIN_VERSION_servant(0,18,1) import Control.Applicative ((<|>)) #endif-import Control.Lens-import Data.Aeson-import Data.Foldable (toList)-import Data.HashMap.Strict.InsOrd (InsOrdHashMap)-import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap-import Data.Kind (Type)-import Data.Maybe (listToMaybe)-import Data.OpenApi hiding (Header, contentType)-import qualified Data.OpenApi as OpenApi-import Data.OpenApi.Declare-import Data.Proxy-import Data.Singletons.Bool-import Data.Text (Text)-import qualified Data.Text as Text-import Data.Typeable (Typeable)-import GHC.TypeLits-import Network.HTTP.Media (MediaType)-import Servant.API-import Servant.API.ContentTypes (AllMime, allMime)-import Servant.API.Description (FoldDescription, reflectDescription)-import Servant.API.Modifiers (FoldRequired)+import Control.Lens+import Data.Aeson+import Data.Foldable (toList)+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+import Data.Proxy+import Data.Singletons.Bool+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Typeable (Typeable)+import GHC.TypeLits+import Network.HTTP.Media (MediaType)+import Servant.API+import Servant.API.ContentTypes (AllMime, allMime)+import Servant.API.Description (FoldDescription, reflectDescription)+import Servant.API.Modifiers (FoldRequired) #if MIN_VERSION_servant(0,20,3) import Servant.API.MultiVerb #endif -import Servant.OpenApi.Internal.TypeLevel.API+import Servant.OpenApi.Internal.TypeLevel.API -- | Generate a OpenApi specification for a servant API. --@@ -97,71 +98,98 @@ -- | All operations of sub API. -- This is similar to @'operationsOf'@ but ensures that operations -- indeed belong to the API at compile time.-subOperations :: (IsSubAPI sub api, HasOpenApi sub) =>- Proxy sub -- ^ Part of a servant API.- -> Proxy api -- ^ The whole servant API.- -> Traversal' OpenApi Operation+subOperations ::+ (IsSubAPI sub api, HasOpenApi sub) =>+ -- | Part of a servant API.+ Proxy sub ->+ -- | The whole servant API.+ Proxy api ->+ Traversal' OpenApi Operation subOperations sub _ = operationsOf (toOpenApi sub) -- | Make a singleton OpenApi spec (with only one endpoint). -- For endpoints with no content see 'mkEndpointNoContent'.-mkEndpoint :: forall a cs hs proxy method status.- (ToSchema a, AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status)- => FilePath -- ^ Endpoint path.- -> proxy (Verb method status cs (Headers hs a)) -- ^ Method, content-types, headers and response.- -> OpenApi-mkEndpoint path proxy- = mkEndpointWithSchemaRef (Just schemaRef) path proxy- & components.schemas .~ schemaDefs+mkEndpoint ::+ forall a cs hs proxy method status.+ (ToSchema a, AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status) =>+ -- | Endpoint path.+ FilePath ->+ -- | Method, content-types, headers and response.+ proxy (Verb method status cs (Headers hs a)) ->+ OpenApi+mkEndpoint path proxy =+ mkEndpointWithSchemaRef (Just schemaRef) path proxy+ & components . schemas .~ schemaDefs where (schemaDefs, schemaRef) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty -- | Make a singleton t'OpenApi' spec (with only one endpoint) and with no content schema.-mkEndpointNoContent :: forall nocontent cs hs proxy method status.- (AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status)- => FilePath -- ^ Endpoint path.- -> proxy (Verb method status cs (Headers hs nocontent)) -- ^ Method, content-types, headers and response.- -> OpenApi-mkEndpointNoContent path proxy- = mkEndpointWithSchemaRef Nothing path proxy+mkEndpointNoContent ::+ forall nocontent cs hs proxy method status.+ (AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status) =>+ -- | Endpoint path.+ FilePath ->+ -- | Method, content-types, headers and response.+ proxy (Verb method status cs (Headers hs nocontent)) ->+ OpenApi+mkEndpointNoContent path proxy =+ mkEndpointWithSchemaRef Nothing path proxy -- | Like @'mkEndpoint'@ but with explicit schema reference. -- Unlike @'mkEndpoint'@ this function does not register any schema components.-mkEndpointWithSchemaRef :: forall cs hs proxy method status a.- (AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status)- => Maybe (Referenced Schema)- -> FilePath- -> proxy (Verb method status cs (Headers hs a))- -> OpenApi-mkEndpointWithSchemaRef mref path _ = mempty- & paths.at path ?~- (mempty & method ?~ (mempty- & at code ?~ Inline (mempty- & content .~ InsOrdHashMap.fromList- [(t, mempty & schema .~ mref) | t <- responseContentTypes]- & headers .~ responseHeaders)))+mkEndpointWithSchemaRef ::+ forall cs hs proxy method status a.+ (AllAccept cs, AllToResponseHeader hs, OpenApiMethod method, KnownNat status) =>+ Maybe (Referenced Schema) ->+ FilePath ->+ proxy (Verb method status cs (Headers hs a)) ->+ OpenApi+mkEndpointWithSchemaRef mref path _ =+ mempty+ & paths . at path+ ?~ ( mempty+ & method+ ?~ ( mempty+ & at code+ ?~ Inline+ ( mempty+ & content+ .~ InsOrdHashMap.fromList+ [(t, mempty & schema .~ mref) | t <- responseContentTypes]+ & headers .~ responseHeaders+ )+ )+ ) where- method = openApiMethod (Proxy :: Proxy method)- code = fromIntegral (natVal (Proxy :: Proxy status))+ method = openApiMethod (Proxy :: Proxy method)+ code = fromIntegral (natVal (Proxy :: Proxy status)) responseContentTypes = allContentType (Proxy :: Proxy cs)- responseHeaders = Inline <$> toAllResponseHeaders (Proxy :: Proxy hs)+ responseHeaders = Inline <$> toAllResponseHeaders (Proxy :: Proxy hs) -mkEndpointNoContentVerb :: forall proxy method.- (OpenApiMethod method)- => FilePath -- ^ Endpoint path.- -> proxy (NoContentVerb method) -- ^ Method- -> OpenApi-mkEndpointNoContentVerb path _ = mempty- & paths.at path ?~- (mempty & method ?~ (mempty- & at code ?~ Inline mempty))+mkEndpointNoContentVerb ::+ forall proxy method.+ (OpenApiMethod method) =>+ -- | Endpoint path.+ FilePath ->+ -- | Method+ proxy (NoContentVerb method) ->+ OpenApi+mkEndpointNoContentVerb path _ =+ mempty+ & paths . at path+ ?~ ( mempty+ & method+ ?~ ( mempty+ & at code ?~ Inline mempty+ )+ ) where- method = openApiMethod (Proxy :: Proxy method)- code = 204 -- hardcoded in servant-server+ method = openApiMethod (Proxy :: Proxy method)+ code = 204 -- hardcoded in servant-server -- | Add parameter to every operation in the spec. addParam :: Param -> OpenApi -> OpenApi-addParam param = allOperations.parameters %~ (Inline param :)+addParam param = allOperations . parameters %~ (Inline param :) -- | Add RequestBody to every operations in the spec. addRequestBody :: RequestBody -> OpenApi -> OpenApi@@ -191,14 +219,20 @@ class OpenApiMethod method where openApiMethod :: proxy method -> Lens' PathItem (Maybe Operation) -instance OpenApiMethod 'GET where openApiMethod _ = get-instance OpenApiMethod 'PUT where openApiMethod _ = put-instance OpenApiMethod 'POST where openApiMethod _ = post-instance OpenApiMethod 'DELETE where openApiMethod _ = delete+instance OpenApiMethod 'GET where openApiMethod _ = get++instance OpenApiMethod 'PUT where openApiMethod _ = put++instance OpenApiMethod 'POST where openApiMethod _ = post++instance OpenApiMethod 'DELETE where openApiMethod _ = delete+ instance OpenApiMethod 'OPTIONS where openApiMethod _ = options-instance OpenApiMethod 'HEAD where openApiMethod _ = head_-instance OpenApiMethod 'PATCH where openApiMethod _ = patch +instance OpenApiMethod 'HEAD where openApiMethod _ = head_++instance OpenApiMethod 'PATCH where openApiMethod _ = patch+ #if MIN_VERSION_servant(0,18,1) instance HasOpenApi (UVerb method cs '[]) where toOpenApi _ = mempty@@ -262,8 +296,11 @@ instance (ToSchema a, Accept ct, KnownNat status, OpenApiMethod method) => HasOpenApi (Stream method status fr ct a) where toOpenApi _ = toOpenApi (Proxy :: Proxy (Verb method status '[ct] (Headers '[] a))) -instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs, AllToResponseHeader hs, KnownNat status, OpenApiMethod method)- => HasOpenApi (Verb method status cs (Headers hs a)) where+instance+ {-# OVERLAPPABLE #-}+ (ToSchema a, AllAccept cs, AllToResponseHeader hs, KnownNat status, OpenApiMethod method) =>+ HasOpenApi (Verb method status cs (Headers hs a))+ where toOpenApi = mkEndpoint "/" -- ATTENTION: do not remove this instance!@@ -273,12 +310,14 @@ instance (AllAccept cs, KnownNat status, OpenApiMethod method) => HasOpenApi (Verb method status cs NoContent) where toOpenApi _ = toOpenApi (Proxy :: Proxy (Verb method status cs (Headers '[] NoContent))) -instance (AllAccept cs, AllToResponseHeader hs, KnownNat status, OpenApiMethod method)- => HasOpenApi (Verb method status cs (Headers hs NoContent)) where+instance+ (AllAccept cs, AllToResponseHeader hs, KnownNat status, OpenApiMethod method) =>+ HasOpenApi (Verb method status cs (Headers hs NoContent))+ where toOpenApi = mkEndpointNoContent "/" instance (OpenApiMethod method) => HasOpenApi (NoContentVerb method) where- toOpenApi = mkEndpointNoContentVerb "/"+ toOpenApi = mkEndpointNoContentVerb "/" instance (HasOpenApi a, HasOpenApi b) => HasOpenApi (a :<|> b) where toOpenApi _ = toOpenApi (Proxy :: Proxy a) <> toOpenApi (Proxy :: Proxy b)@@ -315,123 +354,143 @@ piece = symbolVal (Proxy :: Proxy sym) instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub, KnownSymbol (FoldDescription mods)) => HasOpenApi (Capture' mods sym a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addParam param- & prependPath capture- & addDefaultResponse404 tname+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addParam param+ & prependPath capture+ & addDefaultResponse404 tname where pname = symbolVal (Proxy :: Proxy sym) tname = Text.pack pname- transDesc "" = Nothing+ transDesc "" = Nothing transDesc desc = Just (Text.pack desc) capture = "{" <> pname <> "}"- param = mempty- & name .~ tname- & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))- & required ?~ True- & in_ .~ ParamPath- & schema ?~ Inline (toParamSchema (Proxy :: Proxy a))+ param =+ mempty+ & name .~ tname+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))+ & required ?~ True+ & in_ .~ ParamPath+ & schema ?~ Inline (toParamSchema (Proxy :: Proxy a)) -- | OpenApi Spec doesn't have a notion of CaptureAll, this instance is the best effort. instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub) => HasOpenApi (CaptureAll sym a :> sub) where toOpenApi _ = toOpenApi (Proxy :: Proxy (Capture sym a :> sub)) instance (KnownSymbol desc, HasOpenApi api) => HasOpenApi (Description desc :> api) where- toOpenApi _ = toOpenApi (Proxy :: Proxy api)- & allOperations.description %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>)+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy api)+ & allOperations . description %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>) instance (KnownSymbol desc, HasOpenApi api) => HasOpenApi (Summary desc :> api) where- toOpenApi _ = toOpenApi (Proxy :: Proxy api)- & allOperations.summary %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>)+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy api)+ & allOperations . summary %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>) instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasOpenApi (QueryParam' mods sym a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addParam param- & addDefaultResponse400 tname+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addParam param+ & addDefaultResponse400 tname where tname = Text.pack (symbolVal (Proxy :: Proxy sym))- transDesc "" = Nothing+ transDesc "" = Nothing transDesc desc = Just (Text.pack desc)- param = mempty- & name .~ tname- & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))- & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))- & in_ .~ ParamQuery- & schema ?~ Inline sch+ param =+ mempty+ & name .~ tname+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))+ & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))+ & in_ .~ ParamQuery+ & schema ?~ Inline sch sch = toParamSchema (Proxy :: Proxy a) instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub) => HasOpenApi (QueryParams sym a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addParam param- & addDefaultResponse400 tname+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addParam param+ & addDefaultResponse400 tname where tname = Text.pack (symbolVal (Proxy :: Proxy sym))- param = mempty- & name .~ tname- & in_ .~ ParamQuery- & schema ?~ Inline pschema- pschema = mempty- & type_ ?~ OpenApiTypeSingle OpenApiArray- & items ?~ OpenApiItemsObject (Inline $ toParamSchema (Proxy :: Proxy a))+ param =+ mempty+ & name .~ tname+ & in_ .~ ParamQuery+ & schema ?~ Inline pschema+ pschema =+ mempty+ & type_ ?~ OpenApiTypeSingle OpenApiArray+ & items ?~ OpenApiItemsObject (Inline $ toParamSchema (Proxy :: Proxy a)) instance (KnownSymbol sym, HasOpenApi sub) => HasOpenApi (QueryFlag sym :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addParam param- & addDefaultResponse400 tname+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addParam param+ & addDefaultResponse400 tname where tname = Text.pack (symbolVal (Proxy :: Proxy sym))- param = mempty- & name .~ tname- & in_ .~ ParamQuery- & allowEmptyValue ?~ True- & schema ?~ (Inline $ (toParamSchema (Proxy :: Proxy Bool))- & default_ ?~ toJSON False)+ param =+ mempty+ & name .~ tname+ & in_ .~ ParamQuery+ & allowEmptyValue ?~ True+ & schema+ ?~ ( Inline $+ (toParamSchema (Proxy :: Proxy Bool))+ & default_ ?~ toJSON False+ ) -instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasOpenApi (Header' mods sym a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addParam param- & addDefaultResponse400 tname+instance (KnownSymbol sym, ToParamSchema a, HasOpenApi sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasOpenApi (Header' mods sym a :> sub) where+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addParam param+ & addDefaultResponse400 tname where tname = Text.pack (symbolVal (Proxy :: Proxy sym))- transDesc "" = Nothing+ transDesc "" = Nothing transDesc desc = Just (Text.pack desc)- param = mempty- & name .~ tname- & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))- & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))- & in_ .~ ParamHeader- & schema ?~ (Inline $ toParamSchema (Proxy :: Proxy a))+ param =+ mempty+ & name .~ tname+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))+ & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))+ & in_ .~ ParamHeader+ & schema ?~ (Inline $ toParamSchema (Proxy :: Proxy a)) instance (ToSchema a, AllAccept cs, HasOpenApi sub, KnownSymbol (FoldDescription mods)) => HasOpenApi (ReqBody' mods cs a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addRequestBody reqBody- & addDefaultResponse400 tname- & components.schemas %~ (<> schemaDefs)+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addRequestBody reqBody+ & addDefaultResponse400 tname+ & components . schemas %~ (<> schemaDefs) where tname = "body"- transDesc "" = Nothing+ transDesc "" = Nothing transDesc desc = Just (Text.pack desc) (schemaDefs, schemaRef) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty- reqBody = (mempty :: RequestBody)- & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))- & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ schemaRef) | t <- allContentType (Proxy :: Proxy cs)]+ reqBody =+ (mempty :: RequestBody)+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))+ & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ schemaRef) | t <- allContentType (Proxy :: Proxy cs)] -- | This instance is an approximation. -- -- @since 1.1.7 instance (ToSchema a, Accept ct, HasOpenApi sub, KnownSymbol (FoldDescription mods)) => HasOpenApi (StreamBody' mods fr ct a :> sub) where- toOpenApi _ = toOpenApi (Proxy :: Proxy sub)- & addRequestBody reqBody- & addDefaultResponse400 tname- & components.schemas %~ (<> schemaDefs)+ toOpenApi _ =+ toOpenApi (Proxy :: Proxy sub)+ & addRequestBody reqBody+ & addDefaultResponse400 tname+ & components . schemas %~ (<> schemaDefs) where tname = "body"- transDesc "" = Nothing+ transDesc "" = Nothing transDesc desc = Just (Text.pack desc) (schemaDefs, schemaRef) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty- reqBody = (mempty :: RequestBody)- & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))- & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ schemaRef) | t <- toList $ contentTypes (Proxy :: Proxy ct)]+ reqBody =+ (mempty :: RequestBody)+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))+ & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ schemaRef) | t <- toList $ contentTypes (Proxy :: Proxy ct)] #if MIN_VERSION_servant(0,18,2) instance (HasOpenApi sub) => HasOpenApi (Fragment a :> sub) where@@ -477,7 +536,7 @@ (headerName, headerBS) = toResponseHeader (Proxy :: Proxy h) hdrs = toAllResponseHeaders (Proxy :: Proxy hs) -instance AllToResponseHeader hs => AllToResponseHeader (HList hs) where+instance (AllToResponseHeader hs) => AllToResponseHeader (HList hs) where toAllResponseHeaders _ = toAllResponseHeaders (Proxy :: Proxy hs) #if MIN_VERSION_servant(0,20,3)
src/Servant/OpenApi/Internal/Orphans.hs view
@@ -1,19 +1,18 @@-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE FlexibleContexts #-}-+{-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-}+ -- | Orphan instances needed to derive OpenAPI documents for servant APIs. -- Not subject to the PVP; import "Servant.OpenApi" instead. module Servant.OpenApi.Internal.Orphans where import Data.OpenApi-import Data.Proxy (Proxy (..))-import Data.Typeable (Typeable)+import Data.Proxy (Proxy (..))+import Data.Typeable (Typeable) import Servant.Types.SourceT (SourceT) -- | Pretend that 'SourceT m a' is '[a]'. -- -- @since 1.1.7--- instance (Typeable (SourceT m a), ToSchema a) => ToSchema (SourceT m a) where- declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [a])+ declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [a])
src/Servant/OpenApi/Internal/Test.hs view
@@ -1,28 +1,28 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE TypeOperators #-}+ -- | Internal implementation of the response-conformance test helpers. -- Not subject to the PVP; import "Servant.OpenApi.Test" instead. module Servant.OpenApi.Internal.Test where -import Data.Aeson (ToJSON (..))-import qualified Data.Aeson.Encode.Pretty as P-import qualified Data.ByteString.Lazy as BSL-import Data.OpenApi (Pattern, ToSchema, toSchema)-import Data.OpenApi.Schema.Validation-import Data.Text (Text)-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Encoding as TL-import Data.Typeable-import Test.Hspec-import Test.Hspec.QuickCheck-import Test.QuickCheck (Arbitrary, Property, counterexample, property)--import Servant.API-import Servant.OpenApi.Internal.TypeLevel+import Data.Aeson (ToJSON (..))+import Data.Aeson.Encode.Pretty qualified as P+import Data.ByteString.Lazy qualified as BSL+import Data.OpenApi (Pattern, ToSchema, toSchema)+import Data.OpenApi.Schema.Validation+import Data.Text (Text)+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Encoding qualified as TL+import Data.Typeable+import Servant.API+import Servant.OpenApi.Internal.TypeLevel+import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck (Arbitrary, Property, counterexample, property) -- $setup -- >>> import Control.Applicative@@ -78,29 +78,38 @@ -- ...No instance for ...Arbitrary Contact... -- ... arising from a use of ‘validateEveryToJSON’ -- ...-validateEveryToJSON- :: forall proxy api .- TMap (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema])- (BodyTypes JSON api)- => proxy api -- ^ Servant API.- -> Spec-validateEveryToJSON _ = props- (Proxy :: Proxy [ToJSON, ToSchema])- (maybeCounterExample . prettyValidateWith validateToJSON)- (Proxy :: Proxy (BodyTypes JSON api))+validateEveryToJSON ::+ forall proxy api.+ ( TMap+ (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema])+ (BodyTypes JSON api)+ ) =>+ -- | Servant API.+ proxy api ->+ Spec+validateEveryToJSON _ =+ props+ (Proxy :: Proxy [ToJSON, ToSchema])+ (maybeCounterExample . prettyValidateWith validateToJSON)+ (Proxy :: Proxy (BodyTypes JSON api)) -- | Verify that every type used with @'JSON'@ content type in a servant API -- has compatible @'ToJSON'@ and @'ToSchema'@ instances using @'validateToJSONWithPatternChecker'@. -- -- For validation without patterns see @'validateEveryToJSON'@.-validateEveryToJSONWithPatternChecker :: forall proxy api. TMap (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) =>- (Pattern -> Text -> Bool) -- ^ @'Pattern'@ checker.- -> proxy api -- ^ Servant API.- -> Spec-validateEveryToJSONWithPatternChecker checker _ = props- (Proxy :: Proxy [ToJSON, ToSchema])- (maybeCounterExample . prettyValidateWith (validateToJSONWithPatternChecker checker))- (Proxy :: Proxy (BodyTypes JSON api))+validateEveryToJSONWithPatternChecker ::+ forall proxy api.+ (TMap (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api)) =>+ -- | @'Pattern'@ checker.+ (Pattern -> Text -> Bool) ->+ -- | Servant API.+ proxy api ->+ Spec+validateEveryToJSONWithPatternChecker checker _ =+ props+ (Proxy :: Proxy [ToJSON, ToSchema])+ (maybeCounterExample . prettyValidateWith (validateToJSONWithPatternChecker checker))+ (Proxy :: Proxy (BodyTypes JSON api)) -- * QuickCheck-related stuff @@ -125,11 +134,16 @@ -- ... -- Finished in ... seconds -- ...3 examples, 0 failures...-props :: forall p p'' cs xs. TMap (Every (Typeable ': Show ': Arbitrary ': cs)) xs =>- p cs -- ^ A list of constraints.- -> (forall x. EveryTF cs x => x -> Property) -- ^ Property predicate.- -> p'' xs -- ^ A list of types.- -> Spec+props ::+ forall p p'' cs xs.+ (TMap (Every (Typeable ': Show ': Arbitrary ': cs)) xs) =>+ -- | A list of constraints.+ p cs ->+ -- | Property predicate.+ (forall x. (EveryTF cs x) => x -> Property) ->+ -- | A list of types.+ p'' xs ->+ Spec props _ f px = sequence_ specs where specs :: [Spec]@@ -176,31 +190,34 @@ -- <BLANKLINE> -- -- FIXME: this belongs in "Data.OpenApi.Schema.Validation" (in @openapi-hs@).-prettyValidateWith- :: forall a. (ToJSON a, ToSchema a)- => (a -> [ValidationError]) -> a -> Maybe String+prettyValidateWith ::+ forall a.+ (ToJSON a, ToSchema a) =>+ (a -> [ValidationError]) -> a -> Maybe String prettyValidateWith f x = case f x of- [] -> Nothing- errors -> Just $ unlines- [ "Validation against the schema fails:"- , unlines (map (" * " ++) errors)- , "JSON value:"- , ppJSONString json- , ""- , "OpenApi Schema:"- , ppJSONString (toJSON schema)- ]+ [] -> Nothing+ errors ->+ Just $+ unlines+ [ "Validation against the schema fails:",+ unlines (map (" * " ++) errors),+ "JSON value:",+ ppJSONString json,+ "",+ "OpenApi Schema:",+ ppJSONString (toJSON schema)+ ] where ppJSONString = TL.unpack . TL.decodeUtf8 . encodePretty - json = toJSON x+ json = toJSON x schema = toSchema (Proxy :: Proxy a) -- | Provide a counterexample if there is any. maybeCounterExample :: Maybe String -> Property-maybeCounterExample Nothing = property True+maybeCounterExample Nothing = property True maybeCounterExample (Just s) = counterexample s (property False) -encodePretty :: ToJSON a => a -> BSL.ByteString-encodePretty = P.encodePretty' $ P.defConfig { P.confCompare = P.compare }+encodePretty :: (ToJSON a) => a -> BSL.ByteString+encodePretty = P.encodePretty' $ P.defConfig {P.confCompare = P.compare}
src/Servant/OpenApi/Internal/TypeLevel.hs view
@@ -1,11 +1,12 @@ -- | Re-exports of the type-level helpers used to enumerate and match servant -- API endpoints. Not subject to the PVP; import "Servant.OpenApi.TypeLevel".-module Servant.OpenApi.Internal.TypeLevel (- module Servant.OpenApi.Internal.TypeLevel.API,- module Servant.OpenApi.Internal.TypeLevel.Every,- module Servant.OpenApi.Internal.TypeLevel.TMap,-) where+module Servant.OpenApi.Internal.TypeLevel+ ( module Servant.OpenApi.Internal.TypeLevel.API,+ module Servant.OpenApi.Internal.TypeLevel.Every,+ module Servant.OpenApi.Internal.TypeLevel.TMap,+ )+where -import Servant.OpenApi.Internal.TypeLevel.API-import Servant.OpenApi.Internal.TypeLevel.Every-import Servant.OpenApi.Internal.TypeLevel.TMap+import Servant.OpenApi.Internal.TypeLevel.API+import Servant.OpenApi.Internal.TypeLevel.Every+import Servant.OpenApi.Internal.TypeLevel.TMap
src/Servant/OpenApi/Internal/TypeLevel/Every.hs view
@@ -1,18 +1,18 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-} #if __GLASGOW_HASKELL__ >= 800 {-# LANGUAGE UndecidableSuperClasses #-} #endif@@ -20,10 +20,9 @@ -- a class. Not subject to the PVP; import "Servant.OpenApi.TypeLevel" instead. module Servant.OpenApi.Internal.TypeLevel.Every where -import Data.Kind (Constraint, Type)-import Data.Proxy--import Servant.OpenApi.Internal.TypeLevel.TMap+import Data.Kind (Constraint, Type)+import Data.Proxy+import Servant.OpenApi.Internal.TypeLevel.TMap -- $setup -- >>> :set -XDataKinds@@ -50,16 +49,19 @@ -- | Apply multiple constraint constructors to a type as a class. -- -- This is different from @'EveryTF'@ in that it allows partial application.-class EveryTF cs x => Every (cs :: [Type -> Constraint]) (x :: Type) where+class (EveryTF cs x) => Every (cs :: [Type -> Constraint]) (x :: Type) -instance Every '[] x where-instance (c x, Every cs x) => Every (c ': cs) x where+instance Every '[] x +instance (c x, Every cs x) => Every (c ': cs) x+ -- | Like @'tmap'@, but uses @'Every'@ for multiple constraints. -- -- >>> let zero :: forall p a. (Show a, Num a) => p a -> String; zero _ = show (0 :: a) -- >>> tmapEvery (Proxy :: Proxy [Show, Num]) zero (Proxy :: Proxy [Int, Float]) :: [String] -- ["0","0.0"]-tmapEvery :: forall a cs p p'' xs. (TMap (Every cs) xs) =>- p cs -> (forall x p'. Every cs x => p' x -> a) -> p'' xs -> [a]+tmapEvery ::+ forall a cs p p'' xs.+ (TMap (Every cs) xs) =>+ p cs -> (forall x p'. (Every cs x) => p' x -> a) -> p'' xs -> [a] tmapEvery _ = tmap (Proxy :: Proxy (Every cs))
src/Servant/OpenApi/Internal/TypeLevel/TMap.hs view
@@ -1,19 +1,20 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+ -- | A type-level list traversal that applies a function under a constraint to -- every element. Not subject to the PVP; import "Servant.OpenApi.TypeLevel". module Servant.OpenApi.Internal.TypeLevel.TMap where -import Data.Proxy-import GHC.Exts (Constraint)+import Data.Proxy+import GHC.Exts (Constraint) -- $setup -- >>> :set -XDataKinds@@ -29,11 +30,10 @@ -- >>> tmap (Proxy :: Proxy KnownSymbol) symbolVal (Proxy :: Proxy ["hello", "world"]) -- ["hello","world"] class TMap (q :: k -> Constraint) (xs :: [k]) where- tmap :: p q -> (forall x p'. q x => p' x -> a) -> p'' xs -> [a]+ tmap :: p q -> (forall x p'. (q x) => p' x -> a) -> p'' xs -> [a] instance TMap q '[] where tmap _ _ _ = [] instance (q x, TMap q xs) => TMap q (x ': xs) where tmap q f _ = f (Proxy :: Proxy x) : tmap q f (Proxy :: Proxy xs)-
src/Servant/OpenApi/Test.hs view
@@ -5,9 +5,10 @@ -- Stability: experimental -- -- Automatic tests for servant API against OpenApi spec.-module Servant.OpenApi.Test (- validateEveryToJSON,- validateEveryToJSONWithPatternChecker,-) where+module Servant.OpenApi.Test+ ( validateEveryToJSON,+ validateEveryToJSONWithPatternChecker,+ )+where -import Servant.OpenApi.Internal.Test+import Servant.OpenApi.Internal.Test
src/Servant/OpenApi/TypeLevel.hs view
@@ -5,11 +5,11 @@ -- Stability: experimental -- -- Useful type families for servant APIs.-module Servant.OpenApi.TypeLevel (- IsSubAPI,- EndpointsList,- BodyTypes,-) where--import Servant.OpenApi.Internal.TypeLevel+module Servant.OpenApi.TypeLevel+ ( IsSubAPI,+ EndpointsList,+ BodyTypes,+ )+where +import Servant.OpenApi.Internal.TypeLevel
test/Servant/OpenApiSpec.hs view
@@ -15,6 +15,7 @@ import Data.Aeson.Lens (key, members, _String) import Data.Aeson.QQ.Simple import qualified Data.Aeson.Types as JSON+import qualified Data.ByteString.Lazy as BSL import Data.Char (toLower) import Data.Int (Int64) import Data.OpenApi@@ -22,6 +23,7 @@ import Data.Text (Text) import Data.Time import GHC.Generics+import qualified GenOpenApi.Spec as GenOpenApi import Servant.API #if MIN_VERSION_servant(0,20,3) import Servant.API.MultiVerb (MultiVerb, Respond, RespondAs, RespondEmpty, WithHeaders)@@ -39,6 +41,12 @@ spec :: Spec spec = do+ describe "openapi-hs upgrade regression" $+ it "preserves the openapi-hs 4.1 generated document byte-for-byte" $ do+ expected <- BSL.readFile "test/golden/openapi-hs-4.1-gen-openapi.json"+ let actual = encode GenOpenApi.spec <> BSL.singleton 10+ actual `shouldBe` expected+ describe "HasOpenApi" $ do it "Todo API" $ checkAPI (Proxy :: Proxy TodoAPI) todoAPI it "Hackage API (with tags)" $ checkOpenApi hackageOpenApiWithTags hackageAPI
+ test/golden/openapi-hs-4.1-gen-openapi.json view
@@ -0,0 +1,1 @@+{"info":{"description":"A small, representative Todo CRUD API.","title":"Todo API","version":"1.0.0"},"servers":[{"url":"https://api.example.com"}],"paths":{"/todos":{"get":{"operationId":"getTodos","responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"items":{"$ref":"#/components/schemas/Todo"},"type":"array"}}},"description":""}},"tags":["todos"]},"post":{"operationId":"createTodos","requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/NewTodo"}}}},"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Todo"}}},"description":""},"400":{"description":"Invalid `body`"}},"tags":["todos"]}},"/todos/{id}":{"delete":{"operationId":"deleteTodosId","parameters":[{"in":"path","name":"id","required":true,"schema":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}],"responses":{"200":{"content":{"application/json;charset=utf-8":{}},"description":""},"404":{"description":"`id` not found"}},"tags":["todos"]},"get":{"operationId":"getTodosId","parameters":[{"in":"path","name":"id","required":true,"schema":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}],"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Todo"}}},"description":""},"404":{"description":"`id` not found"}},"tags":["todos"]},"put":{"operationId":"updateTodosId","parameters":[{"in":"path","name":"id","required":true,"schema":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}],"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/NewTodo"}}}},"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Todo"}}},"description":""},"400":{"description":"Invalid `body`"},"404":{"description":"`id` not found"}},"tags":["todos"]}},"/todos/status/{id}":{"get":{"operationId":"getTodosStatusId","parameters":[{"in":"path","name":"id","required":true,"schema":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Todo"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Todo"}}},"description":"The todo's current state"},"203":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Todo"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Todo"}}},"description":"Non-authoritative todo state","headers":{"X-Revision":{"schema":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}}},"404":{"description":"`id` or No todo with that id"}},"tags":["todos"]}}},"components":{"schemas":{"Todo":{"properties":{"completed":{"type":"boolean"},"notes":{"type":"string"},"title":{"type":"string"},"todoId":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"required":["todoId","title","completed"],"type":"object"},"NewTodo":{"properties":{"newNotes":{"type":"string"},"newTitle":{"type":"string"}},"required":["newTitle"],"type":"object"}}},"tags":[{"description":"Operations on todo items","name":"todos"}],"openapi":"3.1.0"}