git.iko.soy/typeable/openapi3
on master help sign in
24 files changed+1370−403
since splitsnapshots
unifiedsplit
.github/workflows/haskell-ci.yml+243 .travis.yml−149 CHANGELOG.md+47 cabal.haskell-ci+11 cabal.project+2 openapi3.cabal+18−17 src/Data/OpenApi.hs+141−20 src/Data/OpenApi/Aeson/Compat.hs+76 src/Data/OpenApi/Internal.hs+88−21 src/Data/OpenApi/Internal/AesonUtils.hs+11−10 src/Data/OpenApi/Internal/ParamSchema.hs+29−11 src/Data/OpenApi/Internal/Schema.hs+321−113 src/Data/OpenApi/Internal/Schema/Validation.hs+39−27 src/Data/OpenApi/Internal/Utils.hs+5 src/Data/OpenApi/Operation.hs+117−10 src/Data/OpenApi/Optics.hs+54−9 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.ymlnew file+243
@@ -0,0 +1,243 @@
1+# This GitHub workflow config has been generated by a script via
2+#
3+# haskell-ci 'github' 'cabal.project'
4+#
5+# To regenerate the script (for example after adjusting tested-with) run
6+#
7+# haskell-ci regenerate
8+#
9+# For more information, see https://github.com/haskell-CI/haskell-ci
10+#
11+# version: 0.14.3
12+#
13+# REGENDATA ("0.14.3",["github","cabal.project"])
14+#
15+name: Haskell-CI
16+on:
17+ push:
18+ branches:
19+ - master
20+ pull_request:
21+ branches:
22+ - master
23+jobs:
24+ linux:
25+ name: Haskell-CI - Linux - ${{ matrix.compiler }}
26+ runs-on: ubuntu-18.04
27+ timeout-minutes:
28+ 60
29+ container:
30+ image: buildpack-deps:focal
31+ continue-on-error: ${{ matrix.allow-failure }}
32+ strategy:
33+ matrix:
34+ include:
35+ - compiler: ghc-9.2.2
36+ compilerKind: ghc
37+ compilerVersion: 9.2.2
38+ setup-method: ghcup
39+ allow-failure: true
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
49+ allow-failure: false
50+ - compiler: ghc-8.8.4
51+ compilerKind: ghc
52+ compilerVersion: 8.8.4
53+ setup-method: hvr-ppa
54+ allow-failure: false
55+ - compiler: ghc-8.6.5
56+ compilerKind: ghc
57+ compilerVersion: 8.6.5
58+ setup-method: hvr-ppa
59+ allow-failure: false
60+ - compiler: ghc-8.4.4
61+ compilerKind: ghc
62+ compilerVersion: 8.4.4
63+ setup-method: hvr-ppa
64+ allow-failure: false
65+ fail-fast: false
66+ steps:
67+ - name: apt
68+ run: |
69+ apt-get update
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
86+ env:
87+ HCKIND: ${{ matrix.compilerKind }}
88+ HCNAME: ${{ matrix.compiler }}
89+ HCVER: ${{ matrix.compilerVersion }}
90+ - name: Set PATH and environment variables
91+ run: |
92+ echo "$HOME/.cabal/bin" >> $GITHUB_PATH
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+
111+ HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
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"
118+ env:
119+ HCKIND: ${{ matrix.compilerKind }}
120+ HCNAME: ${{ matrix.compiler }}
121+ HCVER: ${{ matrix.compilerVersion }}
122+ - name: env
123+ run: |
124+ env
125+ - name: write cabal config
126+ run: |
127+ mkdir -p $CABAL_DIR
128+ cat >> $CABAL_CONFIG <<EOF
129+ remote-build-reporting: anonymous
130+ write-ghc-environment-files: never
131+ remote-repo-cache: $CABAL_DIR/packages
132+ logs-dir: $CABAL_DIR/logs
133+ world-file: $CABAL_DIR/world
134+ extra-prog-path: $CABAL_DIR/bin
135+ symlink-bindir: $CABAL_DIR/bin
136+ installdir: $CABAL_DIR/bin
137+ build-summary: $CABAL_DIR/logs/build.log
138+ store-dir: $CABAL_DIR/store
139+ install-dirs user
140+ prefix: $CABAL_DIR
141+ repository hackage.haskell.org
142+ url: http://hackage.haskell.org/
143+ EOF
144+ cat >> $CABAL_CONFIG <<EOF
145+ program-default-options
146+ ghc-options: $GHCJOBS +RTS -M3G -RTS
147+ EOF
148+ cat $CABAL_CONFIG
149+ - name: versions
150+ run: |
151+ $HC --version || true
152+ $HC --print-project-git-commit-id || true
153+ $CABAL --version || true
154+ - name: update cabal index
155+ run: |
156+ $CABAL v2-update -v
157+ - name: install cabal-plan
158+ run: |
159+ mkdir -p $HOME/.cabal/bin
160+ curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.6.2.0/cabal-plan-0.6.2.0-x86_64-linux.xz > cabal-plan.xz
161+ echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c -
162+ xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
163+ rm -f cabal-plan.xz
164+ chmod a+x $HOME/.cabal/bin/cabal-plan
165+ cabal-plan --version
166+ - name: checkout
167+ uses: actions/checkout@v2
168+ with:
169+ path: source
170+ - name: initial cabal.project for sdist
171+ run: |
172+ touch cabal.project
173+ echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
174+ cat cabal.project
175+ - name: sdist
176+ run: |
177+ mkdir -p sdist
178+ $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
179+ - name: unpack
180+ run: |
181+ mkdir -p unpacked
182+ find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
183+ - name: generate cabal.project
184+ run: |
185+ PKGDIR_openapi3="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/openapi3-[0-9.]*')"
186+ echo "PKGDIR_openapi3=${PKGDIR_openapi3}" >> "$GITHUB_ENV"
187+ rm -f cabal.project cabal.project.local
188+ touch cabal.project
189+ touch cabal.project.local
190+ echo "packages: ${PKGDIR_openapi3}" >> cabal.project
191+ echo "package openapi3" >> cabal.project
192+ echo " ghc-options: -Werror=missing-methods" >> cabal.project
193+ cat >> cabal.project <<EOF
194+ allow-newer: http-media:base
195+ EOF
196+ $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(openapi3)$/; }' >> cabal.project.local
197+ cat cabal.project
198+ cat cabal.project.local
199+ - name: dump install plan
200+ run: |
201+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
202+ cabal-plan
203+ - name: cache
204+ uses: actions/cache@v2
205+ with:
206+ key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
207+ path: ~/.cabal/store
208+ restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
209+ - name: install dependencies
210+ run: |
211+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
212+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
213+ - name: build w/o tests
214+ run: |
215+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
216+ - name: build
217+ run: |
218+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
219+ - name: tests
220+ run: |
221+ $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
222+ - name: cabal check
223+ run: |
224+ cd ${PKGDIR_openapi3} || false
225+ ${CABAL} -vnormal check
226+ - name: haddock
227+ run: |
228+ $CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
229+ - name: unconstrained build
230+ run: |
231+ rm -f cabal.project.local
232+ $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
.travis.ymldeleted−149
@@ -1,149 +0,0 @@
1−# This Travis job script has been generated by a script via
2−#
3−# haskell-ci '--branches' 'master' '--haddock-jobs=>=8.4' '--output' '.travis.yml' 'openapi3.cabal'
4−#
5−# To regenerate the script (for example after adjusting tested-with) run
6−#
7−# haskell-ci regenerate
8−#
9−# For more information, see https://github.com/haskell-CI/haskell-ci
10−#
11−# version: 0.10.1
12−#
13−version: ~> 1.0
14−language: c
15−os: linux
16−dist: xenial
17−git:
18− # whether to recursively clone submodules
19− submodules: false
20−branches:
21− only:
22− - master
23−cache:
24− directories:
25− - $HOME/.cabal/packages
26− - $HOME/.cabal/store
27− - $HOME/.hlint
28−before_cache:
29− - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
30− # remove files that are regenerated by 'cabal update'
31− - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
32− - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
33− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
34− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
35− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
36− - rm -rfv $CABALHOME/packages/head.hackage
37−jobs:
38− include:
39− - compiler: ghc-8.10.1
40− 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.10.1","cabal-install-3.2"]}}
41− os: linux
42− - compiler: ghc-8.8.3
43− 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"]}}
44− os: linux
45− - compiler: ghc-8.6.5
46− 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"]}}
47− os: linux
48− - compiler: ghc-8.4.4
49− 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"]}}
50− os: linux
51−before_install:
52− - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//')
53− - WITHCOMPILER="-w $HC"
54− - HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//')
55− - HCPKG="$HC-pkg"
56− - unset CC
57− - CABAL=/opt/ghc/bin/cabal
58− - CABALHOME=$HOME/.cabal
59− - export PATH="$CABALHOME/bin:$PATH"
60− - TOP=$(pwd)
61− - "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')"
62− - echo $HCNUMVER
63− - CABAL="$CABAL -vnormal+nowrap"
64− - set -o pipefail
65− - TEST=--enable-tests
66− - BENCH=--enable-benchmarks
67− - HEADHACKAGE=false
68− - rm -f $CABALHOME/config
69− - |
70− echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config
71− echo "remote-build-reporting: anonymous" >> $CABALHOME/config
72− echo "write-ghc-environment-files: always" >> $CABALHOME/config
73− echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config
74− echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config
75− echo "world-file: $CABALHOME/world" >> $CABALHOME/config
76− echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config
77− echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config
78− echo "installdir: $CABALHOME/bin" >> $CABALHOME/config
79− echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config
80− echo "store-dir: $CABALHOME/store" >> $CABALHOME/config
81− echo "install-dirs user" >> $CABALHOME/config
82− echo " prefix: $CABALHOME" >> $CABALHOME/config
83− echo "repository hackage.haskell.org" >> $CABALHOME/config
84− echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
85−install:
86− - ${CABAL} --version
87− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
88− - |
89− echo "program-default-options" >> $CABALHOME/config
90− echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
91− - cat $CABALHOME/config
92− - rm -fv cabal.project cabal.project.local cabal.project.freeze
93− - travis_retry ${CABAL} v2-update -v
94− # Generate cabal.project
95− - rm -rf cabal.project cabal.project.local cabal.project.freeze
96− - touch cabal.project
97− - |
98− echo "packages: ." >> cabal.project
99− - echo 'package openapi3' >> cabal.project
100− - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project"
101− - |
102− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(openapi3)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
103− - cat cabal.project || true
104− - cat cabal.project.local || true
105− - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
106− - ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH}
107− - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
108− - rm cabal.project.freeze
109− - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all
110− - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all
111−script:
112− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
113− # Packaging...
114− - ${CABAL} v2-sdist all
115− # Unpacking...
116− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
117− - cd ${DISTDIR} || false
118− - find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \;
119− - find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
120− - PKGDIR_openapi3="$(find . -maxdepth 1 -type d -regex '.*/openapi3-[0-9.]*')"
121− # Generate cabal.project
122− - rm -rf cabal.project cabal.project.local cabal.project.freeze
123− - touch cabal.project
124− - |
125− echo "packages: ${PKGDIR_openapi3}" >> cabal.project
126− - echo 'package openapi3' >> cabal.project
127− - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project"
128− - |
129− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(openapi3)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
130− - cat cabal.project || true
131− - cat cabal.project.local || true
132− # Building...
133− # this builds all libraries and executables (without tests/benchmarks)
134− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
135− # Building with tests and benchmarks...
136− # build & run tests, build benchmarks
137− - ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all
138− # Testing...
139− - ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all
140− # cabal check...
141− - (cd ${PKGDIR_openapi3} && ${CABAL} -vnormal check)
142− # haddock...
143− - ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all
144− # Building without installed constraints for packages in global-db...
145− - rm -f cabal.project.local
146− - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all
147−
148−# REGENDATA ("0.10.1",["--branches","master","--haddock-jobs=>=8.4","--output",".travis.yml","openapi3.cabal"])
149−# EOF
CHANGELOG.md+47
@@ -1,3 +1,50 @@
1+Unreleased
2+----------
3+
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)).
30+
31+3.0.2.0
32+-------
33+
34+- Fix definition for HTTP security scheme (see [#10](https://github.com/biocad/openapi3/pull/10));
35+- Fix error message in `exclusiveMaximum` / `exclusiveMinimum` validation (see
36+ [#15](https://github.com/biocad/openapi3/pull/15));
37+
38+3.0.1.0
39+-------
40+
41+- Support polymorphic types in schema generator.
42+
43+3.0.0
44+-----
45+
46+- Switch to OpenAPI 3.0 specification.
47+
1482.6.1
249-----
350
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
cabal.project+2
@@ -1,2 +1,4 @@
11packages: .
22tests: true
3+
4+allow-newer: http-media:base
openapi3.cabal+18−17
@@ -1,7 +1,6 @@
11cabal-version: >=1.10
22name: openapi3
3−version: 3.0.1.0
4−x-revision: 1
3+version: 3.2.2
54
65synopsis: OpenAPI 3.0 data model
76category: Web, Swagger, OpenApi
@@ -26,8 +25,10 @@ extra-source-files:
2625tested-with:
2726 GHC ==8.4.4
2827 || ==8.6.5
29− || ==8.8.3
30− || ==8.10.1
28+ || ==8.8.4
29+ || ==8.10.7
30+ || ==9.0.2
31+ || ==9.2.2
3132
3233custom-setup
3334 setup-depends:
@@ -56,33 +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.15
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.17
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− -- <1.3.1 is a temporary measure, until we fix doctests not to depend on key order
81− -- see https://github.com/haskell/aeson/issues/837
82− , hashable >=1.2.7.0 && <1.3.1
83+ , hashable >=1.2.7.0 && <1.5
8384 , http-media >=0.8.0.0 && <0.9
8485 , insert-ordered-containers >=0.2.3 && <0.3
85− , lens >=4.16.1 && <5.1
86+ , lens >=4.16.1 && <5.2
8687 , network >=2.6.3.5 && <3.2
8788 , optics-core >=0.2 && <0.5
8889 , optics-th >=0.2 && <0.5
@@ -121,14 +122,14 @@ test-suite spec
121122
122123 -- test-suite only dependencies
123124 build-depends:
124− hspec >=2.5.5 && <2.8
125+ hspec >=2.5.5 && <2.10
125126 , HUnit >=1.6.0.0 && <1.7
126127 , quickcheck-instances >=0.3.19 && <0.14
127128 , utf8-string >=1.0.1.1 && <1.1
128129
129130 -- https://github.com/haskell/cabal/issues/3708
130131 build-tool-depends:
131− hspec-discover:hspec-discover >=2.5.5 && <2.8
132+ hspec-discover:hspec-discover >=2.5.5 && <2.10
132133
133134 other-modules:
134135 SpecCommon
src/Data/OpenApi.hs+141−20
@@ -91,6 +91,7 @@ module Data.OpenApi (
9191 -- ** Security
9292 SecurityScheme(..),
9393 SecuritySchemeType(..),
94+ HttpSchemeType(..),
9495 SecurityDefinitions(..),
9596 SecurityRequirement(..),
9697
@@ -136,6 +137,7 @@ import Data.OpenApi.Internal
136137-- >>> import Data.Proxy
137138-- >>> import GHC.Generics
138139-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL
140+-- >>> import Data.OpenApi.Internal.Utils
139141-- >>> :set -XDeriveGeneric
140142-- >>> :set -XOverloadedStrings
141143-- >>> :set -XOverloadedLists
@@ -152,8 +154,16 @@ import Data.OpenApi.Internal
152154--
153155-- In this library you can use @'mempty'@ for a default/empty value. For instance:
154156--
155−-- >>> BSL.putStrLn $ encode (mempty :: OpenApi)
156−-- {"openapi":"3.0.0","info":{"version":"","title":""},"components":{}}
157+-- >>> BSL.putStrLn $ encodePretty (mempty :: OpenApi)
158+-- {
159+-- "components": {},
160+-- "info": {
161+-- "title": "",
162+-- "version": ""
163+-- },
164+-- "openapi": "3.0.0",
165+-- "paths": {}
166+-- }
157167--
158168-- As you can see some spec properties (e.g. @"version"@) are there even when the spec is empty.
159169-- That is because these properties are actually required ones.
@@ -161,13 +171,20 @@ import Data.OpenApi.Internal
161171-- You /should/ always override the default (empty) value for these properties,
162172-- although it is not strictly necessary:
163173--
164−-- >>> BSL.putStrLn $ encode mempty { _infoTitle = "Todo API", _infoVersion = "1.0" }
165−-- {"version":"1.0","title":"Todo API"}
174+-- >>> BSL.putStrLn $ encodePretty mempty { _infoTitle = "Todo API", _infoVersion = "1.0" }
175+-- {
176+-- "title": "Todo API",
177+-- "version": "1.0"
178+-- }
166179--
167180-- You can merge two values using @'mappend'@ or its infix version @('<>')@:
168181--
169−-- >>> BSL.putStrLn $ encode $ mempty { _infoTitle = "Todo API" } <> mempty { _infoVersion = "1.0" }
170−-- {"version":"1.0","title":"Todo API"}
182+-- >>> BSL.putStrLn $ encodePretty $ mempty { _infoTitle = "Todo API" } <> mempty { _infoVersion = "1.0" }
183+-- {
184+-- "title": "Todo API",
185+-- "version": "1.0"
186+-- }
187+
171188--
172189-- This can be useful for combining specifications of endpoints into a whole API specification:
173190--
@@ -193,14 +210,48 @@ import Data.OpenApi.Internal
193210-- make it fairly simple to construct/modify any part of the specification:
194211--
195212-- >>> :{
196−-- BSL.putStrLn $ encode $ (mempty :: OpenApi)
213+-- BSL.putStrLn $ encodePretty $ (mempty :: OpenApi)
197214-- & components . schemas .~ [ ("User", mempty & type_ ?~ OpenApiString) ]
198215-- & paths .~
199216-- [ ("/user", mempty & get ?~ (mempty
200217-- & at 200 ?~ ("OK" & _Inline.content.at "application/json" ?~ (mempty & schema ?~ Ref (Reference "User")))
201218-- & at 404 ?~ "User info not found")) ]
202219-- :}
203−-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/user":{"get":{"responses":{"404":{"description":"User info not found"},"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"}}}}},"components":{"schemas":{"User":{"type":"string"}}}}
220+-- {
221+-- "components": {
222+-- "schemas": {
223+-- "User": {
224+-- "type": "string"
225+-- }
226+-- }
227+-- },
228+-- "info": {
229+-- "title": "",
230+-- "version": ""
231+-- },
232+-- "openapi": "3.0.0",
233+-- "paths": {
234+-- "/user": {
235+-- "get": {
236+-- "responses": {
237+-- "200": {
238+-- "content": {
239+-- "application/json": {
240+-- "schema": {
241+-- "$ref": "#/components/schemas/User"
242+-- }
243+-- }
244+-- },
245+-- "description": "OK"
246+-- },
247+-- "404": {
248+-- "description": "User info not found"
249+-- }
250+-- }
251+-- }
252+-- }
253+-- }
254+-- }
204255--
205256-- In the snippet above we declare an API with a single path @/user@. This path provides method @GET@
206257-- which produces @application/json@ output. It should respond with code @200@ and body specified
@@ -212,23 +263,34 @@ import Data.OpenApi.Internal
212263-- common field is @'description'@. Many components of a Swagger specification
213264-- can have descriptions, and you can use the same name for them:
214265--
215−-- >>> BSL.putStrLn $ encode $ (mempty :: Response) & description .~ "No content"
216−-- {"description":"No content"}
266+-- >>> BSL.putStrLn $ encodePretty $ (mempty :: Response) & description .~ "No content"
267+-- {
268+-- "description": "No content"
269+-- }
217270-- >>> :{
218−-- BSL.putStrLn $ encode $ (mempty :: Schema)
271+-- BSL.putStrLn $ encodePretty $ (mempty :: Schema)
219272-- & type_ ?~ OpenApiBoolean
220273-- & description ?~ "To be or not to be"
221274-- :}
222−-- {"type":"boolean","description":"To be or not to be"}
275+-- {
276+-- "description": "To be or not to be",
277+-- "type": "boolean"
278+-- }
223279--
224280-- Additionally, to simplify working with @'Response'@, both @'Operation'@ and @'Responses'@
225281-- have direct access to it via @'at' code@. Example:
226282--
227283-- >>> :{
228−-- BSL.putStrLn $ encode $ (mempty :: Operation)
284+-- BSL.putStrLn $ encodePretty $ (mempty :: Operation)
229285-- & at 404 ?~ "Not found"
230286-- :}
231−-- {"responses":{"404":{"description":"Not found"}}}
287+-- {
288+-- "responses": {
289+-- "404": {
290+-- "description": "Not found"
291+-- }
292+-- }
293+-- }
232294--
233295-- You might've noticed that @'type_'@ has an extra underscore in its name
234296-- compared to, say, @'description'@ field accessor.
@@ -275,10 +337,27 @@ import Data.OpenApi.Internal
275337-- >>> data Person = Person { name :: String, age :: Integer } deriving Generic
276338-- >>> instance ToJSON Person
277339-- >>> instance ToSchema Person
278−-- >>> BSL.putStrLn $ encode (Person "David" 28)
279−-- {"age":28,"name":"David"}
280−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Person)
281−-- {"required":["name","age"],"type":"object","properties":{"age":{"type":"integer"},"name":{"type":"string"}}}
340+-- >>> BSL.putStrLn $ encodePretty (Person "David" 28)
341+-- {
342+-- "age": 28,
343+-- "name": "David"
344+-- }
345+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy Person)
346+-- {
347+-- "properties": {
348+-- "age": {
349+-- "type": "integer"
350+-- },
351+-- "name": {
352+-- "type": "string"
353+-- }
354+-- },
355+-- "required": [
356+-- "name",
357+-- "age"
358+-- ],
359+-- "type": "object"
360+-- }
282361--
283362-- This package implements OpenAPI 3.0 spec, which supports @oneOf@ in schemas, allowing any sum types
284363-- to be faithfully represented. All sum encodings supported by @aeson@ are supported here as well, with
@@ -289,8 +368,50 @@ import Data.OpenApi.Internal
289368-- >>> data Error = ErrorNoUser { userId :: Int } | ErrorAccessDenied { requiredPermission :: String } deriving Generic
290369-- >>> instance ToJSON Error
291370-- >>> instance ToSchema Error
292−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Error)
293−-- {"oneOf":[{"required":["userId","tag"],"type":"object","properties":{"tag":{"type":"string","enum":["ErrorNoUser"]},"userId":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}}},{"required":["requiredPermission","tag"],"type":"object","properties":{"tag":{"type":"string","enum":["ErrorAccessDenied"]},"requiredPermission":{"type":"string"}}}],"type":"object"}
371+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy Error)
372+-- {
373+-- "oneOf": [
374+-- {
375+-- "properties": {
376+-- "tag": {
377+-- "enum": [
378+-- "ErrorNoUser"
379+-- ],
380+-- "type": "string"
381+-- },
382+-- "userId": {
383+-- "maximum": 9223372036854775807,
384+-- "minimum": -9223372036854775808,
385+-- "type": "integer"
386+-- }
387+-- },
388+-- "required": [
389+-- "userId",
390+-- "tag"
391+-- ],
392+-- "type": "object"
393+-- },
394+-- {
395+-- "properties": {
396+-- "requiredPermission": {
397+-- "type": "string"
398+-- },
399+-- "tag": {
400+-- "enum": [
401+-- "ErrorAccessDenied"
402+-- ],
403+-- "type": "string"
404+-- }
405+-- },
406+-- "required": [
407+-- "requiredPermission",
408+-- "tag"
409+-- ],
410+-- "type": "object"
411+-- }
412+-- ],
413+-- "type": "object"
414+-- }
294415
295416-- $manipulation
296417-- Sometimes you have to work with an imported or generated @'Swagger'@.
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+88−21
@@ -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,21 +49,19 @@ 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
6262-- >>> import Data.Aeson
63+-- >>> import Data.ByteString.Lazy.Char8 as BSL
64+-- >>> import Data.OpenApi.Internal.Utils
6365
6466-- | A list of definitions that can be used in references.
6567type Definitions = InsOrdHashMap Text
@@ -194,7 +196,7 @@ data Components = Components
194196 , _componentsExamples :: Definitions Example
195197 , _componentsRequestBodies :: Definitions RequestBody
196198 , _componentsHeaders :: Definitions Header
197− , _componentsSecuritySchemes :: Definitions SecurityScheme
199+ , _componentsSecuritySchemes :: SecurityDefinitions
198200 , _componentsLinks :: Definitions Link
199201 , _componentsCallbacks :: Definitions Callback
200202 } deriving (Eq, Show, Generic, Data, Typeable)
@@ -335,7 +337,7 @@ data RequestBody = RequestBody
335337 -- | The content of the request body.
336338 -- The key is a media type or media type range and the value describes it.
337339 -- For requests that match multiple keys, only the most specific key is applicable.
338− -- e.g. @text/plain@ overrides @text/*@
340+ -- e.g. @text/plain@ overrides @text/\*@
339341 , _requestBodyContent :: InsOrdHashMap MediaType MediaTypeObject
340342
341343 -- | Determines if the request body is required in the request.
@@ -392,7 +394,7 @@ data Encoding = Encoding
392394 -- for other primitive types – @text/plain@; for object - @application/json@;
393395 -- for array – the default is defined based on the inner type.
394396 -- The value can be a specific media type (e.g. @application/json@),
395− -- 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.
396398 _encodingContentType :: Maybe MediaType
397399
398400 -- | A map allowing additional information to be provided as headers,
@@ -732,7 +734,7 @@ data Response = Response
732734 -- | A map containing descriptions of potential response payloads.
733735 -- The key is a media type or media type range and the value describes it.
734736 -- For responses that match multiple keys, only the most specific key is applicable.
735− -- e.g. @text/plain@ overrides @text/*@.
737+ -- e.g. @text/plain@ overrides @text/\*@.
736738 , _responseContent :: InsOrdHashMap MediaType MediaTypeObject
737739
738740 -- | Maps a header name to its definition.
@@ -838,8 +840,50 @@ data OAuth2Flows = OAuth2Flows
838840 , _oAuth2FlowsAuthorizationCode :: Maybe (OAuth2Flow OAuth2AuthorizationCodeFlow)
839841 } deriving (Eq, Show, Generic, Data, Typeable)
840842
843+type BearerFormat = Text
844+
845+data HttpSchemeType
846+ = HttpSchemeBearer (Maybe BearerFormat)
847+ | HttpSchemeBasic
848+ | HttpSchemeCustom Text
849+ deriving (Eq, Show, Generic, Data, Typeable)
850+
851+-- |
852+--
853+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeBearer Nothing))
854+-- {
855+-- "scheme": "bearer",
856+-- "type": "http"
857+-- }
858+--
859+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeBearer (Just "jwt")))
860+-- {
861+-- "bearerFormat": "jwt",
862+-- "scheme": "bearer",
863+-- "type": "http"
864+-- }
865+--
866+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp HttpSchemeBasic)
867+-- {
868+-- "scheme": "basic",
869+-- "type": "http"
870+-- }
871+--
872+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeCustom "CANARY"))
873+-- {
874+-- "scheme": "CANARY",
875+-- "type": "http"
876+-- }
877+--
878+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeApiKey (ApiKeyParams "id" ApiKeyCookie))
879+-- {
880+-- "in": "cookie",
881+-- "name": "id",
882+-- "type": "apiKey"
883+-- }
884+--
841885data SecuritySchemeType
842− = SecuritySchemeHttp
886+ = SecuritySchemeHttp HttpSchemeType
843887 | SecuritySchemeApiKey ApiKeyParams
844888 | SecuritySchemeOAuth2 OAuth2Flows
845889 | SecuritySchemeOpenIdConnect URL
@@ -1081,6 +1125,7 @@ instance SwaggerMonoid Response
10811125instance SwaggerMonoid ExternalDocs
10821126instance SwaggerMonoid Operation
10831127instance (Eq a, Hashable a) => SwaggerMonoid (InsOrdHashSet a)
1128+instance SwaggerMonoid SecurityDefinitions
10841129
10851130instance SwaggerMonoid MimeList
10861131deriving instance SwaggerMonoid URL
@@ -1229,8 +1274,19 @@ instance ToJSON OAuth2Flows where
12291274 toEncoding = sopSwaggerGenericToEncoding
12301275
12311276instance ToJSON SecuritySchemeType where
1232− toJSON SecuritySchemeHttp
1233− = object [ "type" .= ("http" :: Text) ]
1277+ toJSON (SecuritySchemeHttp ty) = case ty of
1278+ HttpSchemeBearer mFmt ->
1279+ object $ [ "type" .= ("http" :: Text)
1280+ , "scheme" .= ("bearer" :: Text)
1281+ ] <> maybe [] (\t -> ["bearerFormat" .= t]) mFmt
1282+ HttpSchemeBasic ->
1283+ object [ "type" .= ("http" :: Text)
1284+ , "scheme" .= ("basic" :: Text)
1285+ ]
1286+ HttpSchemeCustom t ->
1287+ object [ "type" .= ("http" :: Text)
1288+ , "scheme" .= t
1289+ ]
12341290 toJSON (SecuritySchemeApiKey params)
12351291 = toJSON params
12361292 <+> object [ "type" .= ("apiKey" :: Text) ]
@@ -1269,8 +1325,12 @@ instance ToJSON Header where
12691325-- | As for nullary schema for 0-arity type constructors, see
12701326-- <https://github.com/GetShopTV/swagger2/issues/167>.
12711327--
1272−-- >>> encode (OpenApiItemsArray [])
1273−-- "{\"example\":[],\"items\":{},\"maxItems\":0}"
1328+-- >>> BSL.putStrLn $ encodePretty (OpenApiItemsArray [])
1329+-- {
1330+-- "example": [],
1331+-- "items": {},
1332+-- "maxItems": 0
1333+-- }
12741334--
12751335instance ToJSON OpenApiItems where
12761336 toJSON (OpenApiItemsObject x) = object [ "items" .= x ]
@@ -1379,7 +1439,12 @@ instance FromJSON SecuritySchemeType where
13791439 parseJSON js@(Object o) = do
13801440 (t :: Text) <- o .: "type"
13811441 case t of
1382− "http" -> pure SecuritySchemeHttp
1442+ "http" -> do
1443+ scheme <- o .: "scheme"
1444+ SecuritySchemeHttp <$> case scheme of
1445+ "bearer" -> HttpSchemeBearer <$> (o .:! "bearerFormat")
1446+ "basic" -> pure HttpSchemeBasic
1447+ t -> pure $ HttpSchemeCustom t
13831448 "apiKey" -> SecuritySchemeApiKey <$> parseJSON js
13841449 "oauth2" -> SecuritySchemeOAuth2 <$> (o .: "flows")
13851450 "openIdConnect" -> SecuritySchemeOpenIdConnect <$> (o .: "openIdConnectUrl")
@@ -1427,7 +1492,7 @@ instance FromJSON Param where
14271492instance FromJSON Responses where
14281493 parseJSON (Object o) = Responses
14291494 <$> o .:? "default"
1430− <*> parseJSON (Object (HashMap.delete "default" o))
1495+ <*> parseJSON (Object (deleteKey "default" o))
14311496 parseJSON _ = empty
14321497
14331498instance FromJSON Example where
@@ -1550,3 +1615,5 @@ instance AesonDefaultValue MimeList where defaultValue = Just mempty
15501615instance AesonDefaultValue Info
15511616instance AesonDefaultValue ParamLocation
15521617instance 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+29−11
@@ -46,7 +46,7 @@ import Data.OpenApi.Lens
4646import Data.OpenApi.SchemaOptions
4747
4848import qualified Data.ByteString as BS
49−import qualified Data.ByteString.Lazy as BSL
49+import qualified Data.ByteString.Lazy.Char8 as BSL
5050import GHC.TypeLits (TypeError, ErrorMessage(..))
5151
5252-- | Default schema for binary data (any sequence of octets).
@@ -112,8 +112,10 @@ passwordSchema = mempty
112112class ToParamSchema a where
113113 -- | Convert a type into a plain parameter schema.
114114 --
115− -- >>> encode $ toParamSchema (Proxy :: Proxy Integer)
116− -- "{\"type\":\"integer\"}"
115+ -- >>> BSL.putStrLn $ encodePretty $ toParamSchema (Proxy :: Proxy Integer)
116+ -- {
117+ -- "type": "integer"
118+ -- }
117119 toParamSchema :: Proxy a -> Schema
118120 default toParamSchema :: (Generic a, GToParamSchema (Rep a)) => Proxy a -> Schema
119121 toParamSchema = genericToParamSchema defaultSchemaOptions
@@ -155,8 +157,12 @@ instance ToParamSchema Word64 where
155157
156158-- | Default plain schema for @'Bounded'@, @'Integral'@ types.
157159--
158−-- >>> encode $ toParamSchemaBoundedIntegral (Proxy :: Proxy Int8)
159−-- "{\"maximum\":127,\"minimum\":-128,\"type\":\"integer\"}"
160+-- >>> BSL.putStrLn $ encodePretty $ toParamSchemaBoundedIntegral (Proxy :: Proxy Int8)
161+-- {
162+-- "maximum": 127,
163+-- "minimum": -128,
164+-- "type": "integer"
165+-- }
160166toParamSchemaBoundedIntegral :: forall a t. (Bounded a, Integral a) => Proxy a -> Schema
161167toParamSchemaBoundedIntegral _ = mempty
162168 & type_ ?~ OpenApiInteger
@@ -210,9 +216,9 @@ instance ToParamSchema LocalTime where
210216
211217-- |
212218-- >>> toParamSchema (Proxy :: Proxy ZonedTime) ^. format
213−-- Just "yyyy-mm-ddThh:MM:ss+hhMM"
219+-- Just "date-time"
214220instance ToParamSchema ZonedTime where
215− toParamSchema _ = timeParamSchema "yyyy-mm-ddThh:MM:ss+hhMM"
221+ toParamSchema _ = timeParamSchema "date-time"
216222
217223-- |
218224-- >>> toParamSchema (Proxy :: Proxy UTCTime) ^. format
@@ -275,8 +281,13 @@ instance ToParamSchema a => ToParamSchema (HashSet a) where
275281 toParamSchema _ = toParamSchema (Proxy :: Proxy (Set a))
276282
277283-- |
278−-- >>> encode $ toParamSchema (Proxy :: Proxy ())
279−-- "{\"type\":\"string\",\"enum\":[\"_\"]}"
284+-- >>> BSL.putStrLn $ encodePretty $ toParamSchema (Proxy :: Proxy ())
285+-- {
286+-- "enum": [
287+-- "_"
288+-- ],
289+-- "type": "string"
290+-- }
280291instance ToParamSchema () where
281292 toParamSchema _ = mempty
282293 & type_ ?~ OpenApiString
@@ -291,8 +302,14 @@ instance ToParamSchema UUID where
291302--
292303-- >>> :set -XDeriveGeneric
293304-- >>> data Color = Red | Blue deriving Generic
294−-- >>> encode $ genericToParamSchema defaultSchemaOptions (Proxy :: Proxy Color)
295−-- "{\"type\":\"string\",\"enum\":[\"Red\",\"Blue\"]}"
305+-- >>> BSL.putStrLn $ encodePretty $ genericToParamSchema defaultSchemaOptions (Proxy :: Proxy Color)
306+-- {
307+-- "enum": [
308+-- "Red",
309+-- "Blue"
310+-- ],
311+-- "type": "string"
312+-- }
296313genericToParamSchema :: forall a t. (Generic a, GToParamSchema (Rep a)) => SchemaOptions -> Proxy a -> Schema
297314genericToParamSchema opts _ = gtoParamSchema opts (Proxy :: Proxy (Rep a)) mempty
298315
@@ -334,3 +351,4 @@ data Proxy3 a b c = Proxy3
334351
335352-- $setup
336353-- >>> import Data.Aeson (encode)
354+-- >>> import Data.OpenApi.Internal.Utils
src/Data/OpenApi/Internal/Schema.hs+321−113
@@ -42,6 +42,7 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
4242import Data.Int
4343import Data.IntSet (IntSet)
4444import Data.IntMap (IntMap)
45+import Data.List (sort)
4546import Data.List.NonEmpty.Compat (NonEmpty)
4647import Data.Map (Map)
4748import Data.Maybe (fromMaybe)
@@ -63,13 +64,14 @@ import GHC.Generics
6364import qualified Data.UUID.Types as UUID
6465import Type.Reflection (Typeable, typeRep)
6566
66−import Data.OpenApi.Declare
67−import Data.OpenApi.Internal
68−import Data.OpenApi.Internal.ParamSchema (ToParamSchema(..))
69−import Data.OpenApi.Lens hiding (name, schema)
70−import qualified Data.OpenApi.Lens as Swagger
71−import Data.OpenApi.SchemaOptions
72−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
7375
7476import qualified Data.ByteString as BS
7577import qualified Data.ByteString.Lazy.Char8 as BSL
@@ -134,7 +136,7 @@ rename name (NamedSchema _ schema) = NamedSchema name schema
134136--
135137-- instance ToSchema Coord
136138-- @
137−class ToSchema a where
139+class Typeable a => ToSchema a where
138140 -- | Convert a type into an optionally named schema
139141 -- together with all used definitions.
140142 -- Note that the schema itself is included in definitions
@@ -156,13 +158,19 @@ declareSchema = fmap _namedSchemaSchema . declareNamedSchema
156158--
157159-- >>> toNamedSchema (Proxy :: Proxy String) ^. name
158160-- Nothing
159−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy String) ^. schema)
160−-- {"type":"string"}
161+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy String) ^. schema)
162+-- {
163+-- "type": "string"
164+-- }
161165--
162166-- >>> toNamedSchema (Proxy :: Proxy Day) ^. name
163167-- Just "Day"
164−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy Day) ^. schema)
165−-- {"example":"2016-07-22","format":"date","type":"string"}
168+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy Day) ^. schema)
169+-- {
170+-- "example": "2016-07-22",
171+-- "format": "date",
172+-- "type": "string"
173+-- }
166174toNamedSchema :: ToSchema a => Proxy a -> NamedSchema
167175toNamedSchema = undeclare . declareNamedSchema
168176
@@ -178,22 +186,35 @@ schemaName = _namedSchemaName . toNamedSchema
178186
179187-- | Convert a type into a schema.
180188--
181−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Int8)
182−-- {"maximum":127,"minimum":-128,"type":"integer"}
189+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy Int8)
190+-- {
191+-- "maximum": 127,
192+-- "minimum": -128,
193+-- "type": "integer"
194+-- }
183195--
184−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy [Day])
185−-- {"items":{"$ref":"#/components/schemas/Day"},"type":"array"}
196+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy [Day])
197+-- {
198+-- "items": {
199+-- "$ref": "#/components/schemas/Day"
200+-- },
201+-- "type": "array"
202+-- }
186203toSchema :: ToSchema a => Proxy a -> Schema
187204toSchema = _namedSchemaSchema . toNamedSchema
188205
189206-- | Convert a type into a referenced schema if possible.
190207-- Only named schemas can be referenced, nameless schemas are inlined.
191208--
192−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Integer)
193−-- {"type":"integer"}
209+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Integer)
210+-- {
211+-- "type": "integer"
212+-- }
194213--
195−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Day)
196−-- {"$ref":"#/components/schemas/Day"}
214+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Day)
215+-- {
216+-- "$ref": "#/components/schemas/Day"
217+-- }
197218toSchemaRef :: ToSchema a => Proxy a -> Referenced Schema
198219toSchemaRef = undeclare . declareSchemaRef
199220
@@ -261,8 +282,15 @@ inlineAllSchemas = inlineSchemasWhen (const True)
261282
262283-- | Convert a type into a schema without references.
263284--
264−-- >>> BSL.putStrLn $ encode $ toInlinedSchema (Proxy :: Proxy [Day])
265−-- {"items":{"example":"2016-07-22","format":"date","type":"string"},"type":"array"}
285+-- >>> BSL.putStrLn $ encodePretty $ toInlinedSchema (Proxy :: Proxy [Day])
286+-- {
287+-- "items": {
288+-- "example": "2016-07-22",
289+-- "format": "date",
290+-- "type": "string"
291+-- },
292+-- "type": "array"
293+-- }
266294--
267295-- __WARNING:__ @'toInlinedSchema'@ will produce infinite schema
268296-- when inlining recursive schemas.
@@ -295,19 +323,64 @@ inlineNonRecursiveSchemas defs = inlineSchemasWhen nonRecursive defs
295323-- | Make an unrestrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
296324-- Produced schema can be used for further refinement.
297325--
298−-- >>> BSL.putStrLn $ encode $ sketchSchema "hello"
299−-- {"example":"hello","type":"string"}
326+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema "hello"
327+-- {
328+-- "example": "hello",
329+-- "type": "string"
330+-- }
300331--
301−-- >>> BSL.putStrLn $ encode $ sketchSchema (1, 2, 3)
302−-- {"example":[1,2,3],"items":{"type":"number"},"type":"array"}
332+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (1, 2, 3)
333+-- {
334+-- "example": [
335+-- 1,
336+-- 2,
337+-- 3
338+-- ],
339+-- "items": {
340+-- "type": "number"
341+-- },
342+-- "type": "array"
343+-- }
303344--
304−-- >>> BSL.putStrLn $ encode $ sketchSchema ("Jack", 25)
305−-- {"example":["Jack",25],"items":[{"type":"string"},{"type":"number"}],"type":"array"}
345+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema ("Jack", 25)
346+-- {
347+-- "example": [
348+-- "Jack",
349+-- 25
350+-- ],
351+-- "items": [
352+-- {
353+-- "type": "string"
354+-- },
355+-- {
356+-- "type": "number"
357+-- }
358+-- ],
359+-- "type": "array"
360+-- }
306361--
307362-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
308363-- >>> instance ToJSON Person
309−-- >>> BSL.putStrLn $ encode $ sketchSchema (Person "Jack" 25)
310−-- {"example":{"age":25,"name":"Jack"},"required":["age","name"],"type":"object","properties":{"age":{"type":"number"},"name":{"type":"string"}}}
364+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (Person "Jack" 25)
365+-- {
366+-- "example": {
367+-- "age": 25,
368+-- "name": "Jack"
369+-- },
370+-- "properties": {
371+-- "age": {
372+-- "type": "number"
373+-- },
374+-- "name": {
375+-- "type": "string"
376+-- }
377+-- },
378+-- "required": [
379+-- "age",
380+-- "name"
381+-- ],
382+-- "type": "object"
383+-- }
311384sketchSchema :: ToJSON a => a -> Schema
312385sketchSchema = sketch . toJSON
313386 where
@@ -333,25 +406,139 @@ sketchSchema = sketch . toJSON
333406 _ -> Nothing
334407 go (Object o) = mempty
335408 & type_ ?~ OpenApiObject
336− & required .~ HashMap.keys o
337− & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
409+ & required .~ sort (objectKeys o)
410+ & properties .~ fmap (Inline . go) (toInsOrdHashMap o)
338411
339412-- | Make a restrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
340413-- Produced schema uses as much constraints as possible.
341414--
342−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema "hello"
343−-- {"maxLength":5,"pattern":"hello","minLength":5,"type":"string","enum":["hello"]}
415+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema "hello"
416+-- {
417+-- "enum": [
418+-- "hello"
419+-- ],
420+-- "maxLength": 5,
421+-- "minLength": 5,
422+-- "pattern": "hello",
423+-- "type": "string"
424+-- }
344425--
345−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema (1, 2, 3)
346−-- {"minItems":3,"uniqueItems":true,"items":[{"maximum":1,"minimum":1,"multipleOf":1,"type":"number","enum":[1]},{"maximum":2,"minimum":2,"multipleOf":2,"type":"number","enum":[2]},{"maximum":3,"minimum":3,"multipleOf":3,"type":"number","enum":[3]}],"maxItems":3,"type":"array","enum":[[1,2,3]]}
426+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (1, 2, 3)
427+-- {
428+-- "enum": [
429+-- [
430+-- 1,
431+-- 2,
432+-- 3
433+-- ]
434+-- ],
435+-- "items": [
436+-- {
437+-- "enum": [
438+-- 1
439+-- ],
440+-- "maximum": 1,
441+-- "minimum": 1,
442+-- "multipleOf": 1,
443+-- "type": "number"
444+-- },
445+-- {
446+-- "enum": [
447+-- 2
448+-- ],
449+-- "maximum": 2,
450+-- "minimum": 2,
451+-- "multipleOf": 2,
452+-- "type": "number"
453+-- },
454+-- {
455+-- "enum": [
456+-- 3
457+-- ],
458+-- "maximum": 3,
459+-- "minimum": 3,
460+-- "multipleOf": 3,
461+-- "type": "number"
462+-- }
463+-- ],
464+-- "maxItems": 3,
465+-- "minItems": 3,
466+-- "type": "array",
467+-- "uniqueItems": true
468+-- }
347469--
348−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema ("Jack", 25)
349−-- {"minItems":2,"uniqueItems":true,"items":[{"maxLength":4,"pattern":"Jack","minLength":4,"type":"string","enum":["Jack"]},{"maximum":25,"minimum":25,"multipleOf":25,"type":"number","enum":[25]}],"maxItems":2,"type":"array","enum":[["Jack",25]]}
470+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema ("Jack", 25)
471+-- {
472+-- "enum": [
473+-- [
474+-- "Jack",
475+-- 25
476+-- ]
477+-- ],
478+-- "items": [
479+-- {
480+-- "enum": [
481+-- "Jack"
482+-- ],
483+-- "maxLength": 4,
484+-- "minLength": 4,
485+-- "pattern": "Jack",
486+-- "type": "string"
487+-- },
488+-- {
489+-- "enum": [
490+-- 25
491+-- ],
492+-- "maximum": 25,
493+-- "minimum": 25,
494+-- "multipleOf": 25,
495+-- "type": "number"
496+-- }
497+-- ],
498+-- "maxItems": 2,
499+-- "minItems": 2,
500+-- "type": "array",
501+-- "uniqueItems": true
502+-- }
350503--
351504-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
352505-- >>> instance ToJSON Person
353−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema (Person "Jack" 25)
354−-- {"minProperties":2,"required":["age","name"],"maxProperties":2,"type":"object","enum":[{"age":25,"name":"Jack"}],"properties":{"age":{"maximum":25,"minimum":25,"multipleOf":25,"type":"number","enum":[25]},"name":{"maxLength":4,"pattern":"Jack","minLength":4,"type":"string","enum":["Jack"]}}}
506+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (Person "Jack" 25)
507+-- {
508+-- "enum": [
509+-- {
510+-- "age": 25,
511+-- "name": "Jack"
512+-- }
513+-- ],
514+-- "maxProperties": 2,
515+-- "minProperties": 2,
516+-- "properties": {
517+-- "age": {
518+-- "enum": [
519+-- 25
520+-- ],
521+-- "maximum": 25,
522+-- "minimum": 25,
523+-- "multipleOf": 25,
524+-- "type": "number"
525+-- },
526+-- "name": {
527+-- "enum": [
528+-- "Jack"
529+-- ],
530+-- "maxLength": 4,
531+-- "minLength": 4,
532+-- "pattern": "Jack",
533+-- "type": "string"
534+-- }
535+-- },
536+-- "required": [
537+-- "age",
538+-- "name"
539+-- ],
540+-- "type": "object"
541+-- }
355542sketchStrictSchema :: ToJSON a => a -> Schema
356543sketchStrictSchema = go . toJSON
357544 where
@@ -383,13 +570,13 @@ sketchStrictSchema = go . toJSON
383570 allUnique = sz == HashSet.size (HashSet.fromList (V.toList xs))
384571 go js@(Object o) = mempty
385572 & type_ ?~ OpenApiObject
386− & required .~ names
387− & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
573+ & required .~ sort names
574+ & properties .~ fmap (Inline . go) (toInsOrdHashMap o)
388575 & maxProperties ?~ fromIntegral (length names)
389576 & minProperties ?~ fromIntegral (length names)
390577 & enum_ ?~ [js]
391578 where
392− names = HashMap.keys o
579+ names = objectKeys o
393580
394581class GToSchema (f :: * -> *) where
395582 gdeclareNamedSchema :: SchemaOptions -> Proxy f -> Schema -> Declare (Definitions Schema) NamedSchema
@@ -424,7 +611,7 @@ instance ToSchema Scientific where declareNamedSchema = plain . paramSchemaToSc
424611instance ToSchema Double where declareNamedSchema = plain . paramSchemaToSchema
425612instance ToSchema Float where declareNamedSchema = plain . paramSchemaToSchema
426613
427−instance HasResolution a => ToSchema (Fixed a) where declareNamedSchema = plain . paramSchemaToSchema
614+instance (Typeable (Fixed a), HasResolution a) => ToSchema (Fixed a) where declareNamedSchema = plain . paramSchemaToSchema
428615
429616instance ToSchema a => ToSchema (Maybe a) where
430617 declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy a)
@@ -441,12 +628,18 @@ instance ToSchema UUID.UUID where
441628 declareNamedSchema p = pure $ named "UUID" $ paramSchemaToSchema p
442629 & example ?~ toJSON (UUID.toText UUID.nil)
443630
444−instance (ToSchema a, ToSchema b) => ToSchema (a, b)
445−instance (ToSchema a, ToSchema b, ToSchema c) => ToSchema (a, b, c)
446−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d) => ToSchema (a, b, c, d)
447−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e) => ToSchema (a, b, c, d, e)
448−instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f) => ToSchema (a, b, c, d, e, f)
449−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
450643
451644timeSchema :: T.Text -> Schema
452645timeSchema fmt = mempty
@@ -465,7 +658,7 @@ instance ToSchema LocalTime where
465658 declareNamedSchema _ = pure $ named "LocalTime" $ timeSchema "yyyy-mm-ddThh:MM:ss"
466659 & example ?~ toJSON (LocalTime (fromGregorian 2016 7 22) (TimeOfDay 7 40 0))
467660
468−-- | 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.
469662instance ToSchema ZonedTime where
470663 declareNamedSchema _ = pure $ named "ZonedTime" $ timeSchema "date-time"
471664 & example ?~ toJSON (ZonedTime (LocalTime (fromGregorian 2016 7 22) (TimeOfDay 7 40 0)) (hoursToTimeZone 3))
@@ -497,7 +690,7 @@ instance ToSchemaByteStringError BSL.ByteString => ToSchema BSL.ByteString where
497690instance ToSchema IntSet where declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy (Set Int))
498691
499692-- | NOTE: This schema does not account for the uniqueness of keys.
500−instance ToSchema a => ToSchema (IntMap a) where
693+instance (ToSchema a) => ToSchema (IntMap a) where
501694 declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [(Int, a)])
502695
503696instance (ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (Map k v) where
@@ -553,8 +746,12 @@ instance ToSchema a => ToSchema (Identity a) where declareNamedSchema _ = declar
553746
554747-- | Default schema for @'Bounded'@, @'Integral'@ types.
555748--
556−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
557−-- {"maximum":32767,"minimum":-32768,"type":"integer"}
749+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
750+-- {
751+-- "maximum": 32767,
752+-- "minimum": -32768,
753+-- "type": "integer"
754+-- }
558755toSchemaBoundedIntegral :: forall a. (Bounded a, Integral a) => Proxy a -> Schema
559756toSchemaBoundedIntegral _ = mempty
560757 & type_ ?~ OpenApiInteger
@@ -586,8 +783,27 @@ genericDeclareNamedSchemaNewtype opts f proxy = genericNameSchema opts proxy <$>
586783-- >>> instance ToSchema ButtonState
587784-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
588785-- >>> type ImageUrl = T.Text
589−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
590−-- {"type":"object","properties":{"Focus":{"type":"string"},"Disabled":{"type":"string"},"Active":{"type":"string"},"Neutral":{"type":"string"},"Hover":{"type":"string"}}}
786+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
787+-- {
788+-- "properties": {
789+-- "Active": {
790+-- "type": "string"
791+-- },
792+-- "Disabled": {
793+-- "type": "string"
794+-- },
795+-- "Focus": {
796+-- "type": "string"
797+-- },
798+-- "Hover": {
799+-- "type": "string"
800+-- },
801+-- "Neutral": {
802+-- "type": "string"
803+-- }
804+-- },
805+-- "type": "object"
806+-- }
591807--
592808-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
593809-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -595,13 +811,13 @@ declareSchemaBoundedEnumKeyMapping :: forall map key value.
595811 (Bounded key, Enum key, ToJSONKey key, ToSchema key, ToSchema value)
596812 => Proxy (map key value) -> Declare (Definitions Schema) Schema
597813declareSchemaBoundedEnumKeyMapping _ = case toJSONKey :: ToJSONKeyFunction key of
598− ToJSONKeyText keyToText _ -> objectSchema keyToText
814+ ToJSONKeyText getKey _ -> objectSchema getKey
599815 ToJSONKeyValue _ _ -> declareSchema (Proxy :: Proxy [(key, value)])
600816 where
601− objectSchema keyToText = do
817+ objectSchema getKey = do
602818 valueRef <- declareSchemaRef (Proxy :: Proxy value)
603819 let allKeys = [minBound..maxBound :: key]
604− mkPair k = (keyToText k, valueRef)
820+ mkPair k = (keyToText $ getKey k, valueRef)
605821 return $ mempty
606822 & type_ ?~ OpenApiObject
607823 & properties .~ InsOrdHashMap.fromList (map mkPair allKeys)
@@ -614,8 +830,27 @@ declareSchemaBoundedEnumKeyMapping _ = case toJSONKey :: ToJSONKeyFunction key o
614830-- >>> instance ToSchema ButtonState
615831-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
616832-- >>> type ImageUrl = T.Text
617−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
618−-- {"type":"object","properties":{"Focus":{"type":"string"},"Disabled":{"type":"string"},"Active":{"type":"string"},"Neutral":{"type":"string"},"Hover":{"type":"string"}}}
833+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
834+-- {
835+-- "properties": {
836+-- "Active": {
837+-- "type": "string"
838+-- },
839+-- "Disabled": {
840+-- "type": "string"
841+-- },
842+-- "Focus": {
843+-- "type": "string"
844+-- },
845+-- "Hover": {
846+-- "type": "string"
847+-- },
848+-- "Neutral": {
849+-- "type": "string"
850+-- }
851+-- },
852+-- "type": "object"
853+-- }
619854--
620855-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
621856-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -625,7 +860,7 @@ toSchemaBoundedEnumKeyMapping :: forall map key value.
625860toSchemaBoundedEnumKeyMapping = flip evalDeclare mempty . declareSchemaBoundedEnumKeyMapping
626861
627862-- | A configurable generic @'Schema'@ creator.
628−genericDeclareSchema :: (Generic a, GToSchema (Rep a)) =>
863+genericDeclareSchema :: (Generic a, GToSchema (Rep a), Typeable a) =>
629864 SchemaOptions -> Proxy a -> Declare (Definitions Schema) Schema
630865genericDeclareSchema opts proxy = _namedSchemaSchema <$> genericDeclareNamedSchema opts proxy
631866
@@ -633,9 +868,24 @@ genericDeclareSchema opts proxy = _namedSchemaSchema <$> genericDeclareNamedSche
633868-- This function applied to @'defaultSchemaOptions'@
634869-- is used as the default for @'declareNamedSchema'@
635870-- when the type is an instance of @'Generic'@.
636−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) =>
637880 SchemaOptions -> Proxy a -> Declare (Definitions Schema) NamedSchema
638−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+
639889
640890-- | Derive a 'Generic'-based name for a datatype and assign it to a given 'Schema'.
641891genericNameSchema :: forall a d f.
@@ -695,10 +945,13 @@ instance (Selector s, GToSchema f, GToSchema (S1 s f)) => GToSchema (C1 c (S1 s
695945 case schema ^. items of
696946 Just (OpenApiItemsArray [_]) -> fieldSchema
697947 _ -> do
698− declare defs
699− 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')
700953 where
701− (defs, NamedSchema _ schema) = runDeclare recordSchema mempty
954+ (_, NamedSchema _ schema) = runDeclare recordSchema mempty
702955 recordSchema = gdeclareNamedSchema opts (Proxy :: Proxy (S1 s f)) s
703956 fieldSchema = gdeclareNamedSchema opts (Proxy :: Proxy f) s
704957
@@ -861,58 +1114,13 @@ data Proxy2 a b = Proxy2
8611114
8621115data Proxy3 a b c = Proxy3
8631116
864−-- | This class allows to generate schemas for polymorphic types (of kind @Type -> Type@).
865−--
866−-- Intended usage:
867−--
868−-- >>> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
869−-- >>> :{
870−-- instance (ToSchema a, Typeable a) => ToSchema (Foo a) where
871−-- declareNamedSchema _ = declareNamedSchema @(BySchema1 Foo a) Proxy
872−-- :}
873−--
874−-- >>> toNamedSchema @(Foo Int) Proxy ^. name
875−-- Just "Foo_Int"
876−-- >>> toNamedSchema @(Foo Bool) Proxy ^. name
877−-- Just "Foo_Bool"
878−-- >>> toNamedSchema @(Foo (Foo T.Text)) Proxy ^. name
879−-- Just "Foo_(Foo_Text)"
880−class ToSchema1 (f :: * -> *) where
881− declareNamedSchema1 :: (Generic (f a), GToSchema (Rep (f a)), ToSchema a) => Proxy f -> Proxy a -> Declare (Definitions Schema) NamedSchema
882−
883− -- It would be cleaner to have GToSchema constraint only on default signature and not in the class method
884− -- above, however sadly GHC does not like it.
885− default declareNamedSchema1 :: forall a. (ToSchema a, Generic (f a), GToSchema (Rep (f a))) => Proxy f -> Proxy a -> Declare (Definitions Schema) NamedSchema
886− declareNamedSchema1 _ _ = genericDeclareNamedSchema @(f a) defaultSchemaOptions Proxy
887−
888−{- | For GHC 8.6+ it's more convenient to use @DerivingVia@ to derive instances of 'ToSchema'
889−using 'ToSchema1' instance, like this:
890−
891−#if __GLASGOW_HASKELL__ >= 806
892−>>> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
893−>>> deriving via BySchema1 Foo a instance (ToSchema a, Typeable a) => ToSchema (Foo a)
894−#else
895−> data Foo a = Foo { foo :: a, bar :: Int } deriving (Eq, Show, Generic, ToSchema1)
896−> deriving via BySchema1 Foo a instance (ToSchema a, Typeable a) => ToSchema (Foo a)
897−#endif
898−-}
899−newtype BySchema1 f a = BySchema1 (f a)
900−
901−instance (ToSchema1 f, Generic (f a), GToSchema (Rep (f a)), Typeable (f a), ToSchema a) => ToSchema (BySchema1 f a) where
902− declareNamedSchema _ = do
903− sch <- declareNamedSchema1 @f @a Proxy Proxy
904− let tName = T.replace " " "_" $ T.pack $ show $ typeRep @(f a)
905− return $ rename (Just tName) sch
906−
9071117{- $setup
9081118>>> import Data.OpenApi
9091119>>> import Data.Aeson (encode)
9101120>>> import Data.Aeson.Types (toJSONKeyText)
1121+>>> import Data.OpenApi.Internal.Utils
9111122>>> :set -XScopedTypeVariables
9121123>>> :set -XDeriveAnyClass
9131124>>> :set -XStandaloneDeriving
9141125>>> :set -XTypeApplications
915−#if __GLASGOW_HASKELL__ >= 806
916−>>> :set -XDerivingVia
917−#endif
9181126-}
src/Data/OpenApi/Internal/Schema/Validation.hs+39−27
@@ -1,4 +1,5 @@
11{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE CPP #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE DeriveFunctor #-}
45{-# LANGUAGE FlexibleInstances #-}
@@ -31,11 +32,14 @@ import Control.Lens hiding (allOf)
3132import Control.Monad (forM, forM_, when)
3233
3334import Data.Aeson hiding (Result)
34−import Data.Aeson.Encode.Pretty (encodePretty)
35+#if MIN_VERSION_aeson(2,0,0)
36+import qualified Data.Aeson.KeyMap as KeyMap
37+#endif
3538import Data.Foldable (for_, sequenceA_,
3639 traverse_)
40+#if !MIN_VERSION_aeson(2,0,0)
3741import Data.HashMap.Strict (HashMap)
38−import qualified Data.HashMap.Strict as HashMap
42+#endif
3943import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
4044import qualified "unordered-containers" Data.HashSet as HashSet
4145import Data.Maybe (fromMaybe)
@@ -48,10 +52,12 @@ import qualified Data.Text.Lazy.Encoding as TL
4852import Data.Vector (Vector)
4953import qualified Data.Vector as Vector
5054
51−import Data.OpenApi.Declare
52−import Data.OpenApi.Internal
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:
@@ -103,33 +109,33 @@ validateToJSONWithPatternChecker checker = validateJSONWithPatternChecker checke
103109-- <BLANKLINE>
104110-- Swagger Schema:
105111-- {
106−-- "required": [
107−-- "name",
108−-- "phone"
109−-- ],
110−-- "type": "object",
111112-- "properties": {
112−-- "phone": {
113−-- "$ref": "#/components/schemas/Phone"
114−-- },
115113-- "name": {
116114-- "type": "string"
115+-- },
116+-- "phone": {
117+-- "$ref": "#/components/schemas/Phone"
117118-- }
118−-- }
119+-- },
120+-- "required": [
121+-- "name",
122+-- "phone"
123+-- ],
124+-- "type": "object"
119125-- }
120126-- <BLANKLINE>
121127-- Swagger Description Context:
122128-- {
123129-- "Phone": {
124−-- "required": [
125−-- "value"
126−-- ],
127−-- "type": "object",
128130-- "properties": {
129131-- "value": {
130132-- "type": "string"
131133-- }
132−-- }
134+-- },
135+-- "required": [
136+-- "value"
137+-- ],
138+-- "type": "object"
133139-- }
134140-- }
135141-- <BLANKLINE>
@@ -315,11 +321,11 @@ validateNumber n = withConfig $ \_cfg -> withSchema $ \sch -> do
315321
316322 check maximum_ $ \m ->
317323 when (if exMax then (n >= m) else (n > m)) $
318− invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ if exMax then "<" else "<=" ++ show m ++ ")")
324+ invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ (if exMax then "<" else "<=") ++ show m ++ ")")
319325
320326 check minimum_ $ \m ->
321327 when (if exMin then (n <= m) else (n < m)) $
322− invalid ("value " ++ show n ++ " falls below minimum (should be " ++ if exMin then ">" else ">=" ++ show m ++ ")")
328+ invalid ("value " ++ show n ++ " falls below minimum (should be " ++ (if exMin then ">" else ">=") ++ show m ++ ")")
323329
324330 check multipleOf $ \k ->
325331 when (not (isInteger (n / k))) $
@@ -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/Internal/Utils.hs+5
@@ -13,6 +13,8 @@ import Control.Lens ((&), (%~))
1313import Control.Lens.TH
1414import Data.Aeson
1515import Data.Aeson.Types
16+import qualified Data.Aeson.Encode.Pretty as P
17+import qualified Data.ByteString.Lazy as BSL
1618import Data.Char
1719import Data.Data
1820import Data.Hashable (Hashable)
@@ -132,3 +134,6 @@ instance SwaggerMonoid (Maybe a) where
132134 swaggerMempty = Nothing
133135 swaggerMappend x Nothing = x
134136 swaggerMappend _ y = y
137+
138+encodePretty :: ToJSON a => a -> BSL.ByteString
139+encodePretty = P.encodePretty' $ P.defConfig { P.confCompare = P.compare }
src/Data/OpenApi/Operation.hs+117−10
@@ -55,13 +55,16 @@ import qualified Data.HashSet.InsOrd as InsOrdHS
5555-- >>> import Data.Proxy
5656-- >>> import Data.Time
5757-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL
58+-- >>> import Data.OpenApi.Internal.Utils
5859
5960-- | Prepend path piece to all operations of the spec.
6061-- Leading and trailing slashes are trimmed/added automatically.
6162--
6263-- >>> let api = (mempty :: OpenApi) & paths .~ [("/info", mempty)]
63−-- >>> BSL.putStrLn $ encode $ prependPath "user/{user_id}" api ^. paths
64−-- {"/user/{user_id}/info":{}}
64+-- >>> BSL.putStrLn $ encodePretty $ prependPath "user/{user_id}" api ^. paths
65+-- {
66+-- "/user/{user_id}/info": {}
67+-- }
6568prependPath :: FilePath -> OpenApi -> OpenApi
6669prependPath path = paths %~ InsOrdHashMap.mapKeys (path </>)
6770 where
@@ -82,10 +85,63 @@ allOperations = paths.traverse.template
8285-- >>> let ok = (mempty :: Operation) & at 200 ?~ "OK"
8386-- >>> let api = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ ok & post ?~ ok)]
8487-- >>> let sub = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ mempty)]
85−-- >>> BSL.putStrLn $ encode api
86−-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/user":{"get":{"responses":{"200":{"description":"OK"}}},"post":{"responses":{"200":{"description":"OK"}}}}},"components":{}}
87−-- >>> BSL.putStrLn $ encode $ api & operationsOf sub . at 404 ?~ "Not found"
88−-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/user":{"get":{"responses":{"404":{"description":"Not found"},"200":{"description":"OK"}}},"post":{"responses":{"200":{"description":"OK"}}}}},"components":{}}
88+-- >>> BSL.putStrLn $ encodePretty api
89+-- {
90+-- "components": {},
91+-- "info": {
92+-- "title": "",
93+-- "version": ""
94+-- },
95+-- "openapi": "3.0.0",
96+-- "paths": {
97+-- "/user": {
98+-- "get": {
99+-- "responses": {
100+-- "200": {
101+-- "description": "OK"
102+-- }
103+-- }
104+-- },
105+-- "post": {
106+-- "responses": {
107+-- "200": {
108+-- "description": "OK"
109+-- }
110+-- }
111+-- }
112+-- }
113+-- }
114+-- }
115+-- >>> BSL.putStrLn $ encodePretty $ api & operationsOf sub . at 404 ?~ "Not found"
116+-- {
117+-- "components": {},
118+-- "info": {
119+-- "title": "",
120+-- "version": ""
121+-- },
122+-- "openapi": "3.0.0",
123+-- "paths": {
124+-- "/user": {
125+-- "get": {
126+-- "responses": {
127+-- "200": {
128+-- "description": "OK"
129+-- },
130+-- "404": {
131+-- "description": "Not found"
132+-- }
133+-- }
134+-- },
135+-- "post": {
136+-- "responses": {
137+-- "200": {
138+-- "description": "OK"
139+-- }
140+-- }
141+-- }
142+-- }
143+-- }
144+-- }
89145operationsOf :: OpenApi -> Traversal' OpenApi Operation
90146operationsOf sub = paths.itraversed.withIndex.subops
91147 where
@@ -123,8 +179,26 @@ applyTagsFor ops ts swag = swag
123179--
124180-- FIXME doc
125181--
126−-- >>> BSL.putStrLn $ encode $ runDeclare (declareResponse "application/json" (Proxy :: Proxy Day)) mempty
127−-- [{"Day":{"example":"2016-07-22","format":"date","type":"string"}},{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Day"}}}}]
182+-- >>> BSL.putStrLn $ encodePretty $ runDeclare (declareResponse "application/json" (Proxy :: Proxy Day)) mempty
183+-- [
184+-- {
185+-- "Day": {
186+-- "example": "2016-07-22",
187+-- "format": "date",
188+-- "type": "string"
189+-- }
190+-- },
191+-- {
192+-- "content": {
193+-- "application/json": {
194+-- "schema": {
195+-- "$ref": "#/components/schemas/Day"
196+-- }
197+-- }
198+-- },
199+-- "description": ""
200+-- }
201+-- ]
128202declareResponse :: ToSchema a => MediaType -> Proxy a -> Declare (Definitions Schema) Response
129203declareResponse cType proxy = do
130204 s <- declareSchemaRef proxy
@@ -143,8 +217,41 @@ declareResponse cType proxy = do
143217--
144218-- >>> let api = (mempty :: OpenApi) & paths .~ [("/user", mempty & get ?~ mempty)]
145219-- >>> let res = declareResponse "application/json" (Proxy :: Proxy Day)
146−-- >>> BSL.putStrLn $ encode $ api & setResponse 200 res
147−-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/user":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Day"}}},"description":""}}}}},"components":{"schemas":{"Day":{"example":"2016-07-22","format":"date","type":"string"}}}}
220+-- >>> BSL.putStrLn $ encodePretty $ api & setResponse 200 res
221+-- {
222+-- "components": {
223+-- "schemas": {
224+-- "Day": {
225+-- "example": "2016-07-22",
226+-- "format": "date",
227+-- "type": "string"
228+-- }
229+-- }
230+-- },
231+-- "info": {
232+-- "title": "",
233+-- "version": ""
234+-- },
235+-- "openapi": "3.0.0",
236+-- "paths": {
237+-- "/user": {
238+-- "get": {
239+-- "responses": {
240+-- "200": {
241+-- "content": {
242+-- "application/json": {
243+-- "schema": {
244+-- "$ref": "#/components/schemas/Day"
245+-- }
246+-- }
247+-- },
248+-- "description": ""
249+-- }
250+-- }
251+-- }
252+-- }
253+-- }
254+-- }
148255--
149256-- See also @'setResponseWith'@.
150257setResponse :: HttpStatusCode -> Declare (Definitions Schema) Response -> OpenApi -> OpenApi
src/Data/OpenApi/Optics.hs+54−9
@@ -22,43 +22,88 @@
2222-- Example from the "Data.OpenApi" module using @optics@:
2323--
2424-- >>> :{
25−-- BSL.putStrLn $ encode $ (mempty :: OpenApi)
25+-- BSL.putStrLn $ encodePretty $ (mempty :: OpenApi)
2626-- & #components % #schemas .~ [ ("User", mempty & #type ?~ OpenApiString) ]
2727-- & #paths .~
2828-- [ ("/user", mempty & #get ?~ (mempty
2929-- & at 200 ?~ ("OK" & #_Inline % #content % at "application/json" ?~ (mempty & #schema ?~ Ref (Reference "User")))
3030-- & at 404 ?~ "User info not found")) ]
3131-- :}
32−-- {"openapi":"3.0.0","info":{"version":"","title":""},"paths":{"/user":{"get":{"responses":{"404":{"description":"User info not found"},"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"}}}}},"components":{"schemas":{"User":{"type":"string"}}}}
32+-- {
33+-- "components": {
34+-- "schemas": {
35+-- "User": {
36+-- "type": "string"
37+-- }
38+-- }
39+-- },
40+-- "info": {
41+-- "title": "",
42+-- "version": ""
43+-- },
44+-- "openapi": "3.0.0",
45+-- "paths": {
46+-- "/user": {
47+-- "get": {
48+-- "responses": {
49+-- "200": {
50+-- "content": {
51+-- "application/json": {
52+-- "schema": {
53+-- "$ref": "#/components/schemas/User"
54+-- }
55+-- }
56+-- },
57+-- "description": "OK"
58+-- },
59+-- "404": {
60+-- "description": "User info not found"
61+-- }
62+-- }
63+-- }
64+-- }
65+-- }
66+-- }
3367--
3468-- For convenience optics are defined as /labels/. It means that field accessor
3569-- names can be overloaded for different types. One such common field is
3670-- @#description@. Many components of a Swagger specification can have
3771-- descriptions, and you can use the same name for them:
3872--
39−-- >>> BSL.putStrLn $ encode $ (mempty :: Response) & #description .~ "No content"
40−-- {"description":"No content"}
73+-- >>> BSL.putStrLn $ encodePretty $ (mempty :: Response) & #description .~ "No content"
74+-- {
75+-- "description": "No content"
76+-- }
4177-- >>> :{
42−-- BSL.putStrLn $ encode $ (mempty :: Schema)
78+-- BSL.putStrLn $ encodePretty $ (mempty :: Schema)
4379-- & #type ?~ OpenApiBoolean
4480-- & #description ?~ "To be or not to be"
4581-- :}
46−-- {"type":"boolean","description":"To be or not to be"}
82+-- {
83+-- "description": "To be or not to be",
84+-- "type": "boolean"
85+-- }
4786--
4887-- Additionally, to simplify working with @'Response'@, both @'Operation'@ and
4988-- @'Responses'@ have direct access to it via @'Optics.Core.At.at'@. Example:
5089--
5190-- >>> :{
52−-- BSL.putStrLn $ encode $ (mempty :: Operation)
91+-- BSL.putStrLn $ encodePretty $ (mempty :: Operation)
5392-- & at 404 ?~ "Not found"
5493-- :}
55−-- {"responses":{"404":{"description":"Not found"}}}
56−--
94+-- {
95+-- "responses": {
96+-- "404": {
97+-- "description": "Not found"
98+-- }
99+-- }
100+-- }
57101module Data.OpenApi.Optics () where
58102
59103import Data.Aeson (Value)
60104import Data.Scientific (Scientific)
61105import Data.OpenApi.Internal
106+import Data.OpenApi.Internal.Utils
62107import Data.Text (Text)
63108import Optics.Core
64109import Optics.TH
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