diff --git a/.travis.yml b/.travis.yml index e45f72b72..1fd2d384b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,14 @@ # This Travis job script has been generated by a script via # -# haskell-ci '--config=cabal.haskell-ci' 'cabal.project' +# haskell-ci 'cabal.project' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.9.20200121 +# version: 0.10.1 # version: ~> 1.0 language: c @@ -13,9 +17,6 @@ dist: xenial git: # whether to recursively clone submodules submodules: false -branches: - only: - - master cache: directories: - $HOME/.cabal/packages @@ -32,20 +33,14 @@ before_cache: - rm -rfv $CABALHOME/packages/head.hackage jobs: include: - - compiler: ghc-8.8.1 - addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.8.1","cabal-install-3.0"]}} + - compiler: ghc-8.8.3 + addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.8.3","cabal-install-3.2"]}} os: linux - compiler: ghc-8.6.5 - addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.6.5","cabal-install-3.0"]}} + addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.6.5","cabal-install-3.2"]}} os: linux - compiler: ghc-8.4.4 - addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.4.4","cabal-install-3.0"]}} - os: linux - - compiler: ghc-8.2.2 - addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.2.2","cabal-install-3.0"]}} - os: linux - - compiler: ghc-8.0.2 - addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.0.2","cabal-install-3.0"]}} + addons: {"apt":{"sources":[{"sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main","key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286"}],"packages":["ghc-8.4.4","cabal-install-3.2"]}} os: linux before_install: - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//') @@ -95,8 +90,15 @@ install: - touch cabal.project - | echo "packages: ." >> cabal.project + - echo 'package servant-swagger' >> cabal.project + - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project" - | - echo "allow-newer: aeson-pretty-0.8.7:base-compat" >> cabal.project + echo "allow-newer: aeson-pretty-0.8.7:base-compat" >> cabal.project + echo "" >> cabal.project + echo "source-repository-package" >> cabal.project + echo " type: git" >> cabal.project + echo " location: https://github.com/biocad/swagger2/" >> cabal.project + echo " tag: ff879159c009627e1bc9414763b4834a2c9bfba7" >> cabal.project - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-swagger)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" - cat cabal.project || true - cat cabal.project.local || true @@ -104,8 +106,8 @@ install: - ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH} - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'" - rm cabal.project.freeze - - ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all - - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all + - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all + - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all script: - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX) # Packaging... @@ -121,8 +123,15 @@ script: - touch cabal.project - | echo "packages: ${PKGDIR_servant_swagger}" >> cabal.project + - echo 'package servant-swagger' >> cabal.project + - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project" - | - echo "allow-newer: aeson-pretty-0.8.7:base-compat" >> cabal.project + echo "allow-newer: aeson-pretty-0.8.7:base-compat" >> cabal.project + echo "" >> cabal.project + echo "source-repository-package" >> cabal.project + echo " type: git" >> cabal.project + echo " location: https://github.com/biocad/swagger2/" >> cabal.project + echo " tag: ff879159c009627e1bc9414763b4834a2c9bfba7" >> cabal.project - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(servant-swagger)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" - cat cabal.project || true - cat cabal.project.local || true @@ -141,16 +150,6 @@ script: # Building without installed constraints for packages in global-db... - rm -f cabal.project.local - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all - # Constraint sets - - rm -rf cabal.project.local - # Constraint set swagger2-2.3 - - if [ $HCNUMVER -lt 80800 ] ; then ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='swagger2 ==2.3.*' all ; fi - # Constraint set swagger2-2.4 - - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='swagger2 ==2.4.*' all - # Constraint set swagger2-2.5 - - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='swagger2 ==2.5.*' all - # Constraint set servant-0.17 - - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --constraint='servant == 0.17.*' all -# REGENDATA ("0.9.20200121",["--config=cabal.haskell-ci","cabal.project"]) +# REGENDATA ("0.10.1",["cabal.project"]) # EOF diff --git a/cabal.haskell-ci b/cabal.haskell-ci deleted file mode 100644 index 05a9061c7..000000000 --- a/cabal.haskell-ci +++ /dev/null @@ -1,14 +0,0 @@ -branches: master - -constraint-set swagger2-2.3 - ghc: <8.8 - constraints: swagger2 ==2.3.* - -constraint-set swagger2-2.4 - constraints: swagger2 ==2.4.* - -constraint-set swagger2-2.5 - constraints: swagger2 ==2.5.* - -constraint-set servant-0.17 - constraints: servant == 0.17.* diff --git a/cabal.project b/cabal.project index 2d61bba2b..dc4a9b80b 100644 --- a/cabal.project +++ b/cabal.project @@ -1,2 +1,7 @@ packages: . allow-newer: aeson-pretty-0.8.7:base-compat + +source-repository-package + type: git + location: https://github.com/biocad/swagger2/ + tag: 69788075a4a321505305ef79b57c183409c0c68f diff --git a/servant-swagger.cabal b/servant-swagger.cabal index 2e1873599..3e5636ec3 100644 --- a/servant-swagger.cabal +++ b/servant-swagger.cabal @@ -29,11 +29,9 @@ category: Web, Servant, Swagger build-type: Custom cabal-version: 1.18 tested-with: - GHC ==8.0.2 - || ==8.2.2 - || ==8.4.4 + GHC ==8.4.4 || ==8.6.5 - || ==8.8.1 + || ==8.8.3 extra-source-files: README.md @@ -54,7 +52,7 @@ source-repository head custom-setup setup-depends: - base >=4.9 && <4.14, + base >=4.9 && <4.15, Cabal >= 1.24 && <3.1, cabal-doctest >=1.0.6 && <1.1 @@ -76,7 +74,7 @@ library hs-source-dirs: src build-depends: aeson >=1.4.2.0 && <1.6 , aeson-pretty >=0.8.7 && <0.9 - , base >=4.9.1.0 && <4.14 + , base >=4.9.1.0 && <4.15 , base-compat >=0.10.5 && <0.12 , bytestring >=0.10.8.1 && <0.11 , http-media >=0.7.1.3 && <0.9 diff --git a/src/Servant/Swagger.hs b/src/Servant/Swagger.hs index 64c37d99c..242acaca3 100644 --- a/src/Servant/Swagger.hs +++ b/src/Servant/Swagger.hs @@ -99,7 +99,7 @@ import Servant.Swagger.Internal.Orphans () -- In order to generate @'Swagger'@ specification for a servant API, just use @'toSwagger'@: -- -- >>> BSL8.putStrLn $ encode $ toSwagger (Proxy :: Proxy UserAPI) --- {"swagger":"2.0","info":{"version":"","title":""},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}} +-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/":{"get":{"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":"array"}}},"description":""}}},"post":{"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"400":{"description":"Invalid `body`"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/UserId"}}},"description":""}}}},"/{user_id}":{"get":{"parameters":[{"required":true,"schema":{"type":"integer"},"in":"path","name":"user_id"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}},"description":""}}}}},"components":{"schemas":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}}} -- -- By default @'toSwagger'@ will generate specification for all API routes, parameters, headers, responses and data schemas. -- @@ -118,9 +118,9 @@ import Servant.Swagger.Internal.Orphans () -- & info.version .~ "1.0" -- & info.description ?~ "This is an API for the Users service" -- & info.license ?~ "MIT" --- & host ?~ "example.com" +-- & servers .~ ["https://example.com"] -- :} --- {"swagger":"2.0","info":{"version":"1.0","title":"User API","license":{"name":"MIT"},"description":"This is an API for the Users service"},"host":"example.com","paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}} +-- {"openapi":"3.0.0","info":{"version":"1.0","title":"User API","license":{"name":"MIT"},"description":"This is an API for the Users service"},"servers":[{"url":"https://example.com"}],"paths":{"/":{"get":{"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":"array"}}},"description":""}}},"post":{"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"400":{"description":"Invalid `body`"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/UserId"}}},"description":""}}}},"/{user_id}":{"get":{"parameters":[{"required":true,"schema":{"type":"integer"},"in":"path","name":"user_id"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}},"description":""}}}}},"components":{"schemas":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}}} -- -- It is also useful to annotate or modify certain endpoints. -- @'subOperations'@ provides a convenient way to zoom into a part of an API. @@ -138,7 +138,7 @@ import Servant.Swagger.Internal.Orphans () -- & applyTagsFor getOps ["get" & description ?~ "GET operations"] -- & applyTagsFor postOps ["post" & description ?~ "POST operations"] -- :} --- {"swagger":"2.0","info":{"version":"","title":""},"paths":{"/":{"get":{"tags":["get"],"produces":["application/json;charset=utf-8"],"responses":{"200":{"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"},"description":""}}},"post":{"tags":["post"],"consumes":["application/json;charset=utf-8"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/User"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/UserId"},"description":""}}}},"/{user_id}":{"get":{"tags":["get"],"produces":["application/json;charset=utf-8"],"parameters":[{"required":true,"in":"path","name":"user_id","type":"integer"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"schema":{"$ref":"#/definitions/User"},"description":""}}}}},"definitions":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}},"tags":[{"name":"get","description":"GET operations"},{"name":"post","description":"POST operations"}]} +-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/":{"get":{"tags":["get"],"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":"array"}}},"description":""}}},"post":{"tags":["post"],"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"400":{"description":"Invalid `body`"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/UserId"}}},"description":""}}}},"/{user_id}":{"get":{"tags":["get"],"parameters":[{"required":true,"schema":{"type":"integer"},"in":"path","name":"user_id"}],"responses":{"404":{"description":"`user_id` not found"},"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/User"}}},"description":""}}}}},"components":{"schemas":{"User":{"required":["name","age"],"properties":{"name":{"type":"string"},"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"type":"object"},"UserId":{"type":"integer"}}},"tags":[{"name":"get","description":"GET operations"},{"name":"post","description":"POST operations"}]} -- -- This applies @\"get\"@ tag to the @GET@ endpoints and @\"post\"@ tag to the @POST@ endpoint of the User API. diff --git a/src/Servant/Swagger/Internal.hs b/src/Servant/Swagger/Internal.hs index f273ac661..c5da6652a 100644 --- a/src/Servant/Swagger/Internal.hs +++ b/src/Servant/Swagger/Internal.hs @@ -23,7 +23,7 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Foldable (toList) import Data.Proxy import Data.Singletons.Bool -import Data.Swagger hiding (Header) +import Data.Swagger hiding (Header, contentType) import qualified Data.Swagger as Swagger import Data.Swagger.Declare import Data.Text (Text) @@ -96,7 +96,7 @@ mkEndpoint :: forall a cs hs proxy method status. -> Swagger mkEndpoint path proxy = mkEndpointWithSchemaRef (Just ref) path proxy - & definitions .~ defs + & components.schemas .~ defs where (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty @@ -120,15 +120,15 @@ mkEndpointWithSchemaRef :: forall cs hs proxy method status a. mkEndpointWithSchemaRef mref path _ = mempty & paths.at path ?~ (mempty & method ?~ (mempty - & produces ?~ MimeList responseContentTypes & at code ?~ Inline (mempty - & schema .~ mref + & content .~ InsOrdHashMap.fromList + [(t, mempty & schema .~ mref) | t <- responseContentTypes] & headers .~ responseHeaders))) where method = swaggerMethod (Proxy :: Proxy method) code = fromIntegral (natVal (Proxy :: Proxy status)) responseContentTypes = allContentType (Proxy :: Proxy cs) - responseHeaders = toAllResponseHeaders (Proxy :: Proxy hs) + responseHeaders = Inline <$> toAllResponseHeaders (Proxy :: Proxy hs) mkEndpointNoContentVerb :: forall proxy method. (SwaggerMethod method) @@ -147,9 +147,9 @@ mkEndpointNoContentVerb path _ = mempty addParam :: Param -> Swagger -> Swagger addParam param = allOperations.parameters %~ (Inline param :) --- | Add accepted content types to every operation in the spec. -addConsumes :: [MediaType] -> Swagger -> Swagger -addConsumes cs = allOperations.consumes %~ (<> Just (MimeList cs)) +-- | Add RequestBody to every operations in the spec. +addRequestBody :: RequestBody -> Swagger -> Swagger +addRequestBody rb = allOperations . requestBody ?~ Inline rb -- | Format given text as inline code in Markdown. markdownCode :: Text -> Text @@ -251,8 +251,8 @@ instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub, KnownSymbol (FoldDes & name .~ tname & description .~ transDesc (reflectDescription (Proxy :: Proxy mods)) & required ?~ True - & schema .~ ParamOther (mempty - & in_ .~ ParamPath + & in_ .~ ParamPath + & schema ?~ Inline (mempty & paramSchema .~ toParamSchema (Proxy :: Proxy a)) -- | Swagger Spec doesn't have a notion of CaptureAll, this instance is the best effort. @@ -279,9 +279,9 @@ instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired & name .~ tname & description .~ transDesc (reflectDescription (Proxy :: Proxy mods)) & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods)) - & schema .~ ParamOther sch - sch = mempty & in_ .~ ParamQuery + & schema ?~ Inline sch + sch = mempty & paramSchema .~ toParamSchema (Proxy :: Proxy a) instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (QueryParams sym a :> sub) where @@ -292,17 +292,13 @@ instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (Query tname = Text.pack (symbolVal (Proxy :: Proxy sym)) param = mempty & name .~ tname - & schema .~ ParamOther sch - sch = mempty & in_ .~ ParamQuery + & schema ?~ Inline sch + sch = mempty & paramSchema .~ pschema pschema = mempty -#if MIN_VERSION_swagger2(2,4,0) & type_ ?~ SwaggerArray -#else - & type_ .~ SwaggerArray -#endif - & items ?~ SwaggerItemsPrimitive (Just CollectionMulti) (toParamSchema (Proxy :: Proxy a)) + & items ?~ SwaggerItemsObject (Inline $ mempty & paramSchema .~ toParamSchema (Proxy :: Proxy a)) instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (QueryFlag sym :> sub) where toSwagger _ = toSwagger (Proxy :: Proxy sub) @@ -312,9 +308,9 @@ instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (QueryFlag sym :> sub) tname = Text.pack (symbolVal (Proxy :: Proxy sym)) param = mempty & name .~ tname - & schema .~ ParamOther (mempty - & in_ .~ ParamQuery - & allowEmptyValue ?~ True + & in_ .~ ParamQuery + & allowEmptyValue ?~ True + & schema ?~ (Inline $ mempty & paramSchema .~ (toParamSchema (Proxy :: Proxy Bool) & default_ ?~ toJSON False)) @@ -330,46 +326,40 @@ instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired & name .~ tname & description .~ transDesc (reflectDescription (Proxy :: Proxy mods)) & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods)) - & schema .~ ParamOther (mempty - & in_ .~ ParamHeader + & in_ .~ ParamHeader + & schema ?~ (Inline $ mempty & paramSchema .~ toParamSchema (Proxy :: Proxy a)) instance (ToSchema a, AllAccept cs, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (ReqBody' mods cs a :> sub) where toSwagger _ = toSwagger (Proxy :: Proxy sub) - & addParam param - & addConsumes (allContentType (Proxy :: Proxy cs)) + & addRequestBody reqBody & addDefaultResponse400 tname - & definitions %~ (<> defs) + & components.schemas %~ (<> defs) where tname = "body" transDesc "" = Nothing transDesc desc = Just (Text.pack desc) (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty - param = mempty - & name .~ tname + reqBody = (mempty :: RequestBody) & description .~ transDesc (reflectDescription (Proxy :: Proxy mods)) - & required ?~ True - & schema .~ ParamBody ref + & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ ref) | t <- allContentType (Proxy :: Proxy cs)] -- | This instance is an approximation. --- +-- -- @since 1.1.7 instance (ToSchema a, Accept ct, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (StreamBody' mods fr ct a :> sub) where toSwagger _ = toSwagger (Proxy :: Proxy sub) - & addParam param - & addConsumes (toList (contentTypes (Proxy :: Proxy ct))) + & addRequestBody reqBody & addDefaultResponse400 tname - & definitions %~ (<> defs) + & components.schemas %~ (<> defs) where tname = "body" transDesc "" = Nothing transDesc desc = Just (Text.pack desc) (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty - param = mempty - & name .~ tname + reqBody = (mempty :: RequestBody) & description .~ transDesc (reflectDescription (Proxy :: Proxy mods)) - & required ?~ True - & schema .~ ParamBody ref + & content .~ InsOrdHashMap.fromList [(t, mempty & schema ?~ ref) | t <- toList $ contentTypes (Proxy :: Proxy ct)] -- ======================================================================= -- Below are the definitions that should be in Servant.API.ContentTypes @@ -391,7 +381,7 @@ instance (KnownSymbol sym, ToParamSchema a) => ToResponseHeader (Header sym a) w toResponseHeader _ = (hname, Swagger.Header Nothing hschema) where hname = Text.pack (symbolVal (Proxy :: Proxy sym)) - hschema = toParamSchema (Proxy :: Proxy a) + hschema = Just $ Inline $ mempty & paramSchema .~ toParamSchema (Proxy :: Proxy a) class AllToResponseHeader hs where toAllResponseHeaders :: Proxy hs -> InsOrdHashMap HeaderName Swagger.Header diff --git a/test/Servant/SwaggerSpec.hs b/test/Servant/SwaggerSpec.hs index 1d03908fe..e470489f1 100644 --- a/test/Servant/SwaggerSpec.hs +++ b/test/Servant/SwaggerSpec.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} @@ -9,7 +8,7 @@ module Servant.SwaggerSpec where import Control.Lens -import Data.Aeson (ToJSON(toJSON), Value, genericToJSON) +import Data.Aeson (ToJSON(toJSON), Value, genericToJSON, encode) import Data.Aeson.QQ.Simple import qualified Data.Aeson.Types as JSON import Data.Char (toLower) @@ -24,16 +23,11 @@ import Servant.Swagger import Servant.Test.ComprehensiveAPI (comprehensiveAPI) import Test.Hspec hiding (example) -#if !MIN_VERSION_swagger2(2,4,0) -import Data.Aeson.Lens (key, _Array) -import qualified Data.Vector as V -#endif - -checkAPI :: HasSwagger api => Proxy api -> Value -> IO () +checkAPI :: HasCallStack => HasSwagger api => Proxy api -> Value -> IO () checkAPI proxy = checkSwagger (toSwagger proxy) -checkSwagger :: Swagger -> Value -> IO () -checkSwagger swag js = toJSON swag `shouldBe` js +checkSwagger :: HasCallStack => Swagger -> Value -> IO () +checkSwagger swag js = encode (toJSON swag) `shouldBe` (encode js) spec :: Spec spec = describe "HasSwagger" $ do @@ -68,60 +62,69 @@ type TodoAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo todoAPI :: Value todoAPI = [aesonQQ| { - "swagger":"2.0", - "info": - { - "title": "", - "version": "" - }, - "definitions": - { - "Todo": - { - "type": "object", - "required": [ "created", "title" ], - "properties": - { - "created": { "$ref": "#/definitions/UTCTime" }, - "title": { "type": "string" }, - "summary": { "type": "string" } - } - }, - "UTCTime": - { - "type": "string", - "format": "yyyy-mm-ddThh:MM:ssZ", - "example": "2016-07-22T00:00:00Z" - } - }, - "paths": - { - "/todo/{id}": - { - "get": - { - "responses": - { - "200": - { - "schema": { "$ref":"#/definitions/Todo" }, - "description": "" - }, - "404": { "description": "`id` not found" } - }, - "produces": [ "application/json;charset=utf-8" ], - "parameters": - [ - { - "required": true, - "in": "path", - "name": "id", - "type": "string" - } - ] - } + "openapi": "3.0.0", + "info": { + "version": "", + "title": "" + }, + "components": { + "schemas": { + "Todo": { + "required": [ + "created", + "title" + ], + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "created": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "type": "string" + } } + }, + "UTCTime": { + "example": "2016-07-22T00:00:00Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + } + }, + "paths": { + "/todo/{id}": { + "get": { + "responses": { + "404": { + "description": "`id` not found" + }, + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "description": "" + } + }, + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "in": "path", + "name": "id" + } + ] + } } + } } |] @@ -174,7 +177,7 @@ instance ToSchema Package hackageSwaggerWithTags :: Swagger hackageSwaggerWithTags = toSwagger (Proxy :: Proxy HackageAPI) - & host ?~ Host "hackage.haskell.org" Nothing + & servers .~ ["https://hackage.haskell.org"] & applyTagsFor usersOps ["users" & description ?~ "Operations about user"] & applyTagsFor packagesOps ["packages" & description ?~ "Query packages"] where @@ -183,170 +186,170 @@ hackageSwaggerWithTags = toSwagger (Proxy :: Proxy HackageAPI) packagesOps = subOperations (Proxy :: Proxy HackagePackagesAPI) (Proxy :: Proxy HackageAPI) hackageAPI :: Value -hackageAPI = modifyValue [aesonQQ| +hackageAPI = [aesonQQ| { - "swagger":"2.0", - "host":"hackage.haskell.org", - "info":{ - "version":"", - "title":"" - }, - "definitions":{ - "UserDetailed":{ - "required":[ - "username", - "userid", - "groups" - ], - "type":"object", - "properties":{ - "groups":{ - "items":{ - "type":"string" - }, - "type":"array" - }, - "username":{ - "type":"string" + "openapi": "3.0.0", + "servers": [ + { + "url": "https://hackage.haskell.org" + } + ], + "components": { + "schemas": { + "UserDetailed": { + "required": [ + "username", + "userid", + "groups" + ], + "type": "object", + "properties": { + "groups": { + "items": { + "type": "string" }, - "userid":{ - "maximum":9223372036854775807, - "minimum":-9223372036854775808, - "type":"integer", - "format":"int64" - } - } + "type": "array" + }, + "username": { + "type": "string" + }, + "userid": { + "maximum": 9223372036854775807, + "format": "int64", + "minimum": -9223372036854775808, + "type": "integer" + } + } }, - "Package":{ - "required":[ - "packageName" - ], - "type":"object", - "properties":{ - "packageName":{ - "type":"string" - } - } + "Package": { + "required": [ + "packageName" + ], + "type": "object", + "properties": { + "packageName": { + "type": "string" + } + } }, - "UserSummary":{ - "required":[ - "username", - "userid" - ], - "type":"object", - "properties":{ - "username":{ - "type":"string" - }, - "userid":{ - "maximum":9223372036854775807, - "minimum":-9223372036854775808, - "type":"integer", - "format":"int64" - } - }, - "example":{ - "username": "JohnDoe", - "userid": 123 - } + "UserSummary": { + "example": { + "username": "JohnDoe", + "userid": 123 + }, + "required": [ + "username", + "userid" + ], + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "userid": { + "maximum": 9223372036854775807, + "format": "int64", + "minimum": -9223372036854775808, + "type": "integer" + } + } } - }, - "paths":{ - "/users":{ - "get":{ - "responses":{ - "200":{ - "schema":{ - "items":{ - "$ref":"#/definitions/UserSummary" - }, - "type":"array" - }, - "description":"" - } - }, - "produces":[ - "application/json;charset=utf-8" - ], - "tags":[ - "users" - ] - } - }, - "/packages":{ - "get":{ - "responses":{ - "200":{ - "schema":{ - "items":{ - "$ref":"#/definitions/Package" - }, - "type":"array" + } + }, + "info": { + "version": "", + "title": "" + }, + "paths": { + "/users": { + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserSummary" }, - "description":"" - } + "type": "array" + } + } }, - "produces":[ - "application/json;charset=utf-8" - ], - "tags":[ - "packages" - ] - } - }, - "/user/{username}":{ - "get":{ - "responses":{ - "404":{ - "description":"`username` not found" - }, - "200":{ - "schema":{ - "$ref":"#/definitions/UserDetailed" + "description": "" + } + }, + "tags": [ + "users" + ] + } + }, + "/packages": { + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Package" }, - "description":"" - } + "type": "array" + } + } }, - "produces":[ - "application/json;charset=utf-8" - ], - "parameters":[ - { - "required":true, - "in":"path", - "name":"username", - "type":"string" - } - ], - "tags":[ - "users" - ] - } + "description": "" + } + }, + "tags": [ + "packages" + ] } - }, - "tags":[ - { - "name":"users", - "description":"Operations about user" - }, - { - "name":"packages", - "description":"Query packages" + }, + "/user/{username}": { + "get": { + "responses": { + "404": { + "description": "`username` not found" + }, + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserDetailed" + } + } + }, + "description": "" + } + }, + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "in": "path", + "name": "username" + } + ], + "tags": [ + "users" + ] } - ] + } + }, + "tags": [ + { + "name": "users", + "description": "Operations about user" + }, + { + "name": "packages", + "description": "Query packages" + } + ] } |] - where - modifyValue :: Value -> Value -#if MIN_VERSION_swagger2(2,4,0) - modifyValue = id -#else - -- swagger2-2.4 preserves order of tags - -- swagger2-2.3 used Set, so they are ordered - -- packages comes before users. - -- We simply reverse, not properly sort here for simplicity: 2 elements. - modifyValue = over (key "tags" . _Array) V.reverse -#endif -- ======================================================================= @@ -365,44 +368,53 @@ getPostSwagger = toSwagger (Proxy :: Proxy GetPostAPI) getPostAPI :: Value getPostAPI = [aesonQQ| { - "swagger":"2.0", - "info":{ - "version":"", - "title":"" - }, - "paths":{ - "/":{ - "post":{ - "responses":{ - "200":{ - "schema":{ - "type":"string" - }, - "description":"" - } + "components": {}, + "openapi": "3.0.0", + "info": { + "version": "", + "title": "" + }, + "paths": { + "/": { + "post": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "type": "string" + } + } }, - "produces":[ "application/json;charset=utf-8" ] - }, - "get":{ - "responses":{ - "200":{ - "schema":{ - "type":"string" - }, - "description":"" - } + "description": "" + } + } + }, + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "type": "string" + } + } }, - "produces":[ "application/json;charset=utf-8" ], - "tags":[ "get" ] - } - } - }, - "tags":[ - { - "name":"get", - "description":"GET operations" + "description": "" + } + }, + "tags": [ + "get" + ] } - ] + } + }, + "tags": [ + { + "name": "get", + "description": "GET operations" + } + ] } |]