git.iko.soy/typeable/openapi3
on master help sign in
18 files changed+503−175
since splitsnapshots
unifiedsplit
.github/workflows/haskell-ci.yml+96−30 CHANGELOG.md+26−1 cabal.haskell-ci+11 openapi3.cabal+17−14 src/Data/OpenApi/Aeson/Compat.hs+76 src/Data/OpenApi/Internal.hs+18−15 src/Data/OpenApi/Internal/AesonUtils.hs+11−10 src/Data/OpenApi/Internal/ParamSchema.hs+2−2 src/Data/OpenApi/Internal/Schema.hs+55−76 src/Data/OpenApi/Internal/Schema/Validation.hs+23−11 src/Data/OpenApi/Schema.hs−2 src/Data/OpenApi/Schema/Generator.hs+3−1 stack.yaml+1−1 test/Data/OpenApi/CommonTestTypes.hs+123 test/Data/OpenApi/Schema/ValidationSpec.hs+20−3 test/Data/OpenApi/SchemaSpec.hs+4 test/Data/OpenApiSpec.hs+17−1 test/SpecCommon.hs−8
.github/workflows/haskell-ci.yml+96−30
@@ -8,67 +8,117 @@
88#
99# For more information, see https://github.com/haskell-CI/haskell-ci
1010#
11−# version: 0.12
11+# version: 0.14.3
1212#
13−# REGENDATA ("0.12",["github","cabal.project"])
13+# REGENDATA ("0.14.3",["github","cabal.project"])
1414#
1515name: Haskell-CI
1616on:
17− - push
18− - pull_request
17+ push:
18+ branches:
19+ - master
20+ pull_request:
21+ branches:
22+ - master
1923jobs:
2024 linux:
2125 name: Haskell-CI - Linux - ${{ matrix.compiler }}
2226 runs-on: ubuntu-18.04
27+ timeout-minutes:
28+ 60
2329 container:
24− image: buildpack-deps:bionic
30+ image: buildpack-deps:focal
2531 continue-on-error: ${{ matrix.allow-failure }}
2632 strategy:
2733 matrix:
2834 include:
29− - compiler: ghc-9.0.1
35+ - compiler: ghc-9.2.2
36+ compilerKind: ghc
37+ compilerVersion: 9.2.2
38+ setup-method: ghcup
3039 allow-failure: true
31− - compiler: ghc-8.10.4
40+ - compiler: ghc-9.0.2
41+ compilerKind: ghc
42+ compilerVersion: 9.0.2
43+ setup-method: ghcup
44+ allow-failure: false
45+ - compiler: ghc-8.10.7
46+ compilerKind: ghc
47+ compilerVersion: 8.10.7
48+ setup-method: ghcup
3249 allow-failure: false
3350 - compiler: ghc-8.8.4
51+ compilerKind: ghc
52+ compilerVersion: 8.8.4
53+ setup-method: hvr-ppa
3454 allow-failure: false
3555 - compiler: ghc-8.6.5
56+ compilerKind: ghc
57+ compilerVersion: 8.6.5
58+ setup-method: hvr-ppa
3659 allow-failure: false
3760 - compiler: ghc-8.4.4
61+ compilerKind: ghc
62+ compilerVersion: 8.4.4
63+ setup-method: hvr-ppa
3864 allow-failure: false
3965 fail-fast: false
4066 steps:
4167 - name: apt
4268 run: |
4369 apt-get update
44− apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common
45− apt-add-repository -y 'ppa:hvr/ghc'
46− apt-get update
47− apt-get install -y $CC cabal-install-3.4
70+ apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
71+ if [ "${{ matrix.setup-method }}" = ghcup ]; then
72+ mkdir -p "$HOME/.ghcup/bin"
73+ curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup"
74+ chmod a+x "$HOME/.ghcup/bin/ghcup"
75+ "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER"
76+ "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0
77+ else
78+ apt-add-repository -y 'ppa:hvr/ghc'
79+ apt-get update
80+ apt-get install -y "$HCNAME"
81+ mkdir -p "$HOME/.ghcup/bin"
82+ curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup"
83+ chmod a+x "$HOME/.ghcup/bin/ghcup"
84+ "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0
85+ fi
4886 env:
49− CC: ${{ matrix.compiler }}
87+ HCKIND: ${{ matrix.compilerKind }}
88+ HCNAME: ${{ matrix.compiler }}
89+ HCVER: ${{ matrix.compilerVersion }}
5090 - name: Set PATH and environment variables
5191 run: |
5292 echo "$HOME/.cabal/bin" >> $GITHUB_PATH
53− echo "LANG=C.UTF-8" >> $GITHUB_ENV
54− echo "CABAL_DIR=$HOME/.cabal" >> $GITHUB_ENV
55− echo "CABAL_CONFIG=$HOME/.cabal/config" >> $GITHUB_ENV
56− HCDIR=$(echo "/opt/$CC" | sed 's/-/\//')
57− HCNAME=ghc
58− HC=$HCDIR/bin/$HCNAME
59− echo "HC=$HC" >> $GITHUB_ENV
60− echo "HCPKG=$HCDIR/bin/$HCNAME-pkg" >> $GITHUB_ENV
61− echo "HADDOCK=$HCDIR/bin/haddock" >> $GITHUB_ENV
62− echo "CABAL=/opt/cabal/3.4/bin/cabal -vnormal+nowrap" >> $GITHUB_ENV
93+ echo "LANG=C.UTF-8" >> "$GITHUB_ENV"
94+ echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV"
95+ echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV"
96+ HCDIR=/opt/$HCKIND/$HCVER
97+ if [ "${{ matrix.setup-method }}" = ghcup ]; then
98+ HC=$HOME/.ghcup/bin/$HCKIND-$HCVER
99+ echo "HC=$HC" >> "$GITHUB_ENV"
100+ echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV"
101+ echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV"
102+ echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
103+ else
104+ HC=$HCDIR/bin/$HCKIND
105+ echo "HC=$HC" >> "$GITHUB_ENV"
106+ echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV"
107+ echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV"
108+ echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
109+ fi
110+
63111 HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
64− echo "HCNUMVER=$HCNUMVER" >> $GITHUB_ENV
65− echo "ARG_TESTS=--enable-tests" >> $GITHUB_ENV
66− echo "ARG_BENCH=--enable-benchmarks" >> $GITHUB_ENV
67− echo "HEADHACKAGE=false" >> $GITHUB_ENV
68− echo "ARG_COMPILER=--$HCNAME --with-compiler=$HC" >> $GITHUB_ENV
69− echo "GHCJSARITH=0" >> $GITHUB_ENV
112+ echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
113+ echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
114+ echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV"
115+ echo "HEADHACKAGE=false" >> "$GITHUB_ENV"
116+ echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV"
117+ echo "GHCJSARITH=0" >> "$GITHUB_ENV"
70118 env:
71− CC: ${{ matrix.compiler }}
119+ HCKIND: ${{ matrix.compilerKind }}
120+ HCNAME: ${{ matrix.compiler }}
121+ HCVER: ${{ matrix.compilerVersion }}
72122 - name: env
73123 run: |
74124 env
@@ -91,6 +141,10 @@ jobs:
91141 repository hackage.haskell.org
92142 url: http://hackage.haskell.org/
93143 EOF
144+ cat >> $CABAL_CONFIG <<EOF
145+ program-default-options
146+ ghc-options: $GHCJOBS +RTS -M3G -RTS
147+ EOF
94148 cat $CABAL_CONFIG
95149 - name: versions
96150 run: |
@@ -129,7 +183,8 @@ jobs:
129183 - name: generate cabal.project
130184 run: |
131185 PKGDIR_openapi3="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/openapi3-[0-9.]*')"
132− echo "PKGDIR_openapi3=${PKGDIR_openapi3}" >> $GITHUB_ENV
186+ echo "PKGDIR_openapi3=${PKGDIR_openapi3}" >> "$GITHUB_ENV"
187+ rm -f cabal.project cabal.project.local
133188 touch cabal.project
134189 touch cabal.project.local
135190 echo "packages: ${PKGDIR_openapi3}" >> cabal.project
@@ -175,3 +230,14 @@ jobs:
175230 run: |
176231 rm -f cabal.project.local
177232 $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
233+ - name: prepare for constraint sets
234+ run: |
235+ rm -f cabal.project.local
236+ - name: constraint set aeson-2
237+ run: |
238+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='aeson >=2.0' --dependencies-only -j2 all
239+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='aeson >=2.0' all
240+ - name: constraint set aeson-1
241+ run: |
242+ if [ $((HCNUMVER < 90200)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='aeson <2.0' --dependencies-only -j2 all ; fi
243+ if [ $((HCNUMVER < 90200)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='aeson <2.0' all ; fi
CHANGELOG.md+26−1
@@ -1,7 +1,32 @@
11Unreleased
22----------
33
4−- Allow `hashable-1.3.1`, prettify doctests.
4+3.2.2
5+-----
6+
7+- Fix bug with infinite recursion in GToSchema [#37](https://github.com/biocad/openapi3/pull/37).
8+
9+3.2.1
10+-----
11+
12+- Support aeson-2.0.3.
13+- Support GHC-9.2 (doctests still fail).
14+
15+3.2.0
16+-----
17+
18+- Support aeson-2 [#34](https://github.com/biocad/openapi3/pull/34).
19+- Use `SecurityDefinitions` type for `_componentsSecuritySchemes`
20+ [#32](https://github.com/biocad/openapi3/pull/32).
21+
22+3.1.0
23+-----
24+
25+- Use `format: date-time` for `ToParamSchema ZonedTime` instance (see
26+ [#20](https://github.com/biocad/openapi3/pull/20)).
27+- Support generating schema for any polymorphic types via `Typeable` (see
28+ [#19](https://github.com/biocad/openapi3/pull/19)).
29+- Allow `hashable-1.3.1`, prettify doctests (see [#18](https://github.com/biocad/openapi3/pull/18)).
530
6313.0.2.0
732-------
cabal.haskell-cinew file+11
@@ -0,0 +1,11 @@
1+branches: master
2+
3+distribution: focal
4+allow-failures: >=9.2
5+
6+constraint-set aeson-1
7+ constraints: aeson <2.0
8+ ghc: <9.2
9+
10+constraint-set aeson-2
11+ constraints: aeson >=2.0
openapi3.cabal+17−14
@@ -1,6 +1,6 @@
11cabal-version: >=1.10
22name: openapi3
3−version: 3.0.2.0
3+version: 3.2.2
44
55synopsis: OpenAPI 3.0 data model
66category: Web, Swagger, OpenApi
@@ -26,8 +26,9 @@ tested-with:
2626 GHC ==8.4.4
2727 || ==8.6.5
2828 || ==8.8.4
29− || ==8.10.4
30− || ==9.0.1
29+ || ==8.10.7
30+ || ==9.0.2
31+ || ==9.2.2
3132
3233custom-setup
3334 setup-depends:
@@ -56,31 +57,33 @@ library
5657 Data.OpenApi.Internal.AesonUtils
5758 Data.OpenApi.Internal.TypeShape
5859
60+ Data.OpenApi.Aeson.Compat
61+
5962 -- GHC boot libraries
6063 build-depends:
61− base >=4.11.1.0 && <4.16
62− , bytestring >=0.10.8.2 && <0.11
64+ base >=4.11.1.0 && <4.17
65+ , bytestring >=0.10.8.2 && <0.12
6366 , containers >=0.5.11.0 && <0.7
64− , template-haskell >=2.13.0.0 && <2.18
65− , time >=1.8.0.2 && <1.10
67+ , template-haskell >=2.13.0.0 && <2.19
68+ , time >=1.8.0.2 && <1.14
6669 , transformers >=0.5.5.0 && <0.6
6770
6871 build-depends:
6972 mtl >=2.2.2 && <2.3
70− , text >=1.2.3.1 && <1.3
73+ , text >=1.2.3.1 && <2.1
7174
7275 -- other dependencies
7376 build-depends:
74− base-compat-batteries >=0.11.1 && <0.12
75− , aeson >=1.4.2.0 && <1.6
77+ base-compat-batteries >=0.11.1 && <0.13
78+ , aeson >=1.4.2.0 && <1.6 || >=2.0.1.0 && < 2.1
7679 , aeson-pretty >=0.8.7 && <0.9
7780 -- cookie 0.4.3 is needed by GHC 7.8 due to time>=1.4 constraint
7881 , cookie >=0.4.3 && <0.5
7982 , generics-sop >=0.5.1.0 && <0.6
80− , hashable >=1.2.7.0 && <1.4
83+ , hashable >=1.2.7.0 && <1.5
8184 , http-media >=0.8.0.0 && <0.9
8285 , insert-ordered-containers >=0.2.3 && <0.3
83− , lens >=4.16.1 && <5.1
86+ , lens >=4.16.1 && <5.2
8487 , network >=2.6.3.5 && <3.2
8588 , optics-core >=0.2 && <0.5
8689 , optics-th >=0.2 && <0.5
@@ -119,14 +122,14 @@ test-suite spec
119122
120123 -- test-suite only dependencies
121124 build-depends:
122− hspec >=2.5.5 && <2.8
125+ hspec >=2.5.5 && <2.10
123126 , HUnit >=1.6.0.0 && <1.7
124127 , quickcheck-instances >=0.3.19 && <0.14
125128 , utf8-string >=1.0.1.1 && <1.1
126129
127130 -- https://github.com/haskell/cabal/issues/3708
128131 build-tool-depends:
129− hspec-discover:hspec-discover >=2.5.5 && <2.8
132+ hspec-discover:hspec-discover >=2.5.5 && <2.10
130133
131134 other-modules:
132135 SpecCommon
src/Data/OpenApi/Aeson/Compat.hsnew file+76
@@ -0,0 +1,76 @@
1+{-# LANGUAGE CPP #-}
2+
3+module Data.OpenApi.Aeson.Compat where
4+
5+#if MIN_VERSION_aeson(2,0,0)
6+import Data.Aeson (Key)
7+import qualified Data.Aeson.Key as Key
8+import qualified Data.Aeson.KeyMap as KeyMap
9+#else
10+import qualified Data.HashMap.Strict as HM
11+#endif
12+import Data.Bifunctor (first)
13+import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
14+import qualified Data.Text as T
15+
16+#if MIN_VERSION_aeson(2,0,0)
17+deleteKey :: Key -> KeyMap.KeyMap v -> KeyMap.KeyMap v
18+deleteKey = KeyMap.delete
19+
20+objectToList :: KeyMap.KeyMap v -> [(Key, v)]
21+objectToList = KeyMap.toList
22+
23+objectKeys :: KeyMap.KeyMap v -> [T.Text]
24+objectKeys = map Key.toText . KeyMap.keys
25+
26+stringToKey :: String -> Key
27+stringToKey = Key.fromString
28+
29+keyToString :: Key -> String
30+keyToString = Key.toString
31+
32+keyToText :: Key -> T.Text
33+keyToText = Key.toText
34+
35+toInsOrdHashMap :: KeyMap.KeyMap v -> InsOrdHashMap.InsOrdHashMap T.Text v
36+toInsOrdHashMap = InsOrdHashMap.fromList . fmap (first Key.toText) . KeyMap.toList
37+
38+fromInsOrdHashMap :: InsOrdHashMap.InsOrdHashMap T.Text v -> KeyMap.KeyMap v
39+fromInsOrdHashMap = KeyMap.fromList . fmap (first Key.fromText) . InsOrdHashMap.toList
40+
41+lookupKey :: T.Text -> KeyMap.KeyMap v -> Maybe v
42+lookupKey = KeyMap.lookup . Key.fromText
43+
44+hasKey :: T.Text -> KeyMap.KeyMap a -> Bool
45+hasKey = KeyMap.member . Key.fromText
46+#else
47+deleteKey :: T.Text -> HM.HashMap T.Text v -> HM.HashMap T.Text v
48+deleteKey = HM.delete
49+
50+objectToList :: HM.HashMap T.Text v -> [(T.Text, v)]
51+objectToList = HM.toList
52+
53+objectKeys :: HM.HashMap T.Text v -> [T.Text]
54+objectKeys = HM.keys
55+
56+stringToKey :: String -> T.Text
57+stringToKey = T.pack
58+
59+keyToString :: T.Text -> String
60+keyToString = T.unpack
61+
62+keyToText :: T.Text -> T.Text
63+keyToText = id
64+
65+toInsOrdHashMap :: HM.HashMap T.Text v -> InsOrdHashMap.InsOrdHashMap T.Text v
66+toInsOrdHashMap = InsOrdHashMap.fromHashMap
67+
68+fromInsOrdHashMap :: InsOrdHashMap.InsOrdHashMap T.Text v -> HM.HashMap T.Text v
69+fromInsOrdHashMap = InsOrdHashMap.toHashMap
70+
71+lookupKey :: T.Text -> HM.HashMap T.Text v -> Maybe v
72+lookupKey = HM.lookup
73+
74+hasKey :: T.Text -> HM.HashMap T.Text a -> Bool
75+hasKey = HM.member
76+#endif
src/Data/OpenApi/Internal.hs+18−15
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE DataKinds #-}
23{-# LANGUAGE DeriveDataTypeable #-}
34{-# LANGUAGE DeriveGeneric #-}
@@ -21,6 +22,9 @@ import Prelude.Compat
2122import Control.Applicative
2223import Control.Lens ((&), (.~), (?~))
2324import Data.Aeson hiding (Encoding)
25+#if MIN_VERSION_aeson(2,0,0)
26+import qualified Data.Aeson.KeyMap as KeyMap
27+#endif
2428import qualified Data.Aeson.Types as JSON
2529import Data.Data (Constr, Data (..), DataType, Fixity (..), Typeable,
2630 constrIndex, mkConstr, mkDataType)
@@ -45,17 +49,13 @@ import Text.Read (readMaybe)
4549import Data.HashMap.Strict.InsOrd (InsOrdHashMap)
4650import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
4751
48−import Generics.SOP.TH (deriveGeneric)
49−import Data.OpenApi.Internal.AesonUtils (sopSwaggerGenericToJSON
50− ,sopSwaggerGenericToJSONWithOpts
51− ,sopSwaggerGenericParseJSON
52− ,HasSwaggerAesonOptions(..)
53− ,AesonDefaultValue(..)
54− ,mkSwaggerAesonOptions
55− ,saoAdditionalPairs
56− ,saoSubObject)
52+import Data.OpenApi.Aeson.Compat (deleteKey)
53+import Data.OpenApi.Internal.AesonUtils (AesonDefaultValue (..), HasSwaggerAesonOptions (..),
54+ mkSwaggerAesonOptions, saoAdditionalPairs, saoSubObject,
55+ sopSwaggerGenericParseJSON, sopSwaggerGenericToEncoding,
56+ sopSwaggerGenericToJSON, sopSwaggerGenericToJSONWithOpts)
5757import Data.OpenApi.Internal.Utils
58−import Data.OpenApi.Internal.AesonUtils (sopSwaggerGenericToEncoding)
58+import Generics.SOP.TH (deriveGeneric)
5959
6060-- $setup
6161-- >>> :seti -XDataKinds
@@ -196,7 +196,7 @@ data Components = Components
196196 , _componentsExamples :: Definitions Example
197197 , _componentsRequestBodies :: Definitions RequestBody
198198 , _componentsHeaders :: Definitions Header
199− , _componentsSecuritySchemes :: Definitions SecurityScheme
199+ , _componentsSecuritySchemes :: SecurityDefinitions
200200 , _componentsLinks :: Definitions Link
201201 , _componentsCallbacks :: Definitions Callback
202202 } deriving (Eq, Show, Generic, Data, Typeable)
@@ -337,7 +337,7 @@ data RequestBody = RequestBody
337337 -- | The content of the request body.
338338 -- The key is a media type or media type range and the value describes it.
339339 -- For requests that match multiple keys, only the most specific key is applicable.
340− -- e.g. @text/plain@ overrides @text/*@
340+ -- e.g. @text/plain@ overrides @text/\*@
341341 , _requestBodyContent :: InsOrdHashMap MediaType MediaTypeObject
342342
343343 -- | Determines if the request body is required in the request.
@@ -394,7 +394,7 @@ data Encoding = Encoding
394394 -- for other primitive types – @text/plain@; for object - @application/json@;
395395 -- for array – the default is defined based on the inner type.
396396 -- The value can be a specific media type (e.g. @application/json@),
397− -- a wildcard media type (e.g. @image/*@), or a comma-separated list of the two types.
397+ -- a wildcard media type (e.g. @image/\*@), or a comma-separated list of the two types.
398398 _encodingContentType :: Maybe MediaType
399399
400400 -- | A map allowing additional information to be provided as headers,
@@ -734,7 +734,7 @@ data Response = Response
734734 -- | A map containing descriptions of potential response payloads.
735735 -- The key is a media type or media type range and the value describes it.
736736 -- For responses that match multiple keys, only the most specific key is applicable.
737− -- e.g. @text/plain@ overrides @text/*@.
737+ -- e.g. @text/plain@ overrides @text/\*@.
738738 , _responseContent :: InsOrdHashMap MediaType MediaTypeObject
739739
740740 -- | Maps a header name to its definition.
@@ -1125,6 +1125,7 @@ instance SwaggerMonoid Response
11251125instance SwaggerMonoid ExternalDocs
11261126instance SwaggerMonoid Operation
11271127instance (Eq a, Hashable a) => SwaggerMonoid (InsOrdHashSet a)
1128+instance SwaggerMonoid SecurityDefinitions
11281129
11291130instance SwaggerMonoid MimeList
11301131deriving instance SwaggerMonoid URL
@@ -1491,7 +1492,7 @@ instance FromJSON Param where
14911492instance FromJSON Responses where
14921493 parseJSON (Object o) = Responses
14931494 <$> o .:? "default"
1494− <*> parseJSON (Object (HashMap.delete "default" o))
1495+ <*> parseJSON (Object (deleteKey "default" o))
14951496 parseJSON _ = empty
14961497
14971498instance FromJSON Example where
@@ -1614,3 +1615,5 @@ instance AesonDefaultValue MimeList where defaultValue = Just mempty
16141615instance AesonDefaultValue Info
16151616instance AesonDefaultValue ParamLocation
16161617instance AesonDefaultValue Link
1618+instance AesonDefaultValue SecurityDefinitions where
1619+ defaultValue = Just mempty
src/Data/OpenApi/Internal/AesonUtils.hs+11−10
@@ -36,18 +36,19 @@ import Data.Text (Text)
3636import Generics.SOP
3737
3838import qualified Data.Text as T
39−import qualified Data.HashMap.Strict as HM
4039import qualified Data.Set as Set
4140import qualified Data.HashMap.Strict.InsOrd as InsOrd
4241import qualified Data.HashSet.InsOrd as InsOrdHS
4342
43+import Data.OpenApi.Aeson.Compat (keyToString, objectToList, stringToKey)
44+
4445-------------------------------------------------------------------------------
4546-- SwaggerAesonOptions
4647-------------------------------------------------------------------------------
4748
4849data SwaggerAesonOptions = SwaggerAesonOptions
4950 { _saoPrefix :: String
50− , _saoAdditionalPairs :: [(Text, Value)]
51+ , _saoAdditionalPairs :: [Pair]
5152 , _saoSubObject :: Maybe String
5253 }
5354
@@ -154,14 +155,14 @@ sopSwaggerGenericToJSON'' (SwaggerAesonOptions prefix _ sub) = go
154155 go Nil Nil Nil = []
155156 go (I x :* xs) (FieldInfo name :* names) (def :* defs)
156157 | Just name' == sub = case json of
157− Object m -> HM.toList m ++ rest
158+ Object m -> objectToList m ++ rest
158159 Null -> rest
159160 _ -> error $ "sopSwaggerGenericToJSON: subjson is not an object: " ++ show json
160161 -- If default value: omit it.
161162 | Just x == def =
162163 rest
163164 | otherwise =
164− (T.pack name', json) : rest
165+ (stringToKey name', json) : rest
165166 where
166167 json = toJSON x
167168 name' = fieldNameModifier name
@@ -195,11 +196,11 @@ sopSwaggerGenericParseJSON = withObject "Swagger Record Object" $ \obj ->
195196 proxy = Proxy :: Proxy a
196197 opts = swaggerAesonOptions proxy
197198
198− parseAdditionalField :: Object -> (Text, Value) -> Parser ()
199+ parseAdditionalField :: Object -> Pair -> Parser ()
199200 parseAdditionalField obj (k, v) = do
200201 v' <- obj .: k
201202 unless (v == v') $ fail $
202− "Additonal field don't match for key " ++ T.unpack k
203+ "Additonal field don't match for key " ++ keyToString k
203204 ++ ": " ++ show v
204205 ++ " /= " ++ show v'
205206
@@ -230,8 +231,8 @@ sopSwaggerGenericParseJSON'' (SwaggerAesonOptions prefix _ sub) obj = go
230231 -- Note: we might strip fields of outer structure.
231232 cons <$> (withDef $ parseJSON $ Object obj) <*> rest
232233 | otherwise = case def of
233− Just def' -> cons <$> obj .:? T.pack name' .!= def' <*> rest
234− Nothing -> cons <$> obj .: T.pack name' <*> rest
234+ Just def' -> cons <$> obj .:? stringToKey name' .!= def' <*> rest
235+ Nothing -> cons <$> obj .: stringToKey name' <*> rest
235236 where
236237 cons h t = I h :* t
237238 name' = fieldNameModifier name
@@ -294,14 +295,14 @@ sopSwaggerGenericToEncoding'' (SwaggerAesonOptions prefix _ sub) = go
294295 go Nil Nil Nil = mempty
295296 go (I x :* xs) (FieldInfo name :* names) (def :* defs)
296297 | Just name' == sub = case toJSON x of
297− Object m -> pairsToSeries (HM.toList m) <> rest
298+ Object m -> pairsToSeries (objectToList m) <> rest
298299 Null -> rest
299300 _ -> error $ "sopSwaggerGenericToJSON: subjson is not an object: " ++ show (toJSON x)
300301 -- If default value: omit it.
301302 | Just x == def =
302303 rest
303304 | otherwise =
304− (T.pack name' .= x) <> rest
305+ (stringToKey name' .= x) <> rest
305306 where
306307 name' = fieldNameModifier name
307308 rest = go xs names defs
src/Data/OpenApi/Internal/ParamSchema.hs+2−2
@@ -216,9 +216,9 @@ instance ToParamSchema LocalTime where
216216
217217-- |
218218-- >>> toParamSchema (Proxy :: Proxy ZonedTime) ^. format
219−-- Just "yyyy-mm-ddThh:MM:ss+hhMM"
219+-- Just "date-time"
220220instance ToParamSchema ZonedTime where
221− toParamSchema _ = timeParamSchema "yyyy-mm-ddThh:MM:ss+hhMM"
221+ toParamSchema _ = timeParamSchema "date-time"
222222
223223-- |
224224-- >>> toParamSchema (Proxy :: Proxy UTCTime) ^. format
src/Data/OpenApi/Internal/Schema.hs+55−76
@@ -64,13 +64,14 @@ import GHC.Generics
6464import qualified Data.UUID.Types as UUID
6565import Type.Reflection (Typeable, typeRep)
6666
67−import Data.OpenApi.Declare
68−import Data.OpenApi.Internal
69−import Data.OpenApi.Internal.ParamSchema (ToParamSchema(..))
70−import Data.OpenApi.Lens hiding (name, schema)
71−import qualified Data.OpenApi.Lens as Swagger
72−import Data.OpenApi.SchemaOptions
73−import Data.OpenApi.Internal.TypeShape
67+import Data.OpenApi.Aeson.Compat (keyToText, objectKeys, toInsOrdHashMap)
68+import Data.OpenApi.Declare
69+import Data.OpenApi.Internal
70+import Data.OpenApi.Internal.ParamSchema (ToParamSchema (..))
71+import Data.OpenApi.Internal.TypeShape
72+import Data.OpenApi.Lens hiding (name, schema)
73+import qualified Data.OpenApi.Lens as Swagger
74+import Data.OpenApi.SchemaOptions
7475
7576import qualified Data.ByteString as BS
7677import qualified Data.ByteString.Lazy.Char8 as BSL
@@ -135,7 +136,7 @@ rename name (NamedSchema _ schema) = NamedSchema name schema
135136--
136137-- instance ToSchema Coord
137138-- @
138−class ToSchema a where
139+class Typeable a => ToSchema a where
139140 -- | Convert a type into an optionally named schema
140141 -- together with all used definitions.
141142 -- Note that the schema itself is included in definitions
@@ -405,8 +406,8 @@ sketchSchema = sketch . toJSON
405406 _ -> Nothing
406407 go (Object o) = mempty
407408 & type_ ?~ OpenApiObject
408− & required .~ sort (HashMap.keys o)
409− & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
409+ & required .~ sort (objectKeys o)
410+ & properties .~ fmap (Inline . go) (toInsOrdHashMap o)
410411
411412-- | Make a restrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
412413-- Produced schema uses as much constraints as possible.
@@ -570,12 +571,12 @@ sketchStrictSchema = go . toJSON
570571 go js@(Object o) = mempty
571572 & type_ ?~ OpenApiObject
572573 & required .~ sort names
573− & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
574+ & properties .~ fmap (Inline . go) (toInsOrdHashMap o)
574575 & maxProperties ?~ fromIntegral (length names)
575576 & minProperties ?~ fromIntegral (length names)
576577 & enum_ ?~ [js]
577578 where
578− names = HashMap.keys o
579+ names = objectKeys o
579580
580581class GToSchema (f :: * -> *) where
581582 gdeclareNamedSchema :: SchemaOptions -> Proxy f -> Schema -> Declare (Definitions Schema) NamedSchema
@@ -610,7 +611,7 @@ instance ToSchema Scientific where declareNamedSchema = plain . paramSchemaToSc
610611instance ToSchema Double where declareNamedSchema = plain . paramSchemaToSchema
611612instance ToSchema Float where declareNamedSchema = plain . paramSchemaToSchema
612613
613−instance HasResolution a => ToSchema (Fixed a) where declareNamedSchema = plain . paramSchemaToSchema
614+instance (Typeable (Fixed a), HasResolution a) => ToSchema (Fixed a) where declareNamedSchema = plain . paramSchemaToSchema
614615
615616instance ToSchema a => ToSchema (Maybe a) where
616617 declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy a)
@@ -627,12 +628,18 @@ instance ToSchema UUID.UUID where
627628 declareNamedSchema p = pure $ named "UUID" $ paramSchemaToSchema p
628629 & example ?~ toJSON (UUID.toText UUID.nil)
629630
630−instance (ToSchema a, ToSchema b) => ToSchema (a, b)
631−instance (ToSchema a, ToSchema b, ToSchema c) => ToSchema (a, b, c)
632−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d) => ToSchema (a, b, c, d)
633−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e) => ToSchema (a, b, c, d, e)
634−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f) => ToSchema (a, b, c, d, e, f)
635−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f, ToSchema g) => ToSchema (a, b, c, d, e, f, g)
631+instance (ToSchema a, ToSchema b) => ToSchema (a, b) where
632+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
633+instance (ToSchema a, ToSchema b, ToSchema c) => ToSchema (a, b, c) where
634+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
635+instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d) => ToSchema (a, b, c, d) where
636+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
637+instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e) => ToSchema (a, b, c, d, e) where
638+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
639+instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f) => ToSchema (a, b, c, d, e, f) where
640+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
641+instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f, ToSchema g) => ToSchema (a, b, c, d, e, f, g) where
642+ declareNamedSchema = fmap unname . genericDeclareNamedSchema defaultSchemaOptions
636643
637644timeSchema :: T.Text -> Schema
638645timeSchema fmt = mempty
@@ -651,7 +658,7 @@ instance ToSchema LocalTime where
651658 declareNamedSchema _ = pure $ named "LocalTime" $ timeSchema "yyyy-mm-ddThh:MM:ss"
652659 & example ?~ toJSON (LocalTime (fromGregorian 2016 7 22) (TimeOfDay 7 40 0))
653660
654−-- | Format @"date"@ corresponds to @yyyy-mm-ddThh:MM:ss(Z|+hh:MM)@ format.
661+-- | Format @"date-time"@ corresponds to @yyyy-mm-ddThh:MM:ss(Z|+hh:MM)@ format.
655662instance ToSchema ZonedTime where
656663 declareNamedSchema _ = pure $ named "ZonedTime" $ timeSchema "date-time"
657664 & example ?~ toJSON (ZonedTime (LocalTime (fromGregorian 2016 7 22) (TimeOfDay 7 40 0)) (hoursToTimeZone 3))
@@ -683,7 +690,7 @@ instance ToSchemaByteStringError BSL.ByteString => ToSchema BSL.ByteString where
683690instance ToSchema IntSet where declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy (Set Int))
684691
685692-- | NOTE: This schema does not account for the uniqueness of keys.
686−instance ToSchema a => ToSchema (IntMap a) where
693+instance (ToSchema a) => ToSchema (IntMap a) where
687694 declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [(Int, a)])
688695
689696instance (ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (Map k v) where
@@ -804,13 +811,13 @@ declareSchemaBoundedEnumKeyMapping :: forall map key value.
804811 (Bounded key, Enum key, ToJSONKey key, ToSchema key, ToSchema value)
805812 => Proxy (map key value) -> Declare (Definitions Schema) Schema
806813declareSchemaBoundedEnumKeyMapping _ = case toJSONKey :: ToJSONKeyFunction key of
807− ToJSONKeyText keyToText _ -> objectSchema keyToText
814+ ToJSONKeyText getKey _ -> objectSchema getKey
808815 ToJSONKeyValue _ _ -> declareSchema (Proxy :: Proxy [(key, value)])
809816 where
810− objectSchema keyToText = do
817+ objectSchema getKey = do
811818 valueRef <- declareSchemaRef (Proxy :: Proxy value)
812819 let allKeys = [minBound..maxBound :: key]
813− mkPair k = (keyToText k, valueRef)
820+ mkPair k = (keyToText $ getKey k, valueRef)
814821 return $ mempty
815822 & type_ ?~ OpenApiObject
816823 & properties .~ InsOrdHashMap.fromList (map mkPair allKeys)
@@ -853,7 +860,7 @@ toSchemaBoundedEnumKeyMapping :: forall map key value.
853860toSchemaBoundedEnumKeyMapping = flip evalDeclare mempty . declareSchemaBoundedEnumKeyMapping
854861
855862-- | A configurable generic @'Schema'@ creator.
856−genericDeclareSchema :: (Generic a, GToSchema (Rep a)) =>
863+genericDeclareSchema :: (Generic a, GToSchema (Rep a), Typeable a) =>
857864 SchemaOptions -> Proxy a -> Declare (Definitions Schema) Schema
858865genericDeclareSchema opts proxy = _namedSchemaSchema <$> genericDeclareNamedSchema opts proxy
859866
@@ -861,9 +868,24 @@ genericDeclareSchema opts proxy = _namedSchemaSchema <$> genericDeclareNamedSche
861868-- This function applied to @'defaultSchemaOptions'@
862869-- is used as the default for @'declareNamedSchema'@
863870-- when the type is an instance of @'Generic'@.
864−genericDeclareNamedSchema :: forall a. (Generic a, GToSchema (Rep a)) =>
871+--
872+-- Default implementation will use the name from 'Typeable' instance, including concrete
873+-- instantioations of type variables.
874+--
875+-- For example:
876+--
877+-- >>> _namedSchemaName $ undeclare $ genericDeclareNamedSchema defaultSchemaOptions (Proxy :: Proxy (Either Int Bool))
878+-- Just "Either_Int_Bool"
879+genericDeclareNamedSchema :: forall a. (Generic a, GToSchema (Rep a), Typeable a) =>
865880 SchemaOptions -> Proxy a -> Declare (Definitions Schema) NamedSchema
866−genericDeclareNamedSchema opts _ = gdeclareNamedSchema opts (Proxy :: Proxy (Rep a)) mempty
881+genericDeclareNamedSchema opts _ =
882+ rename (Just $ T.pack name) <$> gdeclareNamedSchema opts (Proxy :: Proxy (Rep a)) mempty
883+ where
884+ unspace ' ' = '_'
885+ unspace x = x
886+ orig = fmap unspace $ show $ typeRep @a
887+ name = datatypeNameModifier opts orig
888+
867889
868890-- | Derive a 'Generic'-based name for a datatype and assign it to a given 'Schema'.
869891genericNameSchema :: forall a d f.
@@ -923,10 +945,13 @@ instance (Selector s, GToSchema f, GToSchema (S1 s f)) => GToSchema (C1 c (S1 s
923945 case schema ^. items of
924946 Just (OpenApiItemsArray [_]) -> fieldSchema
925947 _ -> do
926− declare defs
927− return (unnamed schema)
948+ -- We have to run recordSchema instead of just using its defs,
949+ -- since those can be recursive and will lead to infinite loop,
950+ -- see https://github.com/biocad/openapi3/pull/37
951+ NamedSchema _ schema' <- recordSchema
952+ return (unnamed schema')
928953 where
929− (defs, NamedSchema _ schema) = runDeclare recordSchema mempty
954+ (_, NamedSchema _ schema) = runDeclare recordSchema mempty
930955 recordSchema = gdeclareNamedSchema opts (Proxy :: Proxy (S1 s f)) s
931956 fieldSchema = gdeclareNamedSchema opts (Proxy :: Proxy f) s
932957
@@ -1089,49 +1114,6 @@ data Proxy2 a b = Proxy2
10891114
10901115data Proxy3 a b c = Proxy3
10911116
1092−-- | This class allows to generate schemas for polymorphic types (of kind @Type -> Type@).
1093−--
1094−-- Intended usage:
1095−--
1096−-- >>> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
1097−-- >>> :{
1098−-- instance (ToSchema a, Typeable a) => ToSchema (Foo a) where
1099−-- declareNamedSchema _ = declareNamedSchema @(BySchema1 Foo a) Proxy
1100−-- :}
1101−--
1102−-- >>> toNamedSchema @(Foo Int) Proxy ^. name
1103−-- Just "Foo_Int"
1104−-- >>> toNamedSchema @(Foo Bool) Proxy ^. name
1105−-- Just "Foo_Bool"
1106−-- >>> toNamedSchema @(Foo (Foo T.Text)) Proxy ^. name
1107−-- Just "Foo_(Foo_Text)"
1108−class ToSchema1 (f :: * -> *) where
1109− declareNamedSchema1 :: (Generic (f a), GToSchema (Rep (f a)), ToSchema a) => Proxy f -> Proxy a -> Declare (Definitions Schema) NamedSchema
1110−
1111− -- It would be cleaner to have GToSchema constraint only on default signature and not in the class method
1112− -- above, however sadly GHC does not like it.
1113− default declareNamedSchema1 :: forall a. (ToSchema a, Generic (f a), GToSchema (Rep (f a))) => Proxy f -> Proxy a -> Declare (Definitions Schema) NamedSchema
1114− declareNamedSchema1 _ _ = genericDeclareNamedSchema @(f a) defaultSchemaOptions Proxy
1115−
1116−{- | For GHC 8.6+ it's more convenient to use @DerivingVia@ to derive instances of 'ToSchema'
1117−using 'ToSchema1' instance, like this:
1118−
1119−#if __GLASGOW_HASKELL__ >= 806
1120−>>> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
1121−>>> deriving via BySchema1 Foo a instance (ToSchema a, Typeable a) => ToSchema (Foo a)
1122−#else
1123−> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
1124−> deriving via BySchema1 Foo a instance (ToSchema a, Typeable a) => ToSchema (Foo a)
1125−#endif
1126−-}
1127−newtype BySchema1 f a = BySchema1 (f a)
1128−
1129−instance (ToSchema1 f, Generic (f a), GToSchema (Rep (f a)), Typeable (f a), ToSchema a) => ToSchema (BySchema1 f a) where
1130− declareNamedSchema _ = do
1131− sch <- declareNamedSchema1 @f @a Proxy Proxy
1132− let tName = T.replace " " "_" $ T.pack $ show $ typeRep @(f a)
1133− return $ rename (Just tName) sch
1134−
11351117{- $setup
11361118>>> import Data.OpenApi
11371119>>> import Data.Aeson (encode)
@@ -1141,7 +1123,4 @@ instance (ToSchema1 f, Generic (f a), GToSchema (Rep (f a)), Typeable (f a), ToS
11411123>>> :set -XDeriveAnyClass
11421124>>> :set -XStandaloneDeriving
11431125>>> :set -XTypeApplications
1144−#if __GLASGOW_HASKELL__ >= 806
1145−>>> :set -XDerivingVia
1146−#endif
11471126-}
src/Data/OpenApi/Internal/Schema/Validation.hs+23−11
@@ -1,4 +1,5 @@
11{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE CPP #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE DeriveFunctor #-}
45{-# LANGUAGE FlexibleInstances #-}
@@ -31,10 +32,14 @@ import Control.Lens hiding (allOf)
3132import Control.Monad (forM, forM_, when)
3233
3334import Data.Aeson hiding (Result)
35+#if MIN_VERSION_aeson(2,0,0)
36+import qualified Data.Aeson.KeyMap as KeyMap
37+#endif
3438import Data.Foldable (for_, sequenceA_,
3539 traverse_)
40+#if !MIN_VERSION_aeson(2,0,0)
3641import Data.HashMap.Strict (HashMap)
37−import qualified Data.HashMap.Strict as HashMap
42+#endif
3843import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
3944import qualified "unordered-containers" Data.HashSet as HashSet
4045import Data.Maybe (fromMaybe)
@@ -47,11 +52,12 @@ import qualified Data.Text.Lazy.Encoding as TL
4752import Data.Vector (Vector)
4853import qualified Data.Vector as Vector
4954
50−import Data.OpenApi.Declare
51−import Data.OpenApi.Internal
52−import Data.OpenApi.Internal.Utils
53−import Data.OpenApi.Internal.Schema
54−import Data.OpenApi.Lens
55+import Data.OpenApi.Aeson.Compat (hasKey, keyToText, lookupKey, objectToList)
56+import Data.OpenApi.Declare
57+import Data.OpenApi.Internal
58+import Data.OpenApi.Internal.Schema
59+import Data.OpenApi.Internal.Utils
60+import Data.OpenApi.Lens
5561
5662-- | Validate @'ToJSON'@ instance matches @'ToSchema'@ for a given value.
5763-- This can be used with QuickCheck to ensure those instances are coherent:
@@ -366,10 +372,16 @@ validateArray xs = do
366372 len = Vector.length xs
367373 allUnique = len == HashSet.size (HashSet.fromList (Vector.toList xs))
368374
369−validateObject :: HashMap Text Value -> Validation Schema ()
375+validateObject ::
376+#if MIN_VERSION_aeson(2,0,0)
377+ KeyMap.KeyMap Value
378+#else
379+ HashMap Text Value
380+#endif
381+ -> Validation Schema ()
370382validateObject o = withSchema $ \sch ->
371383 case sch ^. discriminator of
372− Just (Discriminator pname types) -> case fromJSON <$> HashMap.lookup pname o of
384+ Just (Discriminator pname types) -> case fromJSON <$> lookupKey pname o of
373385 Just (Success pvalue) ->
374386 let ref = fromMaybe pvalue $ InsOrdHashMap.lookup pvalue types
375387 -- TODO ref may be name or reference
@@ -388,15 +400,15 @@ validateObject o = withSchema $ \sch ->
388400 validateRequired
389401 validateProps
390402 where
391− size = fromIntegral (HashMap.size o)
403+ size = fromIntegral (length o)
392404
393405 validateRequired = withSchema $ \sch -> traverse_ validateReq (sch ^. required)
394406 validateReq n =
395− when (not (HashMap.member n o)) $
407+ when (not (hasKey n o)) $
396408 invalid ("property " ++ show n ++ " is required, but not found in " ++ show (encode o))
397409
398410 validateProps = withSchema $ \sch -> do
399− for_ (HashMap.toList o) $ \(k, v) ->
411+ for_ (objectToList o) $ \(keyToText -> k, v) ->
400412 case v of
401413 Null | not (k `elem` (sch ^. required)) -> valid -- null is fine for non-required property
402414 _ ->
src/Data/OpenApi/Schema.hs−2
@@ -13,8 +13,6 @@ module Data.OpenApi.Schema (
1313 toSchemaRef,
1414 schemaName,
1515 toInlinedSchema,
16− ToSchema1(..),
17− BySchema1(..),
1816
1917 -- * Generic schema encoding
2018 genericDeclareNamedSchema,
src/Data/OpenApi/Schema/Generator.hs+3−1
@@ -25,6 +25,8 @@ import Test.QuickCheck (arbitrary)
2525import Test.QuickCheck.Gen
2626import Test.QuickCheck.Property
2727
28+import Data.OpenApi.Aeson.Compat (fromInsOrdHashMap)
29+
2830-- | Note: 'schemaGen' may 'error', if schema type is not specified,
2931-- and cannot be inferred.
3032schemaGen :: Definitions Schema -> Schema -> Gen Value
@@ -95,7 +97,7 @@ schemaGen defns schema =
9597 return . M.fromList $ zip additionalKeys (repeat . schemaGen defns $ dereference defns addlSchema)
9698 _ -> return []
9799 x <- sequence $ gens <> additionalGens
98− return . Object $ M.toHashMap x
100+ return . Object $ fromInsOrdHashMap x
99101
100102dereference :: Definitions a -> Referenced a -> a
101103dereference _ (Inline a) = a
stack.yaml+1−1
@@ -1,4 +1,4 @@
1−resolver: lts-16.8
1+resolver: lts-16.31
22packages:
33- '.'
44extra-deps:
test/Data/OpenApi/CommonTestTypes.hs+123
@@ -863,6 +863,129 @@ singleMaybeFieldSchemaJSON = [aesonQQ|
863863}
864864|]
865865
866+-- ========================================================================
867+-- Natural Language (single field data with recursive fields)
868+-- ========================================================================
869+
870+data Predicate
871+ = PredicateNoun Noun
872+ | PredicateOmitted Omitted
873+ deriving (Eq, Ord, Read, Show, Generic)
874+instance ToJSON Predicate
875+instance ToSchema Predicate
876+
877+data Noun
878+ = Noun
879+ { nounSurf :: LangWord
880+ , nounModify :: [Modifier]
881+ }
882+ deriving (Eq, Ord, Read, Show, Generic)
883+instance ToJSON Noun
884+instance ToSchema Noun
885+
886+data LangWord
887+ = LangWord
888+ { langWordSurf :: String
889+ , langWordBase :: String
890+ }
891+ deriving (Eq, Ord, Read, Show, Generic)
892+instance ToJSON LangWord
893+instance ToSchema LangWord
894+
895+data Modifier
896+ = ModifierNoun Noun
897+ | ModifierOmitted Omitted
898+ deriving (Eq, Ord, Read, Show, Generic)
899+instance ToJSON Modifier
900+instance ToSchema Modifier
901+
902+newtype Omitted
903+ = Omitted
904+ { omittedModify :: [Modifier]
905+ }
906+ deriving (Eq, Ord, Read, Show, Generic)
907+instance ToJSON Omitted
908+instance ToSchema Omitted
909+
910+predicateSchemaDeclareJSON :: Value
911+predicateSchemaDeclareJSON = [aesonQQ|
912+[
913+ {
914+ "Predicate": {
915+ "oneOf": [
916+ {
917+ "properties": {
918+ "contents": { "$ref": "#/components/schemas/Noun" },
919+ "tag": { "enum": ["PredicateNoun"], "type": "string" }
920+ },
921+ "required": ["tag", "contents"],
922+ "type": "object"
923+ },
924+ {
925+ "properties": {
926+ "contents": { "$ref": "#/components/schemas/Omitted" },
927+ "tag": { "enum": ["PredicateOmitted"], "type": "string" }
928+ },
929+ "required": ["tag", "contents"],
930+ "type": "object"
931+ }
932+ ],
933+ "type": "object"
934+ },
935+ "Noun": {
936+ "properties": {
937+ "nounModify": {
938+ "items": { "$ref": "#/components/schemas/Modifier" },
939+ "type": "array"
940+ },
941+ "nounSurf": { "$ref": "#/components/schemas/LangWord" }
942+ },
943+ "required": ["nounSurf", "nounModify"],
944+ "type": "object"
945+ },
946+ "LangWord": {
947+ "properties": {
948+ "langWordBase": { "type": "string" },
949+ "langWordSurf": { "type": "string" }
950+ },
951+ "required": ["langWordSurf", "langWordBase"],
952+ "type": "object"
953+ },
954+ "Modifier": {
955+ "oneOf": [
956+ {
957+ "properties": {
958+ "contents": { "$ref": "#/components/schemas/Noun" },
959+ "tag": { "enum": ["ModifierNoun"], "type": "string" }
960+ },
961+ "required": ["tag", "contents"],
962+ "type": "object"
963+ },
964+ {
965+ "properties": {
966+ "contents": { "$ref": "#/components/schemas/Omitted" },
967+ "tag": { "enum": ["ModifierOmitted"], "type": "string" }
968+ },
969+ "required": ["tag", "contents"],
970+ "type": "object"
971+ }
972+ ],
973+ "type": "object"
974+ },
975+ "Omitted": {
976+ "properties": {
977+ "omittedModify": {
978+ "items": { "$ref": "#/components/schemas/Modifier" },
979+ "type": "array"
980+ }
981+ },
982+ "required": ["omittedModify"],
983+ "type": "object"
984+ }
985+ },
986+ { "$ref": "#/components/schemas/Predicate" }
987+]
988+|]
866989
867990-- ========================================================================
868991-- TimeOfDay
test/Data/OpenApi/Schema/ValidationSpec.hs+20−3
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE DeriveGeneric #-}
23{-# LANGUAGE PackageImports #-}
34{-# LANGUAGE RecordWildCards #-}
@@ -8,6 +9,10 @@ module Data.OpenApi.Schema.ValidationSpec where
89import Control.Applicative
910import Control.Lens ((&), (.~), (?~))
1011import Data.Aeson
12+#if MIN_VERSION_aeson(2,0,0)
13+import qualified Data.Aeson.Key as Key
14+import qualified Data.Aeson.KeyMap as KeyMap
15+#endif
1116import Data.Aeson.Types
1217import Data.Hashable (Hashable)
1318import Data.HashMap.Strict (HashMap)
@@ -30,6 +35,7 @@ import GHC.Generics
3035
3136import Data.OpenApi
3237import Data.OpenApi.Declare
38+import Data.OpenApi.Aeson.Compat (stringToKey)
3339
3440import Test.Hspec
3541import Test.Hspec.QuickCheck
@@ -124,9 +130,9 @@ instance Arbitrary Person where
124130
125131invalidPersonToJSON :: Person -> Value
126132invalidPersonToJSON Person{..} = object
127− [ T.pack "personName" .= toJSON name
128− , T.pack "personPhone" .= toJSON phone
129− , T.pack "personEmail" .= toJSON email
133+ [ stringToKey "personName" .= toJSON name
134+ , stringToKey "personPhone" .= toJSON phone
135+ , stringToKey "personEmail" .= toJSON email
130136 ]
131137
132138-- ========================================================================
@@ -295,6 +301,8 @@ instance Eq ZonedTime where
295301-- Arbitrary instance for Data.Aeson.Value
296302-- ========================================================================
297303
304+-- These instances were introduces in aeson upstream in 2.0.3
305+#if !MIN_VERSION_aeson(2,0,3)
298306instance Arbitrary Value where
299307 -- Weights are almost random
300308 -- Uniform oneof tends not to build complex objects cause of recursive call.
@@ -305,3 +313,12 @@ instance Arbitrary Value where
305313 , (3, Number <$> arbitrary)
306314 , (3, Bool <$> arbitrary)
307315 , (1, return Null) ]
316+
317+#if MIN_VERSION_aeson(2,0,0)
318+instance Arbitrary v => Arbitrary (KeyMap.KeyMap v) where
319+ arbitrary = KeyMap.fromList <$> arbitrary
320+
321+instance Arbitrary Key.Key where
322+ arbitrary = Key.fromText <$> arbitrary
323+#endif
324+#endif
test/Data/OpenApi/SchemaSpec.hs+4
@@ -59,6 +59,9 @@ checkInlinedRecSchema proxy js = inlineNonRecursiveSchemas defs s <=> js
5959 where
6060 (defs, s) = runDeclare (declareSchema proxy) mempty
6161
62+checkToSchemaDeclare :: (HasCallStack, ToSchema a) => Proxy a -> Value -> Spec
63+checkToSchemaDeclare proxy js = runDeclare (declareSchemaRef proxy) mempty <=> js
64+
6265spec :: Spec
6366spec = do
6467 describe "Generic ToSchema" $ do
@@ -78,6 +81,7 @@ spec = do
7881 context "UserId (non-record newtype)" $ checkToSchema (Proxy :: Proxy UserId) userIdSchemaJSON
7982 context "Player (unary record)" $ checkToSchema (Proxy :: Proxy Player) playerSchemaJSON
8083 context "SingleMaybeField (unary record with Maybe)" $ checkToSchema (Proxy :: Proxy SingleMaybeField) singleMaybeFieldSchemaJSON
84+ context "Natural Language (single field data with recursive fields)" $ checkToSchemaDeclare (Proxy :: Proxy Predicate) predicateSchemaDeclareJSON
8185 context "Players (inlining schema)" $ checkToSchema (Proxy :: Proxy Players) playersSchemaJSON
8286 context "MyRoseTree (datatypeNameModifier)" $ checkToSchema (Proxy :: Proxy MyRoseTree) myRoseTreeSchemaJSON
8387 context "MyRoseTree' (datatypeNameModifier)" $ checkToSchema (Proxy :: Proxy MyRoseTree') myRoseTreeSchemaJSON'
test/Data/OpenApiSpec.hs+17−1
@@ -45,6 +45,10 @@ spec = do
4545 fromJSON petstoreExampleJSON `shouldSatisfy` (\x -> case x of Success (_ :: OpenApi) -> True; _ -> False)
4646 it "roundtrips: fmap toJSON . fromJSON" $ do
4747 (toJSON :: OpenApi -> Value) <$> fromJSON petstoreExampleJSON `shouldBe` Success petstoreExampleJSON
48+ context "Security schemes" $ do
49+ it "merged correctly" $ do
50+ let merged = oAuth2SecurityDefinitionsReadOpenApi <> oAuth2SecurityDefinitionsWriteOpenApi
51+ merged `shouldBe` oAuth2SecurityDefinitionsOpenApi
4852
4953main :: IO ()
5054main = hspec spec
@@ -441,7 +445,7 @@ responsesDefinitionExampleJSON = [aesonQQ|
441445|]
442446
443447-- =======================================================================
444−-- Responses Definition object
448+-- Security Definition object
445449-- =======================================================================
446450
447451securityDefinitionsExample :: SecurityDefinitions
@@ -527,6 +531,18 @@ oAuth2SecurityDefinitionsExampleJSON = [aesonQQ|
527531}
528532|]
529533
534+oAuth2SecurityDefinitionsReadOpenApi :: OpenApi
535+oAuth2SecurityDefinitionsReadOpenApi =
536+ mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsReadExample
537+
538+oAuth2SecurityDefinitionsWriteOpenApi :: OpenApi
539+oAuth2SecurityDefinitionsWriteOpenApi =
540+ mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsWriteExample
541+
542+oAuth2SecurityDefinitionsOpenApi :: OpenApi
543+oAuth2SecurityDefinitionsOpenApi =
544+ mempty & components . securitySchemes .~ oAuth2SecurityDefinitionsExample
545+
530546-- =======================================================================
531547-- Swagger object
532548-- =======================================================================
test/SpecCommon.hs−8
@@ -8,14 +8,6 @@ import qualified Data.Vector as Vector
88
99import Test.Hspec
1010
11−isSubJSON :: Value -> Value -> Bool
12−isSubJSON Null _ = True
13−isSubJSON (Object x) (Object y) = HashMap.keys x == HashMap.keys i && F.and i
14− where
15− i = HashMap.intersectionWith isSubJSON x y
16−isSubJSON (Array xs) (Array ys) = Vector.length xs == Vector.length ys && F.and (Vector.zipWith isSubJSON xs ys)
17−isSubJSON x y = x == y
18−
1911(<=>) :: (Eq a, Show a, ToJSON a, FromJSON a, HasCallStack) => a -> Value -> Spec
2012x <=> js = do
2113 it "encodes correctly" $ do