git.iko.soy/typeable/openapi3
on master help sign in
Switch to Github Actions, allow GHC 9.0.1 and hashable 1.3.1.0 (#18)

* Try actions & GHC-9.0.1 * Fix doctests to allow newer hashable * Allow fail for 9.0.1

commite4b27f3b5dbc938a10b4a78eb88d0dca6341eab9 authorMaxim Koltsov <kolmax94@gmail.com> dateWed 7 Apr 2021, 11:58 (6 years ago) parent15b99ab
13 files changed+850−269
.github/workflows/haskell-ci.yml+177 .travis.yml−149 CHANGELOG.md+5 cabal.project+2 openapi3.cabal+6−7 src/Data/OpenApi.hs+140−20 src/Data/OpenApi/Internal.hs+35−12 src/Data/OpenApi/Internal/ParamSchema.hs+27−9 src/Data/OpenApi/Internal/Schema.hs+267−38 src/Data/OpenApi/Internal/Schema/Validation.hs+15−15 src/Data/OpenApi/Internal/Utils.hs+5 src/Data/OpenApi/Operation.hs+117−10 src/Data/OpenApi/Optics.hs+54−9
.github/workflows/haskell-ci.ymlnew file+177
@@ -0,0 +1,177 @@
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.12
12+#
13+# REGENDATA ("0.12",["github","cabal.project"])
14+#
15+name: Haskell-CI
16+on:
17+ - push
18+ - pull_request
19+jobs:
20+ linux:
21+ name: Haskell-CI - Linux - ${{ matrix.compiler }}
22+ runs-on: ubuntu-18.04
23+ container:
24+ image: buildpack-deps:bionic
25+ continue-on-error: ${{ matrix.allow-failure }}
26+ strategy:
27+ matrix:
28+ include:
29+ - compiler: ghc-9.0.1
30+ allow-failure: true
31+ - compiler: ghc-8.10.4
32+ allow-failure: false
33+ - compiler: ghc-8.8.4
34+ allow-failure: false
35+ - compiler: ghc-8.6.5
36+ allow-failure: false
37+ - compiler: ghc-8.4.4
38+ allow-failure: false
39+ fail-fast: false
40+ steps:
41+ - name: apt
42+ run: |
43+ apt-get update
44+ apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common
45+ apt-add-repository -y 'ppa:hvr/ghc'
46+ apt-get update
47+ apt-get install -y $CC cabal-install-3.4
48+ env:
49+ CC: ${{ matrix.compiler }}
50+ - name: Set PATH and environment variables
51+ run: |
52+ echo "$HOME/.cabal/bin" >> $GITHUB_PATH
53+ echo "LANG=C.UTF-8" >> $GITHUB_ENV
54+ echo "CABAL_DIR=$HOME/.cabal" >> $GITHUB_ENV
55+ echo "CABAL_CONFIG=$HOME/.cabal/config" >> $GITHUB_ENV
56+ HCDIR=$(echo "/opt/$CC" | sed 's/-/\//')
57+ HCNAME=ghc
58+ HC=$HCDIR/bin/$HCNAME
59+ echo "HC=$HC" >> $GITHUB_ENV
60+ echo "HCPKG=$HCDIR/bin/$HCNAME-pkg" >> $GITHUB_ENV
61+ echo "HADDOCK=$HCDIR/bin/haddock" >> $GITHUB_ENV
62+ echo "CABAL=/opt/cabal/3.4/bin/cabal -vnormal+nowrap" >> $GITHUB_ENV
63+ HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
64+ echo "HCNUMVER=$HCNUMVER" >> $GITHUB_ENV
65+ echo "ARG_TESTS=--enable-tests" >> $GITHUB_ENV
66+ echo "ARG_BENCH=--enable-benchmarks" >> $GITHUB_ENV
67+ echo "HEADHACKAGE=false" >> $GITHUB_ENV
68+ echo "ARG_COMPILER=--$HCNAME --with-compiler=$HC" >> $GITHUB_ENV
69+ echo "GHCJSARITH=0" >> $GITHUB_ENV
70+ env:
71+ CC: ${{ matrix.compiler }}
72+ - name: env
73+ run: |
74+ env
75+ - name: write cabal config
76+ run: |
77+ mkdir -p $CABAL_DIR
78+ cat >> $CABAL_CONFIG <<EOF
79+ remote-build-reporting: anonymous
80+ write-ghc-environment-files: never
81+ remote-repo-cache: $CABAL_DIR/packages
82+ logs-dir: $CABAL_DIR/logs
83+ world-file: $CABAL_DIR/world
84+ extra-prog-path: $CABAL_DIR/bin
85+ symlink-bindir: $CABAL_DIR/bin
86+ installdir: $CABAL_DIR/bin
87+ build-summary: $CABAL_DIR/logs/build.log
88+ store-dir: $CABAL_DIR/store
89+ install-dirs user
90+ prefix: $CABAL_DIR
91+ repository hackage.haskell.org
92+ url: http://hackage.haskell.org/
93+ EOF
94+ cat $CABAL_CONFIG
95+ - name: versions
96+ run: |
97+ $HC --version || true
98+ $HC --print-project-git-commit-id || true
99+ $CABAL --version || true
100+ - name: update cabal index
101+ run: |
102+ $CABAL v2-update -v
103+ - name: install cabal-plan
104+ run: |
105+ mkdir -p $HOME/.cabal/bin
106+ 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
107+ echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c -
108+ xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
109+ rm -f cabal-plan.xz
110+ chmod a+x $HOME/.cabal/bin/cabal-plan
111+ cabal-plan --version
112+ - name: checkout
113+ uses: actions/checkout@v2
114+ with:
115+ path: source
116+ - name: initial cabal.project for sdist
117+ run: |
118+ touch cabal.project
119+ echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
120+ cat cabal.project
121+ - name: sdist
122+ run: |
123+ mkdir -p sdist
124+ $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
125+ - name: unpack
126+ run: |
127+ mkdir -p unpacked
128+ find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
129+ - name: generate cabal.project
130+ run: |
131+ PKGDIR_openapi3="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/openapi3-[0-9.]*')"
132+ echo "PKGDIR_openapi3=${PKGDIR_openapi3}" >> $GITHUB_ENV
133+ touch cabal.project
134+ touch cabal.project.local
135+ echo "packages: ${PKGDIR_openapi3}" >> cabal.project
136+ echo "package openapi3" >> cabal.project
137+ echo " ghc-options: -Werror=missing-methods" >> cabal.project
138+ cat >> cabal.project <<EOF
139+ allow-newer: http-media:base
140+ EOF
141+ $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(openapi3)$/; }' >> cabal.project.local
142+ cat cabal.project
143+ cat cabal.project.local
144+ - name: dump install plan
145+ run: |
146+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
147+ cabal-plan
148+ - name: cache
149+ uses: actions/cache@v2
150+ with:
151+ key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
152+ path: ~/.cabal/store
153+ restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
154+ - name: install dependencies
155+ run: |
156+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
157+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
158+ - name: build w/o tests
159+ run: |
160+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
161+ - name: build
162+ run: |
163+ $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
164+ - name: tests
165+ run: |
166+ $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
167+ - name: cabal check
168+ run: |
169+ cd ${PKGDIR_openapi3} || false
170+ ${CABAL} -vnormal check
171+ - name: haddock
172+ run: |
173+ $CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
174+ - name: unconstrained build
175+ run: |
176+ rm -f cabal.project.local
177+ $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
.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+5
@@ -1,3 +1,8 @@
1+Unreleased
2+----------
3+
4+- Allow `hashable-1.3.1`, prettify doctests.
5+
163.0.2.0
27-------
38
cabal.project+2
@@ -1,2 +1,4 @@
11packages: .
22tests: true
3+
4+allow-newer: http-media:base
openapi3.cabal+6−7
@@ -25,8 +25,9 @@ extra-source-files:
2525tested-with:
2626 GHC ==8.4.4
2727 || ==8.6.5
28− || ==8.8.3
29− || ==8.10.1
28+ || ==8.8.4
29+ || ==8.10.4
30+ || ==9.0.1
3031
3132custom-setup
3233 setup-depends:
@@ -57,10 +58,10 @@ library
5758
5859 -- GHC boot libraries
5960 build-depends:
60− base >=4.11.1.0 && <4.15
61+ base >=4.11.1.0 && <4.16
6162 , bytestring >=0.10.8.2 && <0.11
6263 , containers >=0.5.11.0 && <0.7
63− , template-haskell >=2.13.0.0 && <2.17
64+ , template-haskell >=2.13.0.0 && <2.18
6465 , time >=1.8.0.2 && <1.10
6566 , transformers >=0.5.5.0 && <0.6
6667
@@ -76,9 +77,7 @@ library
7677 -- cookie 0.4.3 is needed by GHC 7.8 due to time>=1.4 constraint
7778 , cookie >=0.4.3 && <0.5
7879 , generics-sop >=0.5.1.0 && <0.6
79− -- <1.3.1 is a temporary measure, until we fix doctests not to depend on key order
80− -- see https://github.com/haskell/aeson/issues/837
81− , hashable >=1.2.7.0 && <1.3.1
80+ , hashable >=1.2.7.0 && <1.4
8281 , http-media >=0.8.0.0 && <0.9
8382 , insert-ordered-containers >=0.2.3 && <0.3
8483 , lens >=4.16.1 && <5.1
src/Data/OpenApi.hs+140−20
@@ -137,6 +137,7 @@ import Data.OpenApi.Internal
137137-- >>> import Data.Proxy
138138-- >>> import GHC.Generics
139139-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL
140+-- >>> import Data.OpenApi.Internal.Utils
140141-- >>> :set -XDeriveGeneric
141142-- >>> :set -XOverloadedStrings
142143-- >>> :set -XOverloadedLists
@@ -153,8 +154,16 @@ import Data.OpenApi.Internal
153154--
154155-- In this library you can use @'mempty'@ for a default/empty value. For instance:
155156--
156−-- >>> BSL.putStrLn $ encode (mempty :: OpenApi)
157−-- {"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+-- }
158167--
159168-- As you can see some spec properties (e.g. @"version"@) are there even when the spec is empty.
160169-- That is because these properties are actually required ones.
@@ -162,13 +171,20 @@ import Data.OpenApi.Internal
162171-- You /should/ always override the default (empty) value for these properties,
163172-- although it is not strictly necessary:
164173--
165−-- >>> BSL.putStrLn $ encode mempty { _infoTitle = "Todo API", _infoVersion = "1.0" }
166−-- {"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+-- }
167179--
168180-- You can merge two values using @'mappend'@ or its infix version @('<>')@:
169181--
170−-- >>> BSL.putStrLn $ encode $ mempty { _infoTitle = "Todo API" } <> mempty { _infoVersion = "1.0" }
171−-- {"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+
172188--
173189-- This can be useful for combining specifications of endpoints into a whole API specification:
174190--
@@ -194,14 +210,48 @@ import Data.OpenApi.Internal
194210-- make it fairly simple to construct/modify any part of the specification:
195211--
196212-- >>> :{
197−-- BSL.putStrLn $ encode $ (mempty :: OpenApi)
213+-- BSL.putStrLn $ encodePretty $ (mempty :: OpenApi)
198214-- & components . schemas .~ [ ("User", mempty & type_ ?~ OpenApiString) ]
199215-- & paths .~
200216-- [ ("/user", mempty & get ?~ (mempty
201217-- & at 200 ?~ ("OK" & _Inline.content.at "application/json" ?~ (mempty & schema ?~ Ref (Reference "User")))
202218-- & at 404 ?~ "User info not found")) ]
203219-- :}
204−-- {"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+-- }
205255--
206256-- In the snippet above we declare an API with a single path @/user@. This path provides method @GET@
207257-- which produces @application/json@ output. It should respond with code @200@ and body specified
@@ -213,23 +263,34 @@ import Data.OpenApi.Internal
213263-- common field is @'description'@. Many components of a Swagger specification
214264-- can have descriptions, and you can use the same name for them:
215265--
216−-- >>> BSL.putStrLn $ encode $ (mempty :: Response) & description .~ "No content"
217−-- {"description":"No content"}
266+-- >>> BSL.putStrLn $ encodePretty $ (mempty :: Response) & description .~ "No content"
267+-- {
268+-- "description": "No content"
269+-- }
218270-- >>> :{
219−-- BSL.putStrLn $ encode $ (mempty :: Schema)
271+-- BSL.putStrLn $ encodePretty $ (mempty :: Schema)
220272-- & type_ ?~ OpenApiBoolean
221273-- & description ?~ "To be or not to be"
222274-- :}
223−-- {"type":"boolean","description":"To be or not to be"}
275+-- {
276+-- "description": "To be or not to be",
277+-- "type": "boolean"
278+-- }
224279--
225280-- Additionally, to simplify working with @'Response'@, both @'Operation'@ and @'Responses'@
226281-- have direct access to it via @'at' code@. Example:
227282--
228283-- >>> :{
229−-- BSL.putStrLn $ encode $ (mempty :: Operation)
284+-- BSL.putStrLn $ encodePretty $ (mempty :: Operation)
230285-- & at 404 ?~ "Not found"
231286-- :}
232−-- {"responses":{"404":{"description":"Not found"}}}
287+-- {
288+-- "responses": {
289+-- "404": {
290+-- "description": "Not found"
291+-- }
292+-- }
293+-- }
233294--
234295-- You might've noticed that @'type_'@ has an extra underscore in its name
235296-- compared to, say, @'description'@ field accessor.
@@ -276,10 +337,27 @@ import Data.OpenApi.Internal
276337-- >>> data Person = Person { name :: String, age :: Integer } deriving Generic
277338-- >>> instance ToJSON Person
278339-- >>> instance ToSchema Person
279−-- >>> BSL.putStrLn $ encode (Person "David" 28)
280−-- {"age":28,"name":"David"}
281−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Person)
282−-- {"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+-- }
283361--
284362-- This package implements OpenAPI 3.0 spec, which supports @oneOf@ in schemas, allowing any sum types
285363-- to be faithfully represented. All sum encodings supported by @aeson@ are supported here as well, with
@@ -290,8 +368,50 @@ import Data.OpenApi.Internal
290368-- >>> data Error = ErrorNoUser { userId :: Int } | ErrorAccessDenied { requiredPermission :: String } deriving Generic
291369-- >>> instance ToJSON Error
292370-- >>> instance ToSchema Error
293−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Error)
294−-- {"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+-- }
295415
296416-- $manipulation
297417-- Sometimes you have to work with an imported or generated @'Swagger'@.
src/Data/OpenApi/Internal.hs+35−12
@@ -60,6 +60,8 @@ import Data.OpenApi.Internal.AesonUtils (sopSwaggerGenericToEncoding)
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
@@ -848,20 +850,37 @@ data HttpSchemeType
848850
849851-- |
850852--
851−-- >>> encode (SecuritySchemeHttp (HttpSchemeBearer Nothing))
852−-- "{\"scheme\":\"bearer\",\"type\":\"http\"}"
853+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeBearer Nothing))
854+-- {
855+-- "scheme": "bearer",
856+-- "type": "http"
857+-- }
853858--
854−-- >>> encode (SecuritySchemeHttp (HttpSchemeBearer (Just "jwt")))
855−-- "{\"scheme\":\"bearer\",\"type\":\"http\",\"bearerFormat\":\"jwt\"}"
859+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeBearer (Just "jwt")))
860+-- {
861+-- "bearerFormat": "jwt",
862+-- "scheme": "bearer",
863+-- "type": "http"
864+-- }
856865--
857−-- >>> encode (SecuritySchemeHttp HttpSchemeBasic)
858−-- "{\"scheme\":\"basic\",\"type\":\"http\"}"
866+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp HttpSchemeBasic)
867+-- {
868+-- "scheme": "basic",
869+-- "type": "http"
870+-- }
859871--
860−-- >>> encode (SecuritySchemeHttp (HttpSchemeCustom "CANARY"))
861−-- "{\"scheme\":\"CANARY\",\"type\":\"http\"}"
872+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeHttp (HttpSchemeCustom "CANARY"))
873+-- {
874+-- "scheme": "CANARY",
875+-- "type": "http"
876+-- }
862877--
863−-- >>> encode (SecuritySchemeApiKey (ApiKeyParams "id" ApiKeyCookie))
864−-- "{\"in\":\"cookie\",\"name\":\"id\",\"type\":\"apiKey\"}"
878+-- >>> BSL.putStrLn $ encodePretty (SecuritySchemeApiKey (ApiKeyParams "id" ApiKeyCookie))
879+-- {
880+-- "in": "cookie",
881+-- "name": "id",
882+-- "type": "apiKey"
883+-- }
865884--
866885data SecuritySchemeType
867886 = SecuritySchemeHttp HttpSchemeType
@@ -1305,8 +1324,12 @@ instance ToJSON Header where
13051324-- | As for nullary schema for 0-arity type constructors, see
13061325-- <https://github.com/GetShopTV/swagger2/issues/167>.
13071326--
1308−-- >>> encode (OpenApiItemsArray [])
1309−-- "{\"example\":[],\"items\":{},\"maxItems\":0}"
1327+-- >>> BSL.putStrLn $ encodePretty (OpenApiItemsArray [])
1328+-- {
1329+-- "example": [],
1330+-- "items": {},
1331+-- "maxItems": 0
1332+-- }
13101333--
13111334instance ToJSON OpenApiItems where
13121335 toJSON (OpenApiItemsObject x) = object [ "items" .= x ]
src/Data/OpenApi/Internal/ParamSchema.hs+27−9
@@ -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
@@ -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+267−38
@@ -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)
@@ -156,13 +157,19 @@ declareSchema = fmap _namedSchemaSchema . declareNamedSchema
156157--
157158-- >>> toNamedSchema (Proxy :: Proxy String) ^. name
158159-- Nothing
159−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy String) ^. schema)
160−-- {"type":"string"}
160+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy String) ^. schema)
161+-- {
162+-- "type": "string"
163+-- }
161164--
162165-- >>> toNamedSchema (Proxy :: Proxy Day) ^. name
163166-- Just "Day"
164−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy Day) ^. schema)
165−-- {"example":"2016-07-22","format":"date","type":"string"}
167+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy Day) ^. schema)
168+-- {
169+-- "example": "2016-07-22",
170+-- "format": "date",
171+-- "type": "string"
172+-- }
166173toNamedSchema :: ToSchema a => Proxy a -> NamedSchema
167174toNamedSchema = undeclare . declareNamedSchema
168175
@@ -178,22 +185,35 @@ schemaName = _namedSchemaName . toNamedSchema
178185
179186-- | Convert a type into a schema.
180187--
181−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Int8)
182−-- {"maximum":127,"minimum":-128,"type":"integer"}
188+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy Int8)
189+-- {
190+-- "maximum": 127,
191+-- "minimum": -128,
192+-- "type": "integer"
193+-- }
183194--
184−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy [Day])
185−-- {"items":{"$ref":"#/components/schemas/Day"},"type":"array"}
195+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy [Day])
196+-- {
197+-- "items": {
198+-- "$ref": "#/components/schemas/Day"
199+-- },
200+-- "type": "array"
201+-- }
186202toSchema :: ToSchema a => Proxy a -> Schema
187203toSchema = _namedSchemaSchema . toNamedSchema
188204
189205-- | Convert a type into a referenced schema if possible.
190206-- Only named schemas can be referenced, nameless schemas are inlined.
191207--
192−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Integer)
193−-- {"type":"integer"}
208+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Integer)
209+-- {
210+-- "type": "integer"
211+-- }
194212--
195−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Day)
196−-- {"$ref":"#/components/schemas/Day"}
213+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Day)
214+-- {
215+-- "$ref": "#/components/schemas/Day"
216+-- }
197217toSchemaRef :: ToSchema a => Proxy a -> Referenced Schema
198218toSchemaRef = undeclare . declareSchemaRef
199219
@@ -261,8 +281,15 @@ inlineAllSchemas = inlineSchemasWhen (const True)
261281
262282-- | Convert a type into a schema without references.
263283--
264−-- >>> BSL.putStrLn $ encode $ toInlinedSchema (Proxy :: Proxy [Day])
265−-- {"items":{"example":"2016-07-22","format":"date","type":"string"},"type":"array"}
284+-- >>> BSL.putStrLn $ encodePretty $ toInlinedSchema (Proxy :: Proxy [Day])
285+-- {
286+-- "items": {
287+-- "example": "2016-07-22",
288+-- "format": "date",
289+-- "type": "string"
290+-- },
291+-- "type": "array"
292+-- }
266293--
267294-- __WARNING:__ @'toInlinedSchema'@ will produce infinite schema
268295-- when inlining recursive schemas.
@@ -295,19 +322,64 @@ inlineNonRecursiveSchemas defs = inlineSchemasWhen nonRecursive defs
295322-- | Make an unrestrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
296323-- Produced schema can be used for further refinement.
297324--
298−-- >>> BSL.putStrLn $ encode $ sketchSchema "hello"
299−-- {"example":"hello","type":"string"}
325+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema "hello"
326+-- {
327+-- "example": "hello",
328+-- "type": "string"
329+-- }
300330--
301−-- >>> BSL.putStrLn $ encode $ sketchSchema (1, 2, 3)
302−-- {"example":[1,2,3],"items":{"type":"number"},"type":"array"}
331+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (1, 2, 3)
332+-- {
333+-- "example": [
334+-- 1,
335+-- 2,
336+-- 3
337+-- ],
338+-- "items": {
339+-- "type": "number"
340+-- },
341+-- "type": "array"
342+-- }
303343--
304−-- >>> BSL.putStrLn $ encode $ sketchSchema ("Jack", 25)
305−-- {"example":["Jack",25],"items":[{"type":"string"},{"type":"number"}],"type":"array"}
344+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema ("Jack", 25)
345+-- {
346+-- "example": [
347+-- "Jack",
348+-- 25
349+-- ],
350+-- "items": [
351+-- {
352+-- "type": "string"
353+-- },
354+-- {
355+-- "type": "number"
356+-- }
357+-- ],
358+-- "type": "array"
359+-- }
306360--
307361-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
308362-- >>> 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"}}}
363+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (Person "Jack" 25)
364+-- {
365+-- "example": {
366+-- "age": 25,
367+-- "name": "Jack"
368+-- },
369+-- "properties": {
370+-- "age": {
371+-- "type": "number"
372+-- },
373+-- "name": {
374+-- "type": "string"
375+-- }
376+-- },
377+-- "required": [
378+-- "age",
379+-- "name"
380+-- ],
381+-- "type": "object"
382+-- }
311383sketchSchema :: ToJSON a => a -> Schema
312384sketchSchema = sketch . toJSON
313385 where
@@ -333,25 +405,139 @@ sketchSchema = sketch . toJSON
333405 _ -> Nothing
334406 go (Object o) = mempty
335407 & type_ ?~ OpenApiObject
336− & required .~ HashMap.keys o
408+ & required .~ sort (HashMap.keys o)
337409 & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
338410
339411-- | Make a restrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
340412-- Produced schema uses as much constraints as possible.
341413--
342−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema "hello"
343−-- {"maxLength":5,"pattern":"hello","minLength":5,"type":"string","enum":["hello"]}
414+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema "hello"
415+-- {
416+-- "enum": [
417+-- "hello"
418+-- ],
419+-- "maxLength": 5,
420+-- "minLength": 5,
421+-- "pattern": "hello",
422+-- "type": "string"
423+-- }
344424--
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]]}
425+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (1, 2, 3)
426+-- {
427+-- "enum": [
428+-- [
429+-- 1,
430+-- 2,
431+-- 3
432+-- ]
433+-- ],
434+-- "items": [
435+-- {
436+-- "enum": [
437+-- 1
438+-- ],
439+-- "maximum": 1,
440+-- "minimum": 1,
441+-- "multipleOf": 1,
442+-- "type": "number"
443+-- },
444+-- {
445+-- "enum": [
446+-- 2
447+-- ],
448+-- "maximum": 2,
449+-- "minimum": 2,
450+-- "multipleOf": 2,
451+-- "type": "number"
452+-- },
453+-- {
454+-- "enum": [
455+-- 3
456+-- ],
457+-- "maximum": 3,
458+-- "minimum": 3,
459+-- "multipleOf": 3,
460+-- "type": "number"
461+-- }
462+-- ],
463+-- "maxItems": 3,
464+-- "minItems": 3,
465+-- "type": "array",
466+-- "uniqueItems": true
467+-- }
347468--
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]]}
469+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema ("Jack", 25)
470+-- {
471+-- "enum": [
472+-- [
473+-- "Jack",
474+-- 25
475+-- ]
476+-- ],
477+-- "items": [
478+-- {
479+-- "enum": [
480+-- "Jack"
481+-- ],
482+-- "maxLength": 4,
483+-- "minLength": 4,
484+-- "pattern": "Jack",
485+-- "type": "string"
486+-- },
487+-- {
488+-- "enum": [
489+-- 25
490+-- ],
491+-- "maximum": 25,
492+-- "minimum": 25,
493+-- "multipleOf": 25,
494+-- "type": "number"
495+-- }
496+-- ],
497+-- "maxItems": 2,
498+-- "minItems": 2,
499+-- "type": "array",
500+-- "uniqueItems": true
501+-- }
350502--
351503-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
352504-- >>> 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"]}}}
505+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (Person "Jack" 25)
506+-- {
507+-- "enum": [
508+-- {
509+-- "age": 25,
510+-- "name": "Jack"
511+-- }
512+-- ],
513+-- "maxProperties": 2,
514+-- "minProperties": 2,
515+-- "properties": {
516+-- "age": {
517+-- "enum": [
518+-- 25
519+-- ],
520+-- "maximum": 25,
521+-- "minimum": 25,
522+-- "multipleOf": 25,
523+-- "type": "number"
524+-- },
525+-- "name": {
526+-- "enum": [
527+-- "Jack"
528+-- ],
529+-- "maxLength": 4,
530+-- "minLength": 4,
531+-- "pattern": "Jack",
532+-- "type": "string"
533+-- }
534+-- },
535+-- "required": [
536+-- "age",
537+-- "name"
538+-- ],
539+-- "type": "object"
540+-- }
355541sketchStrictSchema :: ToJSON a => a -> Schema
356542sketchStrictSchema = go . toJSON
357543 where
@@ -383,7 +569,7 @@ sketchStrictSchema = go . toJSON
383569 allUnique = sz == HashSet.size (HashSet.fromList (V.toList xs))
384570 go js@(Object o) = mempty
385571 & type_ ?~ OpenApiObject
386− & required .~ names
572+ & required .~ sort names
387573 & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
388574 & maxProperties ?~ fromIntegral (length names)
389575 & minProperties ?~ fromIntegral (length names)
@@ -553,8 +739,12 @@ instance ToSchema a => ToSchema (Identity a) where declareNamedSchema _ = declar
553739
554740-- | Default schema for @'Bounded'@, @'Integral'@ types.
555741--
556−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
557−-- {"maximum":32767,"minimum":-32768,"type":"integer"}
742+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
743+-- {
744+-- "maximum": 32767,
745+-- "minimum": -32768,
746+-- "type": "integer"
747+-- }
558748toSchemaBoundedIntegral :: forall a. (Bounded a, Integral a) => Proxy a -> Schema
559749toSchemaBoundedIntegral _ = mempty
560750 & type_ ?~ OpenApiInteger
@@ -586,8 +776,27 @@ genericDeclareNamedSchemaNewtype opts f proxy = genericNameSchema opts proxy <$>
586776-- >>> instance ToSchema ButtonState
587777-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
588778-- >>> 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"}}}
779+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
780+-- {
781+-- "properties": {
782+-- "Active": {
783+-- "type": "string"
784+-- },
785+-- "Disabled": {
786+-- "type": "string"
787+-- },
788+-- "Focus": {
789+-- "type": "string"
790+-- },
791+-- "Hover": {
792+-- "type": "string"
793+-- },
794+-- "Neutral": {
795+-- "type": "string"
796+-- }
797+-- },
798+-- "type": "object"
799+-- }
591800--
592801-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
593802-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -614,8 +823,27 @@ declareSchemaBoundedEnumKeyMapping _ = case toJSONKey :: ToJSONKeyFunction key o
614823-- >>> instance ToSchema ButtonState
615824-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
616825-- >>> 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"}}}
826+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
827+-- {
828+-- "properties": {
829+-- "Active": {
830+-- "type": "string"
831+-- },
832+-- "Disabled": {
833+-- "type": "string"
834+-- },
835+-- "Focus": {
836+-- "type": "string"
837+-- },
838+-- "Hover": {
839+-- "type": "string"
840+-- },
841+-- "Neutral": {
842+-- "type": "string"
843+-- }
844+-- },
845+-- "type": "object"
846+-- }
619847--
620848-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
621849-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -908,6 +1136,7 @@ instance (ToSchema1 f, Generic (f a), GToSchema (Rep (f a)), Typeable (f a), ToS
9081136>>> import Data.OpenApi
9091137>>> import Data.Aeson (encode)
9101138>>> import Data.Aeson.Types (toJSONKeyText)
1139+>>> import Data.OpenApi.Internal.Utils
9111140>>> :set -XScopedTypeVariables
9121141>>> :set -XDeriveAnyClass
9131142>>> :set -XStandaloneDeriving
src/Data/OpenApi/Internal/Schema/Validation.hs+15−15
@@ -31,7 +31,6 @@ import Control.Lens hiding (allOf)
3131import Control.Monad (forM, forM_, when)
3232
3333import Data.Aeson hiding (Result)
34−import Data.Aeson.Encode.Pretty (encodePretty)
3534import Data.Foldable (for_, sequenceA_,
3635 traverse_)
3736import Data.HashMap.Strict (HashMap)
@@ -50,6 +49,7 @@ import qualified Data.Vector as Vector
5049
5150import Data.OpenApi.Declare
5251import Data.OpenApi.Internal
52+import Data.OpenApi.Internal.Utils
5353import Data.OpenApi.Internal.Schema
5454import Data.OpenApi.Lens
5555
@@ -103,33 +103,33 @@ validateToJSONWithPatternChecker checker = validateJSONWithPatternChecker checke
103103-- <BLANKLINE>
104104-- Swagger Schema:
105105-- {
106−-- "required": [
107−-- "name",
108−-- "phone"
109−-- ],
110−-- "type": "object",
111106-- "properties": {
112−-- "phone": {
113−-- "$ref": "#/components/schemas/Phone"
114−-- },
115107-- "name": {
116108-- "type": "string"
109+-- },
110+-- "phone": {
111+-- "$ref": "#/components/schemas/Phone"
117112-- }
118−-- }
113+-- },
114+-- "required": [
115+-- "name",
116+-- "phone"
117+-- ],
118+-- "type": "object"
119119-- }
120120-- <BLANKLINE>
121121-- Swagger Description Context:
122122-- {
123123-- "Phone": {
124−-- "required": [
125−-- "value"
126−-- ],
127−-- "type": "object",
128124-- "properties": {
129125-- "value": {
130126-- "type": "string"
131127-- }
132−-- }
128+-- },
129+-- "required": [
130+-- "value"
131+-- ],
132+-- "type": "object"
133133-- }
134134-- }
135135-- <BLANKLINE>
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