git.iko.soy/typeable/compaREST
on master help sign in
.github/actions/common_setup/action.yamlnew file+42
since splitsnapshots
unifiedsplit
@@ -0,0 +1,42 @@
1+name: "Common setup"
2+description: "Common setup steps"
3+inputs:
4+ authToken:
5+ description: "Cachix auth token"
6+ required: true
7+runs:
8+ using: "composite"
9+ steps:
10+ - name: Increase swap if linux
11+ shell: bash
12+ run: |
13+ if [ "$RUNNER_OS" == "Linux" ]; then
14+
15+ echo "Is Linux"
16+
17+ free -h
18+
19+ sudo swapoff /mnt/swapfile
20+ sudo fallocate -l 12G /mnt/swapfile
21+ sudo mkswap /mnt/swapfile
22+ sudo swapon /mnt/swapfile
23+
24+ free -h
25+
26+ else
27+
28+ echo "Not Linux"
29+
30+ fi
31+
32+ - name: Install Nix
33+ uses: cachix/install-nix-action@v27
34+ with:
35+ extra_nix_config: |
36+ system-features = kvm
37+ trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
38+ substituters = https://hydra.iohk.io https://cache.nixos.org/
39+ - uses: cachix/cachix-action@v15
40+ with:
41+ name: typeable
42+ authToken: "${{ inputs.authToken }}"
.github/workflows/github-actions-image.yamlnew file+290
@@ -0,0 +1,290 @@
1+name: Github Action Docker Image
2+
3+on:
4+ push:
5+ branches: [master]
6+ pull_request:
7+ branches: [master]
8+ release:
9+ types: [prereleased]
10+
11+jobs:
12+# check-formatting:
13+# name: Check Haskell formatting
14+# runs-on: ubuntu-latest
15+# steps:
16+# - uses: actions/checkout@v2
17+#
18+# - name: Common setup
19+# uses: ./.github/actions/common_setup
20+# with:
21+# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
22+#
23+# - name: Run fourmolu
24+# run: |
25+# nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace -o -XRecursiveDo -o -XTypeApplications -o -XPatternSynonyms -o -XBangPatterns"
26+# git diff --exit-code
27+
28+ check_versions:
29+ runs-on: ubuntu-latest
30+ steps:
31+ - uses: actions/checkout@v2
32+ with:
33+ submodules: recursive
34+
35+ - name: Common setup
36+ uses: ./.github/actions/common_setup
37+ with:
38+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
39+
40+ - name: Check library version
41+ if: github.event_name == 'release'
42+ run: |
43+ if [ "$(awk -F ':' '/^version/ {gsub(/ /, "", $2); print $2}' compaREST.cabal)" != "${{ github.event.release.tag_name }}" ]; then
44+ echo "::error file=compaREST.cabal::Library version does not match release version."
45+ exit 1
46+ fi
47+
48+ - name: Check docker tag
49+ if: github.event_name == 'release'
50+ run: |
51+ if [ "$(yq eval '.runs.image' action.yaml | awk -F ':' '{print $NF}')" != "${{ github.event.release.tag_name }}" ]; then
52+ echo "::error file=action.yaml::Action docker tag does not match release tag."
53+ exit 1
54+ fi
55+
56+ - name: Check changelog
57+ if: github.event_name == 'release'
58+ run: |
59+ if ! grep '^## ${{ github.event.release.tag_name }}$' CHANGELOG.md; then
60+ echo "::error file=CHANGELOG.md::CHANGELOG.md does not contain an entry for this release."
61+ exit 1
62+ fi
63+
64+ Linux:
65+ needs: check_versions
66+ runs-on: ubuntu-latest
67+ steps:
68+ - uses: actions/checkout@v2
69+ with:
70+ submodules: recursive
71+
72+ - name: Common setup
73+ uses: ./.github/actions/common_setup
74+ with:
75+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
76+
77+ - name: Test
78+ shell: bash
79+ run: |
80+ $(nix-build -A test -j auto --no-out-link)/bin/compaREST-tests
81+
82+ - name: Build Linux static binary
83+ shell: bash
84+ run: |
85+ cp $(nix-build -A compaRESTStaticBin -j auto --no-out-link)/bin/compaREST compaREST
86+
87+ - name: Upload compaREST Linux Binary
88+ uses: actions/upload-artifact@v2
89+ with:
90+ name: compaREST-Linux
91+ path: compaREST
92+
93+ - name: Build Docker Images
94+ shell: bash
95+ run: |
96+ cp $(nix-build -A compaRESTGithubAction -j auto --no-out-link) compaREST-Github-Action-image.tar.gz
97+ cp $(nix-build -A compaRESTImage -j auto --no-out-link) compaREST-image.tar.gz
98+
99+ - name: Upload compaREST Docker Github Action Image
100+ uses: actions/upload-artifact@v2
101+ with:
102+ name: compaREST Github Action Docker Image
103+ path: compaREST-Github-Action-image.tar.gz
104+
105+ - name: Upload compaREST Docker Image
106+ uses: actions/upload-artifact@v2
107+ with:
108+ name: compaREST Docker Image
109+ path: compaREST-image.tar.gz
110+
111+ - name: Login to DockerHub
112+ uses: docker/login-action@v1.10.0
113+ with:
114+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
115+ password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
116+
117+ - name: Push Nightly Docker Images to DockerHub
118+ if: github.ref == 'refs/heads/master' && github.event_name == 'push'
119+ shell: bash
120+ run: |
121+ skopeo copy docker-archive:$(nix-build -A compaRESTGithubAction -j auto --no-out-link) docker://typeable/comparest-github-action:nightly
122+ skopeo copy docker-archive:$(nix-build -A compaRESTImage -j auto --no-out-link) docker://typeable/comparest:nightly
123+
124+ - name: Push Release Docker Images to DockerHub
125+ if: github.event_name == 'release'
126+ shell: bash
127+ run: |
128+ skopeo copy docker-archive:$(nix-build -A compaRESTGithubAction -j auto --no-out-link) docker://typeable/comparest-github-action:${{ github.event.release.tag_name }}
129+ skopeo copy docker-archive:$(nix-build -A compaRESTImage -j auto --no-out-link) docker://typeable/comparest:${{ github.event.release.tag_name }}
130+
131+ - name: Zip Release Assets
132+ if: github.event_name == 'release'
133+ shell: bash
134+ run: |
135+ zip compaREST-Linux-x86.zip compaREST
136+
137+ - name: Upload compaREST Github Action Docker Image to release
138+ if: github.event_name == 'release'
139+ uses: svenstaro/upload-release-action@2.2.1
140+ with:
141+ repo_token: ${{ secrets.GITHUB_TOKEN }}
142+ file: compaREST-Github-Action-image.tar.gz
143+ tag: ${{ github.ref }}
144+
145+ - name: Upload compaREST Docker Image to release
146+ if: github.event_name == 'release'
147+ uses: svenstaro/upload-release-action@2.2.1
148+ with:
149+ repo_token: ${{ secrets.GITHUB_TOKEN }}
150+ file: compaREST-image.tar.gz
151+ tag: ${{ github.ref }}
152+
153+ - name: Upload compaREST binary – Linux – x86 to release
154+ if: github.event_name == 'release'
155+ uses: svenstaro/upload-release-action@2.2.1
156+ with:
157+ repo_token: ${{ secrets.GITHUB_TOKEN }}
158+ file: compaREST-Linux-x86.zip
159+ tag: ${{ github.ref }}
160+
161+ dist:
162+ needs: [check_versions, Linux]
163+ runs-on: ubuntu-latest
164+ steps:
165+ - uses: actions/checkout@v2
166+ with:
167+ submodules: recursive
168+
169+ - name: Common setup
170+ uses: ./.github/actions/common_setup
171+ with:
172+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
173+
174+ - name: Cache
175+ uses: actions/cache@v1
176+ with:
177+ path: |
178+ ~/.stack
179+ .stack-work
180+ key: ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }}-${{ hashFiles('**.cabal') }}
181+ restore-keys: |
182+ ${{ runner.os }}-stack-${{ hashFiles('stack.yaml') }}
183+ ${{ runner.os }}-stack
184+
185+ - name: Upgrade stack
186+ shell: bash
187+ # Needed for Hackage token authorization
188+ run: stack upgrade --git
189+
190+ - name: Build dist
191+ run: |
192+ stack sdist --pvp-bounds both
193+ echo "distPath=$(stack path | awk -F ": " '/dist-dir/ {print $2}')" >> $GITHUB_ENV
194+
195+ - name: Upload compaREST sources artifact
196+ uses: actions/upload-artifact@v2
197+ with:
198+ name: compaREST sources
199+ path: ${{ env.distPath }}/*.tar.gz
200+
201+ - name: Upload compaREST sources to release
202+ if: github.event_name == 'release'
203+ uses: svenstaro/upload-release-action@2.2.1
204+ with:
205+ repo_token: ${{ secrets.GITHUB_TOKEN }}
206+ file_glob: true
207+ file: ${{ env.distPath }}/*.tar.gz
208+ tag: ${{ github.ref }}
209+
210+ - name: Publish compaREST candidate to Hackage
211+ shell: bash
212+ if: github.event_name == 'release'
213+ env:
214+ HACKAGE_KEY: ${{ secrets.HACKAGE_KEY }}
215+ run: |
216+ stack upload --pvp-bounds both --candidate .
217+
218+ macOS:
219+ needs: check_versions
220+ runs-on: macos-latest
221+ steps:
222+ - uses: actions/checkout@v2
223+ with:
224+ submodules: recursive
225+
226+ - name: Common setup
227+ uses: ./.github/actions/common_setup
228+ with:
229+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
230+
231+ - name: Build macOS binary bundle
232+ run: |
233+ cp -LR $(nix-build -A macOSCompaRESTBundle -j auto --no-out-link) compaREST
234+
235+ - name: Upload macOS compaREST bundle
236+ uses: actions/upload-artifact@v2
237+ with:
238+ name: compaREST binary – macOS – x86
239+ path: compaREST
240+
241+ - name: Zip Release Assets
242+ if: github.event_name == 'release'
243+ shell: bash
244+ run: |
245+ zip -r compaREST-macOS-x86.zip compaREST
246+
247+ - name: Upload compaREST binary – macOS – x86 to release
248+ if: github.event_name == 'release'
249+ uses: svenstaro/upload-release-action@2.2.1
250+ with:
251+ repo_token: ${{ secrets.GITHUB_TOKEN }}
252+ file: compaREST-macOS-x86.zip
253+ tag: ${{ github.ref }}
254+
255+# Linux-Windows:
256+# needs: check_versions
257+# runs-on: ubuntu-latest
258+# steps:
259+# - uses: actions/checkout@v2
260+# with:
261+# submodules: recursive
262+#
263+# - name: Common setup
264+# uses: ./.github/actions/common_setup
265+# with:
266+# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
267+#
268+# - name: Build Windows binary
269+# run: |
270+# cp -LR $(nix-build -A WindowsCompaRESTBin -j auto --no-out-link)/bin compaREST
271+#
272+# - name: Upload Windows compaREST bundle
273+# uses: actions/upload-artifact@v2
274+# with:
275+# name: compaREST binary – Windows
276+# path: compaREST
277+#
278+# - name: Zip Release Assets
279+# if: github.event_name == 'release'
280+# shell: bash
281+# run: |
282+# zip -r compaREST-Windows.zip compaREST
283+#
284+# - name: Upload compaREST binary – Windows to release
285+# if: github.event_name == 'release'
286+# uses: svenstaro/upload-release-action@2.2.1
287+# with:
288+# repo_token: ${{ secrets.GITHUB_TOKEN }}
289+# file: compaREST-Windows.zip
290+# tag: ${{ github.ref }}
.github/workflows/haskell.ymldeleted−54
@@ -1,54 +0,0 @@
1−name: Haskell CI
2−
3−on:
4− push:
5− branches: [master]
6− pull_request:
7− branches: [master]
8−
9−jobs:
10− linux-build:
11− runs-on: ubuntu-latest
12−
13− steps:
14− - uses: actions/checkout@v2
15− with:
16− submodules: recursive
17− - name: Cache
18− uses: actions/cache@v1
19− with:
20− path: ~/.stack
21− key: ${{ runner.os }}-build-stack-${{ hashFiles('**/stack.yaml') }}-${{ hashFiles('**/*.cabal') }}
22− restore-keys: |
23− ${{ runner.os }}-build-stack-${{ hashFiles('**/stack.yaml') }}
24− ${{ runner.os }}-build-stack-
25− - name: Increase swap
26− run: |
27− free -h
28−
29− sudo swapoff /mnt/swapfile
30− sudo fallocate -l 12G /mnt/swapfile
31− sudo mkswap /mnt/swapfile
32− sudo swapon /mnt/swapfile
33−
34− free -h
35−
36− - name: Build
37− run: |
38− stack build --ghc-options=-O3 --local-bin-path openapi-diff-linux --copy-bins --flag openapi-diff:Static
39− - name: Test
40− run: stack test --ghc-options=-O3 --flag openapi-diff:Static --test-arguments='--hide-successes --no-create'
41−
42− - name: Zip
43− if: github.ref == 'refs/heads/master'
44− run: |
45− zip openapi-diff-linux openapi-diff-linux/openapi-diff
46− - uses: "marvinpinto/action-automatic-releases@latest"
47− if: github.ref == 'refs/heads/master'
48− with:
49− repo_token: "${{ secrets.GITHUB_TOKEN }}"
50− automatic_release_tag: "latest"
51− prerelease: false
52− title: "Pre-Release"
53− files: |
54− *.zip
.github/workflows/remove-artifacts.yamldeleted−17
@@ -1,17 +0,0 @@
1−name: Remove old artifacts
2−
3−on:
4− schedule:
5− - cron: '0 0 * * *'
6−jobs:
7− remove-old-artifacts:
8− runs-on: ubuntu-latest
9− timeout-minutes: 10
10−
11− steps:
12− - name: Remove old artifacts
13− uses: c-hive/gha-remove-artifacts@v1
14− with:
15− age: '7 days'
16− skip-tags: true
17− skip-recent: 10
.gitmodulesdeleted−3
@@ -1,3 +0,0 @@
1−[submodule "awsm-css"]
2− path = awsm-css
3− url = https://github.com/igoradamenko/awsm.css.git
CHANGELOG.md+7−3
@@ -1,9 +1,13 @@
1# Changelog
1# Changelog
2
2
3−`openapi-diff` uses [PVP Versioning][1].
3+`compaREST` uses [PVP Versioning][1].
4
4
5−## 0.0.0.0
5+## 0.1.0.1
6
6
7−* Initially created.
7+* Fixed an issue related to empty commit SHAs in GitHub Actions.
8+
9+## 0.1.0.0
10+
11+* Initial release.
8
12
9[1]: https://pvp.haskell.org
13[1]: https://pvp.haskell.org
LICENSE+1−1
@@ -1,6 +1,6 @@
1MIT License
1MIT License
2
2
3−Copyright (c) 2021 Aleksey Uymanov
3+Copyright (c) 2021 Typeable
4
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
6of this software and associated documentation files (the "Software"), to deal
README.md+326−4
@@ -1,8 +1,330 @@
1−# openapi-diff
1+# compaREST
2
2
3−[![Hackage](https://img.shields.io/hackage/v/openapi-diff.svg?logo=haskell)](https://hackage.haskell.org/package/openapi-diff)
3+[![Hackage](https://img.shields.io/hackage/v/compaREST.svg?logo=haskell)](https://hackage.haskell.org/package/compaREST)
4−[![Stackage Lts](http://stackage.org/package/openapi-diff/badge/lts)](http://stackage.org/lts/package/openapi-diff)
5−[![Stackage Nightly](http://stackage.org/package/openapi-diff/badge/nightly)](http://stackage.org/nightly/package/openapi-diff)
6[![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4[![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
5
8Compatibility checker for OpenAPI
6Compatibility checker for OpenAPI
7+
8+## Using compaREST in Github Actions
9+
10+Add the following step to your Github Actions workflow:
11+
12+```yaml
13+- uses: typeable/comparest
14+ if: ${{ github.event_name == 'pull_request' }}
15+ with:
16+ old: old-openapi.yaml
17+ new: new-openapi.yaml
18+```
19+
20+The `new` and `old` values should be paths to your OpenAPI specifications you want to compare.
21+
22+You will get something like this in your pull requests:
23+
24+![](docs/img/github-action-report.png)
25+
26+For more detail please see our [integration guide](docs/Integration_guide.md).
27+
28+## An example
29+
30+### Your situation
31+
32+You are developing a very important server with a REST API. You have clients who use your API that you do not control. Say, you are also developing a mobile app that uses your API and you can't force someone to update to the latest version. (Or you prefer not to for UX reasons.)
33+
34+You have recently released version 1.0 and things are going great: user are downloading your app, servers are processing requests.
35+
36+You describe your API in a file `api-1.0.0.yaml`:
37+
38+```yaml
39+openapi: "3.0.0"
40+info:
41+ version: 1.0.0
42+ title: Swagger Petstore
43+ license:
44+ name: MIT
45+servers:
46+ - url: https://example.com
47+paths:
48+ /pets:
49+ get:
50+ parameters:
51+ - name: limit
52+ in: query
53+ required: false
54+ schema:
55+ type: integer
56+ maximum: 20
57+ responses:
58+ "200":
59+ description: ""
60+ headers:
61+ x-next:
62+ schema:
63+ type: string
64+ content:
65+ application/json:
66+ schema:
67+ $ref: "#/components/schemas/Pets"
68+ post:
69+ requestBody:
70+ content:
71+ application/json:
72+ schema:
73+ $ref: "#/components/schemas/Pet"
74+ responses:
75+ "201":
76+ description: ""
77+components:
78+ schemas:
79+ Pet:
80+ type: object
81+ required:
82+ - id
83+ - name
84+ properties:
85+ id:
86+ type: integer
87+ name:
88+ type: string
89+ minLength: 3
90+ maxLength: 10
91+ Pets:
92+ type: array
93+ items:
94+ $ref: "#/components/schemas/Pet"
95+```
96+
97+### Evolving your product
98+
99+Enthused over your initial success you hurry to release a new and improved version of your API and mobile app.
100+
101+After a round of very intense programming you take a look at your new `api-1.1.0.yaml`:
102+
103+```yaml
104+openapi: "3.0.0"
105+info:
106+ version: 1.1.0
107+ title: Swagger Petstore
108+ license:
109+ name: MIT
110+servers:
111+ - url: https://example.com
112+paths:
113+ /pets:
114+ get:
115+ parameters:
116+ - name: limit
117+ in: query
118+ required: false
119+ schema:
120+ type: integer
121+ maximum: 30
122+ responses:
123+ "200":
124+ description: ""
125+ headers:
126+ x-next:
127+ schema:
128+ type: string
129+ content:
130+ application/json:
131+ schema:
132+ $ref: "#/components/schemas/Pets"
133+ post:
134+ requestBody:
135+ content:
136+ application/json:
137+ schema:
138+ $ref: "#/components/schemas/Pet"
139+ responses:
140+ "201":
141+ description: ""
142+components:
143+ schemas:
144+ Pet:
145+ type: object
146+ required:
147+ - id
148+ - name
149+ properties:
150+ id:
151+ type: integer
152+ name:
153+ type: string
154+ minLength: 1
155+ maxLength: 15
156+ weight:
157+ type: integer
158+ Pets:
159+ type: array
160+ items:
161+ $ref: "#/components/schemas/Pet"
162+```
163+
164+Looking at the very large and complex API description, you grow more and more concerned that your old mobile app might stop working when you update the server. But the spec is too large and too complex to reasonably assess this manually.
165+
166+### Assessing compatibility automatically
167+
168+Luckily, you have access to compaREST which can programmatically analyze your APIs and determine what, if anything, breaks compatibility and what doesn't.
169+
170+You can call it, passing the API your client will be aware of, and the API your server will serve like so:
171+
172+```bash
173+docker run --rm -v $(pwd):/data:rw typeable/comparest --client /data/api-1.0.0.yaml --server /data/api-1.1.0.yaml --output /data/report.md
174+```
175+
176+Running this command will output a file `report.md`, containing the compatibility report between the two APIs:
177+
178+> # Summary
179+>
180+> | [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
181+> |------------------------------------------|--------------------------------------------------|
182+> | 5 | 6 |
183+>
184+> # <span id="breaking-changes"></span>❌ Breaking changes
185+>
186+> ## **GET** /pets
187+>
188+> ### ⬅️☁️ JSON Response – 200
189+>
190+> #### `$[*].name(String)`
191+>
192+> 1. Maximum length of the string changed from 10 to 15.
193+>
194+> 2. Minimum length of the string changed from 3 to 1.
195+>
196+> ## **POST** /pets
197+>
198+> ### ➡️☁️ JSON Request
199+>
200+> #### `$.weight`
201+>
202+> 1. Values are now limited to the following types:
203+>
204+> - Number
205+>
206+> 2. The property was previously implicitly described by the catch-all
207+> "additional properties" case. It is now explicitly defined.
208+>
209+> #### `$.weight(Number)`
210+>
211+> Value is now a multiple of 1.0.
212+>
213+> # <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
214+>
215+> ## **GET** /pets
216+>
217+> ### Parameter limit
218+>
219+> #### JSON Schema
220+>
221+> ##### `$(Number)`
222+>
223+> Upper bound changed from 20.0 inclusive to 30.0 inclusive.
224+>
225+> ### ⬅️☁️ JSON Response – 200
226+>
227+> #### `$[*].weight`
228+>
229+> 1. Values are now limited to the following types:
230+>
231+> - Number
232+>
233+> 2. The property was previously implicitly described by the catch-all
234+> "additional properties" case. It is now explicitly defined.
235+>
236+> #### `$[*].weight(Number)`
237+>
238+> Value is now a multiple of 1.0.
239+>
240+> ## **POST** /pets
241+>
242+> ### ➡️☁️ JSON Request
243+>
244+> #### `$.name(String)`
245+>
246+> 1. Maximum length of the string changed from 10 to 15.
247+>
248+> 2. Minimum length of the string changed from 3 to 1.
249+
250+You now know exactly in what situations and in what way your 1.0 version of the app will break if you deploy your 1.1 version of the server.
251+
252+### Additional formats
253+
254+You can also produce a self-contained HTML report that you can open in your browser by simply omitting the file extension of the output file:
255+
256+```bash
257+docker run --rm -v $(pwd):/data:rw typeable/comparest --client /data/api-1.0.0.yaml --server /data/api-1.1.0.yaml --output /data/report
258+```
259+
260+## CLI docs
261+
262+For more detail please see our [user guide](docs/User_guide.md).
263+
264+```
265+Usage: comparest (-c|--client ARG) (-s|--server ARG)
266+ [--silent | --only-breaking | --all] [-o|--output ARG]
267+ [--folding-block-quotes-style | --header-style]
268+ [--signal-exit-code]
269+ A tool to check compatibility between two OpenAPI specifications.
270+
271+ Usage examples
272+
273+ Compare files old.yaml with new.yaml and output the resulting report to
274+ stdout:
275+
276+ comparest -c old.yaml -s new.yaml
277+
278+ Only output breaking changes and write a styled HTML report to file
279+ report.html:
280+
281+ comparest -c old.yaml -s new.yaml --only-breaking -o report
282+
283+ Don't output anything, only fail if there are breaking changes:
284+
285+ comparest -c old.json -s new.json --silent
286+
287+ Write full report suitable for embedding into a GitHub comment to
288+ report.html:
289+
290+ comparest -c old.json -s new.json --folding-block-quotes-style -o report.html
291+
292+Available options:
293+ -h,--help Show this help text
294+ -c,--client ARG A path to the file containing the specification that
295+ will be used for the client of the API. Can be either
296+ a YAML or JSON file.
297+ -s,--server ARG A path to the file containing the specification that
298+ will be used for the server of the API. Can be either
299+ a YAML or JSON file.
300+ --silent Silence all output. Only makes sense in combination
301+ with --signal-exit-code.
302+ --only-breaking Only report breaking changes in the output.
303+ --all Report both incompatible and compatible changes.
304+ Compatible changes will not trigger a failure exit
305+ code.
306+ -o,--output ARG The file path where the output should be written. If
307+ the option is omitted the result will be written to
308+ stdout.
309+
310+ The file extension is used to determine the type of
311+ the output file.
312+
313+ Supports many formats such as markdown, html, rtf,
314+ doc, txt, rst, and many more.
315+
316+ Leave out the extension to produce a self-contained
317+ HTML report with styling.
318+ --folding-block-quotes-style
319+ The report tree is structured using summary/detail
320+ HTML elements and indented using block quotes. This
321+ style renders well on GitHub.Intended for HTML output
322+ format. Markdown has rendering bugs on GitHub.
323+ --header-style The report tree is structured using increasing levels
324+ of headers.
325+ --signal-exit-code Signal API compatibility with the exit code.
326+
327+ Exit with 0 if there are no breaking changes.
328+ Exit with 1 if there are breaking changes.
329+ Exit with 2 if could not determine compatibility.
330+```
action.yamlnew file+48
@@ -0,0 +1,48 @@
1+name: compaREST
2+author: Typeable
3+description: API reliability checker
4+inputs:
5+ GITHUB_TOKEN:
6+ description: "The GitHub access token (e.g. secrets.GITHUB_TOKEN) used to create or update the comment. This defaults to {{ github.token }}."
7+ default: "${{ github.token }}"
8+ required: false
9+ repo:
10+ description: The owner of the repo in which to post the results.
11+ default: "${{ github.repository }}"
12+ required: false
13+ project_name:
14+ description: The name of the project to which the API pertains. Used to name the check. Useful when you have multiple APIs in the same repo.
15+ required: false
16+ default: ""
17+ footer:
18+ description: A footer that can be appended to the check body.
19+ required: false
20+ default: ""
21+ old:
22+ description: The path to the old (client) specification of the API.
23+ required: true
24+ new:
25+ description: The path to new (server) specification of the API.
26+ required: true
27+ sha:
28+ description: The sha of the commit to post the check for.
29+ required: false
30+ default: "${{ github.event.pull_request.head.sha }}"
31+runs:
32+ using: "docker"
33+ image: "docker://typeable/comparest-github-action:0.1.0.1"
34+ env:
35+ GITHUB_TOKEN: "${{ inputs.GITHUB_TOKEN }}"
36+ REPO: "${{ inputs.repo }}"
37+ PROJECT_NAME: "${{ inputs.project_name }}"
38+ FOOTER: "${{ inputs.footer }}"
39+ ROOT: "/github/workspace"
40+ OLD: "${{ inputs.old }}"
41+ NEW: "${{ inputs.new }}"
42+ SHA: "${{ inputs.sha }}"
43+ pre-entrypoint: "/bin/pre"
44+ entrypoint: "/bin/run"
45+
46+branding:
47+ icon: crosshair
48+ color: blue
app/Data/OpenApi/Compare/Options.hsnew file+135
@@ -0,0 +1,135 @@
1+module Data.OpenApi.Compare.Options
2+ ( Options (..),
3+ OutputMode (..),
4+ parseOptions,
5+ )
6+where
7+
8+import Data.OpenApi.Compare.Report
9+import GHC.Generics (Generic)
10+import Options.Applicative
11+import Options.Applicative.Help hiding (fullDesc)
12+import Text.PrettyPrint.ANSI.Leijen ((<$$>),string)
13+
14+parseOptions :: IO Options
15+parseOptions = customExecParser (prefs $ showHelpOnError) optionsParserInfo
16+
17+data Options = Options
18+ { clientFile :: FilePath
19+ , serverFile :: FilePath
20+ , -- | 'Nothing' means "don't produce any output"
21+ mode :: Maybe ReportMode
22+ , outputMode :: OutputMode
23+ , reportTreeStyle :: ReportTreeStyle
24+ , signalExitCode :: Bool
25+ }
26+ deriving stock (Generic)
27+
28+data OutputMode = StdoutMode | FileMode FilePath
29+
30+optionsParserInfo :: ParserInfo Options
31+optionsParserInfo =
32+ info
33+ (helper <*> optionsParser)
34+ ( fullDesc
35+ <> header "CompaREST"
36+ <> progDescDoc
37+ ( Just $
38+ par "A tool to check compatibility between two OpenAPI specifications."
39+ <$$> hardline <> par "Usage examples" <> hardline
40+ <$$> indent
41+ 4
42+ ( par "Compare files old.yaml with new.yaml and output the resulting report to stdout:"
43+ <$$> hardline <> indent 4 "comparest -c old.yaml -s new.yaml"
44+ <$$> hardline <> par "Only output breaking changes and write a styled HTML report to file report.html:"
45+ <$$> hardline <> indent 4 "comparest -c old.yaml -s new.yaml --only-breaking -o report"
46+ <$$> hardline <> par "Don't output anything, only fail if there are breaking changes:"
47+ <$$> hardline <> indent 4 "comparest -c old.json -s new.json --silent"
48+ <$$> hardline <> par "Write full report suitable for embedding into a GitHub comment to report.html:"
49+ <$$> hardline <> indent 4 "comparest -c old.json -s new.json --folding-block-quotes-style -o report.html"
50+ )
51+ )
52+ )
53+
54+optionsParser :: Parser Options
55+optionsParser =
56+ Options
57+ <$> strOption
58+ ( short 'c'
59+ <> long "client"
60+ <> help
61+ "A path to the file containing the specification that will be \
62+ \used for the client of the API. Can be either a YAML or JSON file."
63+ )
64+ <*> strOption
65+ ( short 's'
66+ <> long "server"
67+ <> help
68+ "A path to the file containing the specification that will be \
69+ \used for the server of the API. Can be either a YAML or JSON file."
70+ )
71+ <*> ( flag'
72+ Nothing
73+ ( long "silent"
74+ <> help "Silence all output. Only makes sense in combination with --signal-exit-code."
75+ )
76+ <|> flag'
77+ (Just OnlyErrors)
78+ ( long "only-breaking"
79+ <> help "Only report breaking changes in the output."
80+ )
81+ <|> flag'
82+ (Just All)
83+ ( long "all"
84+ <> help
85+ "Report both incompatible and compatible changes. \
86+ \Compatible changes will not trigger a failure exit code."
87+ )
88+ <|> pure (Just All)
89+ )
90+ <*> ( ( FileMode
91+ <$> strOption
92+ ( short 'o' <> long "output"
93+ <> helpDoc
94+ ( Just $
95+ par "The file path where the output should be written. If the option is omitted the result will be written to stdout."
96+ <$$> hardline <> par "The file extension is used to determine the type of the output file."
97+ <$$> hardline <> par "Supports many formats such as markdown, html, rtf, doc, txt, rst, and many more."
98+ <$$> hardline <> par "Leave out the extension to produce a self-contained HTML report with styling."
99+ )
100+ )
101+ )
102+ <|> pure StdoutMode
103+ )
104+ <*> ( flag'
105+ FoldingBlockquotesTreeStyle
106+ ( long "folding-block-quotes-style"
107+ <> help
108+ "The report tree is structured using \
109+ \summary/detail HTML elements and indented using \
110+ \block quotes. This style renders well on GitHub.\
111+ \Intended for HTML output format. Markdown has rendering \
112+ \bugs on GitHub."
113+ )
114+ <|> flag'
115+ HeadersTreeStyle
116+ ( long "header-style"
117+ <> help
118+ "The report tree is structured using \
119+ \increasing levels of headers."
120+ )
121+ <|> pure HeadersTreeStyle
122+ )
123+ <*> switch
124+ ( long "signal-exit-code"
125+ <> helpDoc
126+ ( Just $
127+ par "Signal API compatibility with the exit code."
128+ <$$> hardline <> par "Exit with 0 if there are no breaking changes."
129+ <$$> par "Exit with 1 if there are breaking changes."
130+ <$$> par "Exit with 2 if could not determine compatibility."
131+ )
132+ )
133+
134+par :: String -> Doc
135+par = foldr1 (</>) . fmap string . words
app/FormatHeuristic.hs+3−3
@@ -2,7 +2,7 @@
2-- Originally based on:
2-- Originally based on:
3-- https://github.com/jgm/pandoc/blob/master/src/Text/Pandoc/App/FormatHeuristics.hs
3-- https://github.com/jgm/pandoc/blob/master/src/Text/Pandoc/App/FormatHeuristics.hs
4module FormatHeuristic
4module FormatHeuristic
5− ( formatFromFilePath
5+ ( formatFromFilePath,
6 )
6 )
7where
7where
8
8
@@ -10,10 +10,10 @@ import Data.ByteString.Lazy (ByteString)
10import Data.Char (toLower)
10import Data.Char (toLower)
11import Data.Functor
11import Data.Functor
12import qualified Data.Map as M
12import qualified Data.Map as M
13+import qualified Data.OpenApi.Compare.Report.Html.Template as Html
13import Data.Text (Text)
14import Data.Text (Text)
14import qualified Data.Text.Lazy as TL
15import qualified Data.Text.Lazy as TL
15import qualified Data.Text.Lazy.Encoding as TL
16import qualified Data.Text.Lazy.Encoding as TL
16−import qualified OpenAPI.Checker.Report.Html.Template as Html
17import System.FilePath (takeExtension)
17import System.FilePath (takeExtension)
18import Text.DocTemplates.Internal
18import Text.DocTemplates.Internal
19import Text.Pandoc
19import Text.Pandoc
@@ -80,7 +80,7 @@ formatFromFilePath x =
80 def
80 def
81 { writerTemplate = Just Html.template
81 { writerTemplate = Just Html.template
82 , -- Not actually used. Needed to silence warning.
82 , -- Not actually used. Needed to silence warning.
83− writerVariables = Context $ M.fromList [("pagetitle", toVal ("OpenApi Diff" :: Text))]
83+ writerVariables = Context $ M.fromList [("pagetitle", toVal ("CompaREST" :: Text))]
84 }
84 }
85 _ -> Nothing
85 _ -> Nothing
86 where
86 where
app/Main.hs+11−8
@@ -2,22 +2,24 @@ module Main (main) where
2
2
3import Control.Monad
3import Control.Monad
4import Control.Monad.Except
4import Control.Monad.Except
5+import Control.Monad.Trans (lift)
5import Data.Aeson
6import Data.Aeson
6import qualified Data.ByteString.Lazy as BSL
7import qualified Data.ByteString.Lazy as BSL
7import Data.Default
8import Data.Default
8import Data.Maybe
9import Data.Maybe
10+import Data.OpenApi.Compare.Options
11+import Data.OpenApi.Compare.Run
9import qualified Data.Text.IO as T
12import qualified Data.Text.IO as T
10import qualified Data.Yaml as Yaml
13import qualified Data.Yaml as Yaml
11import FormatHeuristic
14import FormatHeuristic
12−import OpenAPI.Checker.Options
13−import OpenAPI.Checker.Run
14import System.Exit
15import System.Exit
15import System.IO
16import System.IO
16import Text.Pandoc hiding (report)
17import Text.Pandoc hiding (report)
18+import Text.Pandoc.Builder
17
19
18main :: IO ()
20main :: IO ()
19main = do
21main = do
20− opts <- execParser optionsParserInfo
22+ opts <- parseOptions
21 let parseSchema path =
23 let parseSchema path =
22 eitherDecodeFileStrict path >>= \case
24 eitherDecodeFileStrict path >>= \case
23 Left jsonErr -> do
25 Left jsonErr -> do
@@ -47,12 +49,13 @@ main = do
47 }
49 }
48 (report, status) = runReport reportConfig (a, b)
50 (report, status) = runReport reportConfig (a, b)
49 case mode opts of
51 case mode opts of
50− Just _ -> either handler pure <=< runExceptT $ write report
52+ Just _ -> either handler pure <=< runExceptT $ write $ doc report
51 Nothing -> pure ()
53 Nothing -> pure ()
54+ when (signalExitCode opts) $
52− case status of
55+ case status of
53− NoBreakingChanges -> exitSuccess
56+ NoBreakingChanges -> exitSuccess
54− BreakingChanges -> exitWith $ ExitFailure 1
57+ BreakingChanges -> exitWith $ ExitFailure 1
55− OnlyUnsupportedChanges -> exitWith $ ExitFailure 2
58+ OnlyUnsupportedChanges -> exitWith $ ExitFailure 2
56
59
57data Errors
60data Errors
58 = DocumentError PandocError
61 = DocumentError PandocError
app/OpenAPI/Checker/Options.hsdeleted−81
@@ -1,81 +0,0 @@
1−module OpenAPI.Checker.Options
2− ( Options (..)
3− , OutputMode (..)
4− , optionsParserInfo
5− , execParser
6− )
7−where
8−
9−import GHC.Generics (Generic)
10−import OpenAPI.Checker.Report
11−import Options.Applicative
12−
13−data Options = Options
14− { clientFile :: FilePath
15− , serverFile :: FilePath
16− , -- | 'Nothing' means "don't produce any output"
17− mode :: Maybe ReportMode
18− , outputMode :: OutputMode
19− , reportTreeStyle :: ReportTreeStyle
20− }
21− deriving stock (Generic)
22−
23−data OutputMode = StdoutMode | FileMode FilePath
24−
25−optionsParserInfo :: ParserInfo Options
26−optionsParserInfo =
27− info
28− (helper <*> optionsParser)
29− (fullDesc
30− <> header "openapi-diff"
31− <> progDesc "A tool to check compatibility between two OpenApi specifications.")
32−
33−optionsParser :: Parser Options
34−optionsParser =
35− Options
36− <$> strOption
37− (short 'c'
38− <> long "client"
39− <> help "The specification that will be used for the client of the API.")
40− <*> strOption
41− (short 's'
42− <> long "server"
43− <> help "The specification that will be used for the server of the API.")
44− <*> (flag'
45− Nothing
46− (long "silent"
47− <> help "Silence all output.")
48− <|> flag'
49− (Just OnlyErrors)
50− (long "only-errors"
51− <> help "Only report incompatibility errors in the output.")
52− <|> flag'
53− (Just All)
54− (long "all"
55− <> help
56− "Report both incompatible and compatible changes. \
57− \Compatible changes will not trigger a failure exit code.")
58− <|> pure (Just All))
59− <*> ((FileMode
60− <$> strOption
61− (short 'o' <> long "output"
62− <> help
63− "The file path where the output should be writtrn. \
64− \Leave blank to output result to stdout."))
65− <|> pure StdoutMode)
66− <*> (flag'
67− FoldingBlockquotesTreeStyle
68− (long "folding-block-quotes-style"
69− <> help
70− "The report tree is structured using \
71− \summary/detail HTML elements and indented using \
72− \block quotes. This style renders well on GitHub.\
73− \Intended for HTML output format. Markdown has rendering \
74− \bugs on GitHub.")
75− <|> flag'
76− HeadersTreeStyle
77− (long "header-style"
78− <> help
79− "The report tree is structured using \
80− \increasing levels of headers.")
81− <|> pure HeadersTreeStyle)
awsm-cssdeleted−1

Submodule changed.

benchmarks/README.md+2−2
@@ -1,6 +1,6 @@
1# OpenApi comparison benchmarks
1# OpenApi comparison benchmarks
2
2
3−This is a nix expression that automatically downloads, builds and runs multiple OpenApi comparison tools on the same set of inputs to compare how well they behave.
3+This is a nix expression that automatically downloads, builds and runs multiple OpenAPI comparison tools on the same set of inputs to compare how well they behave.
4
4
5To run the benchmarks run the following command in this directory:
5To run the benchmarks run the following command in this directory:
6
6
@@ -12,7 +12,7 @@ nix-build
12
12
13| Tool | Output file name |
13| Tool | Output file name |
14| -------------------------------------------------------------------------- | ------------------------------------------- |
14| -------------------------------------------------------------------------- | ------------------------------------------- |
15−| [Typeable OpenApi Diff](https://github.com/typeable/openapi-diff) | `typeable.md` |
15+| [Typeable CompaREST](https://github.com/typeable/compaREST) | `typeable.md` |
16| [Atlassian OpenApi Diff](https://bitbucket.org/atlassian/openapi-diff) | `atlassian.json`, `atlassian.error.txt` |
16| [Atlassian OpenApi Diff](https://bitbucket.org/atlassian/openapi-diff) | `atlassian.json`, `atlassian.error.txt` |
17| [OpenApi Tools OpenApi Diff](https://github.com/OpenAPITools/openapi-diff) | `openapitools.md`, `openapitools.error.txt` |
17| [OpenApi Tools OpenApi Diff](https://github.com/OpenAPITools/openapi-diff) | `openapitools.md`, `openapitools.error.txt` |
18| [Tufin OpenApi Diff](https://github.com/tufin/oasdiff) | `oasdiff.txt`, `oasdiff.error.txt` |
18| [Tufin OpenApi Diff](https://github.com/tufin/oasdiff) | `oasdiff.txt`, `oasdiff.error.txt` |
benchmarks/default.nix+8−12
@@ -1,6 +1,6 @@
1{ sources ? import ./nix/sources.nix
1{ sources ? import ./nix/sources.nix
2−, haskellNix ? import sources.haskellNix { }
3, gomod2nix ? sources.gomod2nix
2, gomod2nix ? sources.gomod2nix
3+, haskellNix ? (import ../. {}).haskellNix
4, pkgs ? import haskellNix.sources.nixpkgs-2105 (haskellNix.nixpkgsArgs // {
4, pkgs ? import haskellNix.sources.nixpkgs-2105 (haskellNix.nixpkgsArgs // {
5 overlays = haskellNix.nixpkgsArgs.overlays ++ [
5 overlays = haskellNix.nixpkgsArgs.overlays ++ [
6 (self: super: {
6 (self: super: {
@@ -8,23 +8,19 @@
8 })
8 })
9 ];
9 ];
10 })
10 })
11+, compaRESThsPkgs ? (import ../. {}).hsPkgs
11, npmNix ? import (sources.npmNix + "/npmPackages") { inherit pkgs; }
12, npmNix ? import (sources.npmNix + "/npmPackages") { inherit pkgs; }
12, mavenix ? import (sources.mavenix) { inherit pkgs; }
13, mavenix ? import (sources.mavenix) { inherit pkgs; }
13, bumpToken ? null
14, bumpToken ? null
14, bumpDocumentation ? null
15, bumpDocumentation ? null
15}:
16}:
16let
17let
17− typeable-openapi-diff = (pkgs.haskell-nix.stackProject {
18+ typeable-comparest = compaRESThsPkgs.comparest.components.exes.comparest;
18− src = pkgs.haskell-nix.haskellLib.cleanGit {
19− name = "openapi-diff";
20− src = ./..;
21− };
22− }).openapi-diff.components.exes.openapi-diff;
23− typeable-openapi-diff-exe = typeable-openapi-diff + "/bin/openapi-diff";
19+ typeable-comparest-exe = typeable-comparest + "/bin/comparest";
24− typeable-openapi-diff-differ = pkgs.writeScript "typeable-openapi-diff-differ" ''
20+ typeable-comparest-differ = pkgs.writeScript "typeable-comparest-differ" ''
25 #!${pkgs.stdenv.shell}
21 #!${pkgs.stdenv.shell}
26 set +e
22 set +e
27− ${typeable-openapi-diff-exe} -c $1 -s $2 -o $3/typeable.md
23+ ${typeable-comparest-exe} -c $1 -s $2 -o $3/typeable.md
28 exit 0
24 exit 0
29 '';
25 '';
30
26
@@ -130,7 +126,7 @@ let
130 })
126 })
131 (getInputs root);
127 (getInputs root);
132 differs = [
128 differs = [
133− typeable-openapi-diff-differ
129+ typeable-comparest-differ
134 atlassian-openapi-diff-differ
130 atlassian-openapi-diff-differ
135 openapitools-openapi-diff-differ
131 openapitools-openapi-diff-differ
136 oasdiff-differ
132 oasdiff-differ
@@ -164,7 +160,7 @@ let
164 tools = [
160 tools = [
165 "Typeable"
161 "Typeable"
166 "Atlassian"
162 "Atlassian"
167− "OpenApi Tools"
163+ "OpenAPI Tools"
168 "oasdiff"
164 "oasdiff"
169 "Bump.sh"
165 "Bump.sh"
170 ];
166 ];
ci/policeman.nixnew file+4
@@ -0,0 +1,4 @@
1+{ sources ? import ../nix/sources.nix
2+, pkgs ? import sources.nixpkgs { }
3+}:
4+pkgs.haskellPackages.policeman
openapi-diff.cabal → compaREST.cabalrenamed+89−53
@@ -1,29 +1,24 @@
1cabal-version: 2.4
1cabal-version: 2.4
2−name: openapi-diff
2+name: compaREST
3−version: 0.0.0.0
3+version: 0.1.0.1
4synopsis: Compatibility checker for OpenAPI
4synopsis: Compatibility checker for OpenAPI
5−description: Compatibility checker for OpenAPI
5+description: Compatibility checker for OpenAPI.
6license: MIT
6license: MIT
7license-file: LICENSE
7license-file: LICENSE
8−author: Aleksey Uymanov
8+author: Typeable
9−maintainer: Aleksey Uymanov <s9gf4ult@gmail.com>
9+maintainer: compaREST@typeable.io
10−copyright: 2021 Aleksey Uymanov
10+copyright: 2021 Typeable
11category: Web
11category: Web
12build-type: Simple
12build-type: Simple
13extra-doc-files: README.md
13extra-doc-files: README.md
14 CHANGELOG.md
14 CHANGELOG.md
15−tested-with: GHC == 8.10.4
15+tested-with: GHC == 9.6
16−extra-source-files: awsm-css/dist/awsm.min.css
16+extra-source-files: css/awsm.min.css
17−
17+extra-source-files:
18−Flag static
18+ test/golden/**/*.yaml
19− Default: False
19+ test/golden/**/*.md
20− Manual: True
21
20
22common common-options
21common common-options
23− if flag(static)
24− cc-options: -static
25− ld-options: -static -pthread
26− ghc-options: -static
27 ghc-options:
22 ghc-options:
28 -Weverything
23 -Weverything
29 -Wno-implicit-prelude
24 -Wno-implicit-prelude
@@ -31,16 +26,17 @@ common common-options
31 -Wno-safe
26 -Wno-safe
32 -Wno-prepositive-qualified-module
27 -Wno-prepositive-qualified-module
33 -Wno-missing-import-lists
28 -Wno-missing-import-lists
29+ -Wno-missing-kind-signatures
34 -Wno-partial-fields
30 -Wno-partial-fields
35 -Wno-all-missed-specialisations
31 -Wno-all-missed-specialisations
36 -Wno-missing-local-signatures
32 -Wno-missing-local-signatures
37 -Wno-unsafe
33 -Wno-unsafe
38 -fconstraint-solver-iterations=0
34 -fconstraint-solver-iterations=0
39 default-language: Haskell2010
35 default-language: Haskell2010
40− build-depends: base >= 4.12.0.0 && < 4.16
36+ build-depends: base >= 4.12.0.0 && < 5
41− , bytestring
42 , text
37 , text
43
38
39+
44 default-extensions: ApplicativeDo
40 default-extensions: ApplicativeDo
45 , BangPatterns
41 , BangPatterns
46 , ConstraintKinds
42 , ConstraintKinds
@@ -82,6 +78,7 @@ common common-options
82 , QuantifiedConstraints
78 , QuantifiedConstraints
83 , DefaultSignatures
79 , DefaultSignatures
84
80
81+
85library
82library
86 import: common-options
83 import: common-options
87 build-depends:
84 build-depends:
@@ -111,45 +108,53 @@ library
111 , file-embed
108 , file-embed
112 , data-default
109 , data-default
113 , ordered-containers
110 , ordered-containers
111+ , bytestring
114 hs-source-dirs: src
112 hs-source-dirs: src
115− exposed-modules: Data.HList
113+ exposed-modules: Data.OpenApi.Compare.Behavior
116− , OpenAPI.Checker.Behavior
114+ , Data.OpenApi.Compare.Common
117− , OpenAPI.Checker.Formula
115+ , Data.OpenApi.Compare.Formula
118− , OpenAPI.Checker.Memo
116+ , Data.OpenApi.Compare.Memo
119− , OpenAPI.Checker.Orphans
117+ , Data.OpenApi.Compare.Orphans
120− , OpenAPI.Checker.Paths
118+ , Data.OpenApi.Compare.Paths
121− , OpenAPI.Checker.PathsPrefixTree
119+ , Data.OpenApi.Compare.PathsPrefixTree
122− , OpenAPI.Checker.References
120+ , Data.OpenApi.Compare.References
123− , OpenAPI.Checker.Subtree
121+ , Data.OpenApi.Compare.Report
122+ , Data.OpenApi.Compare.Report.Html.Template
123+ , Data.OpenApi.Compare.Report.Jet
124+ , Data.OpenApi.Compare.Run
125+ , Data.OpenApi.Compare.Subtree
126+ , Data.OpenApi.Compare.Validate.Header
127+ , Data.OpenApi.Compare.Validate.Link
124− , OpenAPI.Checker.Validate.MediaTypeObject
128+ , Data.OpenApi.Compare.Validate.MediaTypeObject
129+ , Data.OpenApi.Compare.Validate.OAuth2Flows
125− , OpenAPI.Checker.Validate.OpenApi
130+ , Data.OpenApi.Compare.Validate.OpenApi
126− , OpenAPI.Checker.Validate.Operation
131+ , Data.OpenApi.Compare.Validate.Operation
127− , OpenAPI.Checker.Validate.Param
132+ , Data.OpenApi.Compare.Validate.Param
128− , OpenAPI.Checker.Validate.PathFragment
133+ , Data.OpenApi.Compare.Validate.PathFragment
129− , OpenAPI.Checker.Validate.Products
134+ , Data.OpenApi.Compare.Validate.Products
130− , OpenAPI.Checker.Validate.RequestBody
135+ , Data.OpenApi.Compare.Validate.RequestBody
131− , OpenAPI.Checker.Validate.Responses
136+ , Data.OpenApi.Compare.Validate.Responses
137+ , Data.OpenApi.Compare.Validate.Schema
132− , OpenAPI.Checker.Validate.Schema
138+ , Data.OpenApi.Compare.Validate.Schema.DNF
139+ , Data.OpenApi.Compare.Validate.Schema.Issues
140+ , Data.OpenApi.Compare.Validate.Schema.JsonFormula
141+ , Data.OpenApi.Compare.Validate.Schema.Partition
142+ , Data.OpenApi.Compare.Validate.Schema.Process
143+ , Data.OpenApi.Compare.Validate.Schema.Traced
144+ , Data.OpenApi.Compare.Validate.Schema.TypedJson
133− , OpenAPI.Checker.Validate.SecurityRequirement
145+ , Data.OpenApi.Compare.Validate.SecurityRequirement
146+ , Data.OpenApi.Compare.Validate.SecurityScheme
134− , OpenAPI.Checker.Validate.Server
147+ , Data.OpenApi.Compare.Validate.Server
135− , OpenAPI.Checker.Validate.Sums
148+ , Data.OpenApi.Compare.Validate.Sums
136− , OpenAPI.Checker.Validate.Header
149+ other-modules: Data.HList
137− , OpenAPI.Checker.Validate.Link
138− , OpenAPI.Checker.Common
139− , OpenAPI.Checker.Validate.SecurityScheme
140− , OpenAPI.Checker.Validate.OAuth2Flows
141− , OpenAPI.Checker.Report
142 , Data.OpenUnion.Extra
150 , Data.OpenUnion.Extra
143− , OpenAPI.Checker.Report.Jet
144− , OpenAPI.Checker.Run
145− , OpenAPI.Checker.Report.Html.Template
146
151
147−executable openapi-diff
152+executable compaREST
148 import: common-options
153 import: common-options
149 hs-source-dirs: app
154 hs-source-dirs: app
150 main-is: Main.hs
155 main-is: Main.hs
151 build-depends:
156 build-depends:
152− openapi-diff
157+ compaREST
153 , pandoc
158 , pandoc
154 , data-default
159 , data-default
155 , bytestring
160 , bytestring
@@ -160,14 +165,42 @@ executable openapi-diff
160 , aeson
165 , aeson
161 , containers
166 , containers
162 , doctemplates
167 , doctemplates
168+ , pandoc-types
169+ , prettyprinter-compat-ansi-wl-pprint
163 ghc-options: -threaded
170 ghc-options: -threaded
164 -rtsopts
171 -rtsopts
165 -with-rtsopts=-N
172 -with-rtsopts=-N
166 other-modules:
173 other-modules:
167 FormatHeuristic
174 FormatHeuristic
168− OpenAPI.Checker.Options
175+ Data.OpenApi.Compare.Options
176+
177+executable compaREST-GitHub-Action
178+ import: common-options
179+ hs-source-dirs: github-action
180+ main-is: Main.hs
181+ build-depends:
182+ compaREST
183+ , pandoc
184+ , yaml
185+ , aeson
186+ , github
187+ , freer-simple
188+ , vector
189+ , pandoc-types
190+ , envy
191+ , filepath
192+ , bytestring
193+ ghc-options: -threaded
194+ -rtsopts
195+ -with-rtsopts=-N
196+ other-modules:
197+ Control.Monad.Freer.GitHub
198+ CompaREST.GitHub.API
199+ CompaREST.GitHub.Action.Config
200+ GitHub.Data.Checks
201+ GitHub.Endpoints.Checks
169
202
170−test-suite openapi-diff-test
203+test-suite compaREST-tests
171 import: common-options
204 import: common-options
172 type: exitcode-stdio-1.0
205 type: exitcode-stdio-1.0
173 hs-source-dirs: test
206 hs-source-dirs: test
@@ -175,8 +208,10 @@ test-suite openapi-diff-test
175 other-modules:
208 other-modules:
176 Spec.Golden.Extra
209 Spec.Golden.Extra
177 Spec.Golden.TraceTree
210 Spec.Golden.TraceTree
211+ Paths_compaREST
212+ autogen-modules: Paths_compaREST
178 build-depends:
213 build-depends:
179− , openapi-diff
214+ , compaREST
180 , tasty-golden
215 , tasty-golden
181 , tasty
216 , tasty
182 , bytestring
217 , bytestring
@@ -186,6 +221,7 @@ test-suite openapi-diff-test
186 , pandoc
221 , pandoc
187 , data-default
222 , data-default
188 , lens
223 , lens
224+ , pandoc-types
189 ghc-options: -threaded
225 ghc-options: -threaded
190 -rtsopts
226 -rtsopts
191 -with-rtsopts=-N
227 -with-rtsopts=-N
css/awsm.min.cssnew file+1
@@ -0,0 +1 @@
1+@charset "UTF-8";html{font-family:"PT Serif",Georgia,serif;font-size:100%;line-height:1.6;background:#fff;color:#000;-webkit-overflow-scrolling:touch}body{margin:1em;font-size:1rem}@media (min-width:20rem){body{font-size:calc(1rem + .0125*(100vw - 20rem))}}@media (min-width:40rem){body{font-size:1.25rem}}body article,body footer,body header,body main{position:relative;max-width:40rem;margin:0 auto}body>header{margin-bottom:3.5em}body>header h1{margin:0;font-size:1.5em}body>header p{margin:0;font-size:.85em}body>footer{margin-top:4.5em;padding-bottom:1.5em;text-align:center;font-size:.8rem;color:#aaa}details,nav,p{margin:1em 0}nav ul{list-style:none;margin:0;padding:0}nav li{display:inline-block;margin-right:1em;margin-bottom:.25em}nav a:visited{color:#0064c1}article header h1 a:visited:hover,article header h2 a:visited:hover,nav a:hover{color:#3000c1}ol,ul{margin-top:0;padding-top:0;padding-left:2.5em}p{-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}aside:first-child,p:first-child{margin-top:0}aside:last-child,p:last-child{margin-bottom:0}p+ol,p+ul{margin-top:-.75em}p img,p picture{float:right;margin-bottom:.5em;margin-left:.5em}p picture img{float:none;margin:0}blockquote,dd{padding-left:2.5em}dd{margin-bottom:1em;margin-left:0}dt{font-weight:700}blockquote{margin:0}aside{margin:.5em 0;font-style:italic;color:#aaa}@media (min-width:65rem){aside{position:absolute;right:-12.5rem;width:9.375rem;max-width:9.375rem;margin:0;padding-left:.5em;font-size:.8em;border-left:1px solid #f2f2f2}}section+section{margin-top:2em}h1,h2,h3,h4,h5,h6{margin:1.25em 0 0;line-height:1.2}h1:focus>a[href^="#"][id],h1:hover>a[href^="#"][id],h2:focus>a[href^="#"][id],h2:hover>a[href^="#"][id],h3:focus>a[href^="#"][id],h3:hover>a[href^="#"][id],h4:focus>a[href^="#"][id],h4:hover>a[href^="#"][id],h5:focus>a[href^="#"][id],h5:hover>a[href^="#"][id],h6:focus>a[href^="#"][id],h6:hover>a[href^="#"][id]{opacity:1;transition:opacity 0s,color 300ms ease-out}figure+p,h1+details,h1+p,h2+details,h2+p,h3+details,h3+p,h4+details,h4+p,h5+details,h5+p,h6+details,h6+p{margin-top:.5em}h1>a[href^="#"][id],h2>a[href^="#"][id],h3>a[href^="#"][id],h4>a[href^="#"][id],h5>a[href^="#"][id],h6>a[href^="#"][id]{position:absolute;left:-.5em;opacity:0;background:0 0;color:rgba(0,0,0,.5);transition:opacity 300ms ease-out}h1>a[href^="#"][id]:focus,h1>a[href^="#"][id]:hover,h1>a[href^="#"][id]:target,h2>a[href^="#"][id]:focus,h2>a[href^="#"][id]:hover,h2>a[href^="#"][id]:target,h3>a[href^="#"][id]:focus,h3>a[href^="#"][id]:hover,h3>a[href^="#"][id]:target,h4>a[href^="#"][id]:focus,h4>a[href^="#"][id]:hover,h4>a[href^="#"][id]:target,h5>a[href^="#"][id]:focus,h5>a[href^="#"][id]:hover,h5>a[href^="#"][id]:target,h6>a[href^="#"][id]:focus,h6>a[href^="#"][id]:hover,h6>a[href^="#"][id]:target{opacity:1;box-shadow:none;color:#000;transition:opacity 0s,color 0s}h1>a[href^="#"][id]:target:focus,h2>a[href^="#"][id]:target:focus,h3>a[href^="#"][id]:target:focus,h4>a[href^="#"][id]:target:focus,h5>a[href^="#"][id]:target:focus,h6>a[href^="#"][id]:target:focus{outline:0}h1>a[href^="#"][id]::before,h2>a[href^="#"][id]::before,h3>a[href^="#"][id]::before,h4>a[href^="#"][id]::before,h5>a[href^="#"][id]::before,h6>a[href^="#"][id]::before{content:"•"}h1{font-size:2em}h2{font-size:1.75em}h3{font-size:1.25em}h4{font-size:1.15em}h5,h6{font-size:1em}h6{margin-top:1em;color:#aaa}article+article{margin-top:5em}article header p{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:.6em;color:#aaa}article header p+h1,article header p+h2{margin-top:-.25em}article header h1+p,article header h2+p{margin-top:.25em}article header h1 a,article header h2 a{color:#000;background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"%3E%3Crect x="0" y="0.5" width="1" height="0.5" fill="rgba(0, 0, 0, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){article header h1 a,article header h2 a{background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="2"%3E%3Crect x="0" y="1" width="1" height="1" fill="rgba(0, 0, 0, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}}article header h1 a:visited,article header h2 a:visited{color:#aaa;background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"%3E%3Crect x="0" y="0.5" width="1" height="0.5" fill="rgba(170, 170, 170, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){article header h1 a:visited,article header h2 a:visited{background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="2"%3E%3Crect x="0" y="1" width="1" height="1" fill="rgba(170, 170, 170, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}}article>footer{margin-top:1.5em;font-size:.85em}a,abbr{text-decoration:none}a{color:#0064c1;transition:color 300ms ease-out,box-shadow 300ms ease-out;background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"%3E%3Crect x="0" y="0.5" width="1" height="0.5" fill="rgba(0, 100, 193, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){a{background:url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="2"%3E%3Crect x="0" y="1" width="1" height="1" fill="rgba(0, 100, 193, 0.35)"/%3E%3C/svg%3E') left bottom repeat-x}}a:visited{color:#80b2e0}a:active,a:hover{outline-width:0}a:hover{box-shadow:inset 0 -1px #3000c1;color:#3000c1;transition:color 0s,box-shadow 0s}abbr{margin-right:-.075em;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none;letter-spacing:.075em;font-size:.9em;border-bottom:1px dotted}img,picture{display:block;max-width:100%;margin:0 auto}audio,video{width:100%;max-width:100%}video{-o-object-position:0 -1px;object-position:0 -1px}figure{margin:1em 0 .5em;padding:0}figure figcaption{opacity:.65;font-size:.85em}table{display:inline-block;border-spacing:0;border-collapse:collapse;overflow-x:auto;max-width:100%;text-align:left;vertical-align:top;background:linear-gradient(rgba(0,0,0,.15) 0%,rgba(0,0,0,.15) 100%) 0 0,linear-gradient(rgba(0,0,0,.15) 0%,rgba(0,0,0,.15) 100%) 100% 0;background-attachment:scroll,scroll;background-size:1px 100%,1px 100%;background-repeat:no-repeat,no-repeat}table caption{font-size:.9em;background:#fff}table td,table th{padding:.35em .75em;vertical-align:top;font-size:.9em;border:1px solid #f2f2f2;border-top:0;border-left:0}table td:first-child,table th:first-child{padding-left:0;background-image:linear-gradient(to right,#fff 50%,rgba(255,255,255,0) 100%);background-size:2px 100%;background-repeat:no-repeat}table td:last-child,table th:last-child{padding-right:0;border-right:0;background-image:linear-gradient(to left,#fff 50%,rgba(255,255,255,0) 100%);background-position:100% 0;background-size:2px 100%;background-repeat:no-repeat}table td:only-child,table th:only-child{background-image:linear-gradient(to right,#fff 50%,rgba(255,255,255,0) 100%),linear-gradient(to left,#fff 50%,rgba(255,255,255,0) 100%);background-position:0 0,100% 0;background-size:2px 100%,2px 100%;background-repeat:no-repeat,no-repeat}table th{line-height:1.2}code,kbd,samp,var{font-family:Consolas,"Lucida Console",Monaco,monospace;font-style:normal}pre{overflow-x:auto;font-size:.8em;background:linear-gradient(rgba(0,0,0,.15) 0%,rgba(0,0,0,.15) 100%) 0 0,linear-gradient(rgba(0,0,0,.15) 0%,rgba(0,0,0,.15) 100%) 100% 0;background-attachment:scroll,scroll;background-size:1px 100%,1px 100%;background-repeat:no-repeat,no-repeat}pre>code,summary{display:inline-block}pre>code{overflow-x:visible;box-sizing:border-box;min-width:100%;border-right:3px solid #fff;border-left:1px solid #fff}hr{height:1px;margin:2em 0;border:0;background:#f2f2f2}details[open]{padding-bottom:.5em;border-bottom:1px solid #f2f2f2}summary{font-weight:700;border-bottom:1px dashed;cursor:pointer}summary::-webkit-details-marker{display:none}noscript{color:#d00000}::selection{background:rgba(0,100,193,.15)}
default.nixnew file+125
@@ -0,0 +1,125 @@
1+{ sources ? import ./nix/sources.nix
2+, haskellNix ? import sources.haskellNix { inherit system; }
3+, pkgs ? import haskellNix.sources.nixpkgs-2311 (haskellNix.nixpkgsArgs // { inherit system; })
4+, system ? builtins.currentSystem
5+, nix-filter ? import sources.nix-filter
6+}:
7+let
8+ masterPkgs = import sources.nixpkgs { inherit system; };
9+ yq = import ./nix/yq.nix { pkgs = masterPkgs; };
10+ diffYaml = import ./nix/diff-yaml.nix { pkgs = masterPkgs; };
11+
12+ hsPkgs = pkgs.haskell-nix.stackProject {
13+ src = nix-filter {
14+ root = ./.;
15+ name = "compaREST";
16+ include = [
17+ ./stack.yaml
18+ ./stack.yaml.lock
19+ ./compaREST.cabal
20+ ];
21+ };
22+
23+ modules = [
24+ {
25+ dontStrip = false;
26+ dontPatchELF = false;
27+ enableDeadCodeElimination = true;
28+ ghcOptions = [
29+ "-O2"
30+ "-fexpose-all-unfoldings"
31+ "-fspecialise-aggressively"
32+ ];
33+ packages.pandoc.ghcOptions = [ "-O1" ];
34+ packages.compaREST.src = nix-filter {
35+ root = ./.;
36+ name = "compaREST-src";
37+ include = with nix-filter; [
38+ (./compaREST.cabal)
39+ (inDirectory ./test)
40+ (inDirectory ./src)
41+ (inDirectory ./app)
42+ (inDirectory ./github-action)
43+ ./css/awsm.min.css
44+ ./LICENSE
45+ ];
46+ };
47+ }
48+ ];
49+ };
50+
51+ staticify = name: drv: pkgs.runCommand name { } ''
52+ mkdir -p $out/bin
53+ cp ${drv + "/bin"}/* $out/bin
54+
55+ ${pkgs.nukeReferences}/bin/nuke-refs $out/bin/*
56+ '';
57+
58+ compaRESTBin = hsPkgs.compaREST.components.exes.compaREST;
59+ compaRESTStaticBin = (staticify "compaREST-static" hsPkgs.projectCross.musl64.hsPkgs.compaREST.components.exes.compaREST);
60+
61+
62+ # doesn't work
63+ armDarwinCompaREST = hsPkgs.projectCross.aarch64-darwin.hsPkgs.compaREST.components.exes.compaREST;
64+
65+ compaRESTImage = pkgs.dockerTools.buildImage {
66+ name = "compaREST";
67+ contents = [ compaRESTStaticBin ];
68+ config = {
69+ Entrypoint = [ "/bin/compaREST" ];
70+ };
71+ };
72+
73+ macOSCompaRESTBundle = pkgs.runCommand "compaREST-macOS-bundled"
74+ {
75+ buildInputs = [ masterPkgs.macdylibbundler ];
76+ } ''
77+ mkdir -p $out/lib
78+ cp ${compaRESTBin}/bin/compaREST $out/compaREST
79+ chmod 755 $out/compaREST
80+ dylibbundler -b \
81+ -x $out/compaREST \
82+ -d $out/lib \
83+ -p '@executable_path/lib'
84+ '';
85+
86+
87+ compaRESTGithubAction =
88+ let
89+ action = staticify "compaREST-GitHub-Action-static" hsPkgs.projectCross.musl64.hsPkgs.compaREST.components.exes.compaREST-GitHub-Action;
90+ wrapped = pkgs.runCommand "wrapped-compaREST-GitHub-Action" { buildInputs = [ pkgs.makeWrapper ]; } ''
91+ makeWrapper ${action}/bin/compaREST-GitHub-Action $out/bin/pre --add-flags "pre"
92+ makeWrapper ${action}/bin/compaREST-GitHub-Action $out/bin/run --add-flags "run"
93+ '';
94+ in
95+ pkgs.dockerTools.buildImage {
96+ name = "typeable/compaREST-GitHub-Action";
97+ tag = "latest";
98+ contents = [ wrapped pkgs.cacert ];
99+ };
100+
101+ WindowsCompaRESTBin = hsPkgs.projectCross.mingwW64.hsPkgs.compaREST.components.exes.compaREST;
102+
103+ # We use the static version so that we don't have to rebuild everything on CI.
104+ # The only binaries build on CI are static.
105+ test = hsPkgs.projectCross.musl64.hsPkgs.compaREST.components.tests.compaREST-tests.overrideAttrs (final: old: {
106+ buildInputs = old.buildInputs ++ [ diffYaml yq ];
107+ nativeBuildInputs = old.nativeBuildInputs ++ (with masterPkgs; [ makeWrapper ]);
108+
109+ postInstall = ''
110+ wrapProgram $out/bin/compaREST-tests \
111+ --prefix PATH : ${masterPkgs.lib.makeBinPath [ diffYaml yq ]}
112+ '';
113+ });
114+in
115+{
116+ inherit
117+ compaRESTImage
118+ compaRESTGithubAction
119+ compaRESTStaticBin
120+ compaRESTBin
121+ hsPkgs
122+ macOSCompaRESTBundle
123+ WindowsCompaRESTBin
124+ test;
125+}
docs/Development_guide.mdnew file+149
@@ -0,0 +1,149 @@
1+CompaREST
2+=========
3+The tool works by simultaneously looking at a pair of nodes in OpenAPI schema
4+trees. At every place we try to understand whether one side (the producer) can
5+produce something that the other side (the consumer) cannot consume. This often
6+involves recursively running the same check on child sub-nodes. The necessary
7+criteria are implemented manually for all types of nodes that appear in the
8+schema. The `Subtree` class in `Data.OpenApi.Compare.Subtree` designates types
9+of nodes for which this compatibility checking is defined. The implementations
10+for various types reside in `Data.OpenApi.Compare.Validate.*`.
11+
12+Producer/Consumer
13+-----------------
14+Rather than looking at which schema is the server's and which is the client's,
15+in terms of compatibility checking it is more useful to track which side is the
16+"producer" and which is the "consumer". At the root of the schema the producer
17+is the client, but as we descend into an HTTP response, the direction flips and
18+the producer becomes the server.
19+
20+We deal with a lot of things in pairs, where one thing belongs to the producer
21+and another belongs to the consumer. The `ProdCons` datatype (in
22+`Data.OpenApi.Compare.Subtree`) is just a pair type that provides an Applicative
23+abstraction for working with two things simultaneously.
24+
25+Trees
26+-----
27+We directly use the datatypes defined in the `openapi3` library. For reasons of
28+identifiability and memoization we tag all nodes in the trees with a path from
29+the root. The tree is heterogeneous (nodes have different types), and every type
30+of node has a different arrangement of children. There is a `Step` data family
31+that defines all the possible children (of a specific type) of a node (of a
32+specific type).
33+
34+A `Trace` is a sequence of steps, except in a path the types of adjacent steps
35+have to agree. The `Paths` datatype achieves this. Together all paths form a
36+`Category`, and we often concatenate them using `>>>`. This is defined in
37+`Data.OpenApi.Compare.Paths`.
38+
39+We often keep a node and a path to it next to each other using an environment
40+comonad. `Traced` is a type alias around `Env` which ensures that the type of
41+the path matches the type of the node.
42+
43+Issues
44+------
45+The output of the tool is a list of compatibility issues that came up during
46+checking. The `Issue` data family describes the kinds of issues that can occur
47+at each type of node. An issue is supposed to characterize a set of interactions
48+(requests or responses) that demonstrate that the schemas are incompatible.
49+
50+Each issue is tagged with a `Behavior` that describes the path to reproducing
51+the issue. While a `Trace` describes a syntactic path down the schema tree, a
52+`Behavior` describes which part of an interaction needs to be focused on for the
53+issue to manifest itself.
54+
55+In most places where we keep multiple issues (e.g. the output), to store them we
56+use a prefix tree map, indexed by both types and values of `Behavior`s. See
57+`Data.OpenApi.Compare.PathsPrefixTree`.
58+
59+Report
60+------
61+The checker ultimately outputs a list of `Issue`s, together with `Behavior`s
62+that cause them. We run the checker twice: forwards and backwards -- to detect
63+non-breaking changes. All of this is then compiled into a report in
64+`Data.OpenApi.Compare.Report`. The tree structure of headings is computed from
65+`Behavior`s, using `Jet`s to collapse particular sequences of behavior steps
66+into a single human-readable header item. The text of each paragraph comes from
67+`describeIssue`.
68+
69+We then use the `pandoc` library to render the report in a variety of formats.
70+
71+Formulas
72+--------
73+The schema has references, so it can end up being recursive. Similarly the
74+process for establishing compatibility can end up being recursive. To keep track
75+of this, the checker actually manipulates *formulas* which can have variables in
76+them, defined in (`Data.OpenApi.Compare.Formula`).
77+
78+A formula represents a set of issues, with the empty set corresponding to a
79+successful compatibility check. A formula can have conjunctions, where we only
80+report success if all parts succeeded, and if not we take the union of all
81+issues. A formula can also have disjunctions, but there it's impossible to
82+guarantee that the issues for the parts also make sense as issues for the whole
83+thing. Due to this if all disjuncted parts have issues we instead return a
84+different issue (usually non-descriptive).
85+
86+The `FormulaF` datatype implements a functor that carries formulaic calculations
87+as well as a result value (a co-Yoneda extension of formulas for the most part).
88+The Applicative interface provides conjunction, and the Alternative interface
89+provides disjunction.
90+
91+When we detect recursion we introduce a variable, compute the compatibility
92+check as a formula with that variable, and then we solve a fixed point equation
93+(`maxFixpoint`) with the variable to obtain the answer.
94+
95+This `FomulaF` is further wrapped in a couple monad transformers
96+(`SemanticCompatFormula`, `StructuralCompatFormula`), and the whole checker is
97+then implemented in the resulting Applicative using ApplicativeDo.
98+
99+Memoization
100+-----------
101+The `Data.OpenApi.Compare.Memo` module contains utilities for stateful
102+memoization and recursion detection. The entire checking process is memoized,
103+and can detect, propagate, and resolve recursion.
104+
105+Structural/Semantic Compatibility
106+---------------------------------
107+When we encounter two nodes we first try to optimistically establish whether
108+they're structurally equal, i.e. the trees are exactly the same modulo reference
109+inlining. This check is also memoized and recursion-aware. If the check succeeds
110+we conclude that the schemas are compatible. Otherwise we fall back to the
111+"normal" semantic compatibility.
112+
113+
114+JSON Schema
115+-----------
116+The language that describes JSON payloads is probably the richest part of
117+OpenAPI schemas, and the checker for it also comes with a lot of moving parts.
118+
119+JSON schema allows arbitrary set arithmetic using `allOf`, `anyOf`, `not` and
120+`oneOf`. To fully check `not` and `oneOf` we would need to compare set
121+subtraction, which involves negating a comparison result. If recursion is
122+involved we would need to be able to solve equations with negation, which we
123+cannot do. So `not` is unsupported altogether, and `oneOf` is only supported
124+when it has disjoint branches and behaves like `anyOf`.
125+
126+With that in mind a JSON schema is first pre-processed into a Disjunctive Normal
127+Form (`DNF` in `Data.OpenApi.Compare.Validate.Schema.DNF`). This pre-processing
128+happens in `Data.OpenApi.Compare.Validate.Schema.Process`.
129+
130+The DNF contains elementary clauses that match on a specific aspect of a JSON
131+object (`Condition`). Most clauses only affect objects of a single type, so JSON
132+values and clauses are segregated by type (`JsonType`, `TypedValue` in
133+`Data.OpenApi.Compare.Validate.Schema.JsonFormula`)
134+
135+Then we reduce the problem of comparing two DNF's to the comparison of a
136+conjunction of clauses in the producer with a single clause in the consumer
137+(`checkFormulas` in `Data.OpenApi.Compare.Validate.Schema`).
138+
139+When we do disjunction in the formula we lose information, so we try to avoid
140+having disjunctions in the consumer DNF. This is done by the means of
141+partitioning. We look for factors that would let us partition the set of all
142+objects into several bins, in a way that hopefully corresponds only a single
143+consumer to each producer. This is implemented in
144+`Data.OpenApi.Compare.Validate.Schema.Partition`. Currently we can only
145+partition by an `enum` value that is accessible via a chain of `required`
146+fields.
147+
148+The same partitioning mechanism is used to test whether the branches of a
149+`oneOf` are disjoint, and emit a warning otherwise.
docs/Integration_guide.mdnew file+49
@@ -0,0 +1,49 @@
1+# Integration guide
2+
3+## Github Actions
4+
5+In cases where your OpenAPI spec can be generated from your code, it can be useful to see how a pull request changes your API. For this very purpose we have a Github Action.
6+
7+### Generating the inputs
8+
9+To use it you need to first get the old (before the pull request) and new (after the pull request) specs.
10+
11+For example:
12+
13+- To get the old spec you can:
14+ - Download from a public "production" server (if your api is public and you provide an OpenAPI spec)
15+ - Checkout the base commit of the pull request and generate is programmatically.
16+
17+ You can checkout the base commit like so in Github Actions:
18+
19+ ```bash
20+ git checkout $(git merge-base ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.base.sha }})
21+ ```
22+- To get the new spec you can:
23+ - Build and run the spec generation for the head commit of the pull request
24+
25+### Run the action
26+
27+In our example we have already generated the two OpenAPI specs and placed them in the `old-openapi.yaml` and `new-openapi.yaml` files.
28+
29+We can now run our action:
30+
31+```yaml
32+- uses: typeable/comparest
33+ with:
34+ old: old-openapi.yaml
35+ new: new-openapi.yaml
36+ project_name: TEST
37+```
38+
39+This will create a check on the pull request displaying the changes (if there are any) similar to this:
40+
41+![](img/github-action-report.png)
42+
43+The check will show success when there are no breaking changes, and be neutral otherwise.
44+
45+## Integrating into something other than Github Actions
46+
47+We have a dedicated implementation only for Github Actions. However, we provide a generic CLI which can be used to integrate compaREST into arbitrary workflows and CI systems.
48+
49+You see an example of CLI usage in our [readme](../README.md#assessing-compatibility-automatically).
docs/Releasing.mdnew file+15
@@ -0,0 +1,15 @@
1+# The release procedure
2+
3+1. Update the version number everywhere:
4+ 1. Version in [compaREST.cabal](../compaREST.cabal)
5+ 2. Docker tag in [action.yaml](../action.yaml)
6+ 3. Set the version of unreleased changes in [CHANGELOG.md](../CHANGELOG.md)
7+2. Publish!
8+ 1. Create a pre-release on GitHub:
9+ ![](img/pre-release.png)
10+ 2. This will automatically start the release procedure. You can check the state of it in the "Actions" tab on GitHub:
11+ ![](img/pre-release-ci.png)
12+ 3. If everything goes as planned, the release action should pass and all assets should be added to the release:
13+ ![](img/pre-release-artifacts.png)
14+ 4. The new version should be automatically uploaded a release candidate to [Hackage](https://hackage.haskell.org/package/compaREST/candidates/). Make sure everything looks okay and publish it.
15+ 5. Go to the Releases tab on GitHub and edit the release to make it not a "pre-release".
docs/User_guide.mdnew file+51
@@ -0,0 +1,51 @@
1+CompaREST User Guide
2+====================
3+
4+Running the Tool
5+----------------
6+The tool accepts two OpenAPI 3.0.0 schema files in either JSON or YAML format.
7+One is assumed to be the client version of the schema, and the other is the
8+server version.
9+
10+The tool will look for changes between the schemas and detect whether they are
11+breaking or not -- that is, if they prevent interoperability between the client
12+and the server.
13+
14+Running:
15+```
16+comparest -c client.json -s server.json
17+```
18+will output a markdown report of the changes to the standard output.
19+
20+Compatibility status can be signaled via the exit code using
21+`--signal-exit-code`. In case no report is needed, output can be suppressed with
22+`--silent`. For example:
23+```
24+comparest -c client.json -s server.json --signal-exit-code --silent
25+echo $?
26+```
27+If there were changes that the tool determined to be breaking, the exit code
28+will be 1. If there were some changes the tool couldn't understand, the exit
29+code will be 2. Otherwise if there were no breaking changes, the exit code will
30+be 0.
31+
32+Controlling the Report
33+----------------------
34+By default the report includes breaking changes, as well as non-breaking:
35+changes that would be considered breaking in the opposite direction. To only
36+include breaking changes in the report use `--only-breaking`. The `--all` option
37+restores the default behavior.
38+
39+The report can be formatted in a variety of ways supported by Pandoc. The `-o`
40+option causes the report to be written to a file. The format of the file is
41+determined from the extension. The supported extensions include:
42+ - `.md` for markdown
43+ - `.html` for an HTML snippet
44+ - `.rst` for restructured text
45+ - no extension for a self-contained HTML document with styles
46+
47+By default the report is split up into parts relating to different paths,
48+requests, responses, etc, using headers of various levels. Alternatively, the
49+report can use indended block-quotes to visualize the tree structure of the
50+report. The header style is enabled with `--header-style`, and the block-quote
51+style is enabled with `--folding-block-quotes-style`.
docs/img/github-action-report.pngnew file

Binary file not shown.

docs/img/pre-release-artifacts.pngnew file

Binary file not shown.

docs/img/pre-release-ci.pngnew file

Binary file not shown.

docs/img/pre-release.pngnew file

Binary file not shown.

fourmolu.yamlnew file+8
@@ -0,0 +1,8 @@
1+indentation: 2
2+comma-style: leading # for lists, tuples etc. - can also be 'trailing'
3+record-brace-space: true # rec {x = 1} vs. rec{x = 1}
4+indent-wheres: true # 'false' means save space by only half-indenting the 'where' keyword
5+diff-friendly-import-export: false # 'false' uses Ormolu-style lists
6+respectful: true # don't be too opinionated about newlines etc.
7+haddock-style: single-line # '--' vs. '{-'
8+newlines-between-decls: 1 # number of newlines between top-level declarations
github-action/CompaREST/GitHub/API.hsnew file+86
@@ -0,0 +1,86 @@
1+module CompaREST.GitHub.API
2+ ( postStatus,
3+ postStatusProcessing,
4+ )
5+where
6+
7+import CompaREST.GitHub.Action.Config
8+import Control.Monad.Freer
9+import Control.Monad.Freer.GitHub
10+import Control.Monad.Freer.Reader
11+import Control.Monad.IO.Class
12+import Data.Aeson
13+import qualified Data.ByteString.Lazy.Char8 as BSLC
14+import Data.OpenApi.Compare.Report
15+import Data.Proxy
16+import Data.Text (Text)
17+import GitHub
18+import GitHub.Data.Checks
19+import GitHub.Endpoints.Checks
20+
21+postStatusProcessing ::
22+ (Members '[GitHub, Reader Config] effs, MonadIO (Eff effs)) =>
23+ Eff effs ()
24+postStatusProcessing = do
25+ Config {..} <- ask
26+ printJSON $
27+ sendGitHub $
28+ checkR
29+ repoOwner
30+ repoName
31+ Check
32+ { checkName = mkName Proxy checkName
33+ , checkSha = sha
34+ , checkDetailsURL = Nothing
35+ , checkExternalId = Nothing
36+ , checkStatus = Just CheckInProgress
37+ , checkStartedAt = Nothing
38+ , checkConclusion = Nothing
39+ , checkCompletedAt = Nothing
40+ , checkOutput = Nothing
41+ , checkActions = Nothing
42+ }
43+
44+postStatus ::
45+ (Members '[GitHub, Reader Config] effs, MonadIO (Eff effs)) =>
46+ -- | 'Nothing' means that there were no changes at all
47+ Maybe (Text, ReportStatus) ->
48+ Eff effs ()
49+postStatus x = do
50+ let (body, (title, conclusion)) = case x of
51+ Just (b, s) -> (b,) $ case s of
52+ BreakingChanges -> ("⚠️ Breaking changes found!", CheckNeutral)
53+ NoBreakingChanges -> ("No breaking changes found ✨", CheckSuccess)
54+ OnlyUnsupportedChanges -> ("🤷 Couldn't determine compatibility", CheckNeutral)
55+ Nothing -> ("", ("✅ The API did not change", CheckSuccess))
56+ Config {..} <- ask
57+ printJSON $
58+ sendGitHub $
59+ checkR
60+ repoOwner
61+ repoName
62+ Check
63+ { checkName = mkName Proxy checkName
64+ , checkSha = sha
65+ , checkDetailsURL = Nothing
66+ , checkExternalId = Nothing
67+ , checkStatus = Just CheckCompleted
68+ , checkStartedAt = Nothing
69+ , checkConclusion = Just conclusion
70+ , checkCompletedAt = Nothing
71+ , checkOutput =
72+ Just $
73+ CheckOutput
74+ { checkTitle = title
75+ , checkSummary = body
76+ , checkText = Nothing
77+ , checkAnnotations = Nothing
78+ , checkImages = Nothing
79+ }
80+ , checkActions = Nothing
81+ }
82+
83+printJSON :: MonadIO (Eff effs) => Eff effs Value -> Eff effs ()
84+printJSON m = do
85+ x <- m
86+ liftIO . BSLC.putStrLn $ encode x
github-action/CompaREST/GitHub/Action/Config.hsnew file+47
@@ -0,0 +1,47 @@
1+module CompaREST.GitHub.Action.Config
2+ ( Config (..),
3+ )
4+where
5+
6+import Data.Proxy
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import qualified GitHub as GH
10+
11+import Data.Functor
12+import System.Envy
13+
14+data Config = Config
15+ { githubToken :: GH.Auth
16+ , repoOwner :: GH.Name GH.Owner
17+ , repoName :: GH.Name GH.Repo
18+ , checkName :: Text
19+ , footerText :: Text
20+ , root :: FilePath
21+ , sha :: GH.Name GH.Commit
22+ }
23+
24+instance FromEnv Config where
25+ fromEnv _ = do
26+ token <- GH.OAuth <$> env "GITHUB_TOKEN"
27+ (owner, repo) <-
28+ T.split (== '/') <$> env "REPO" >>= \case
29+ [owner, name] -> pure (owner, name)
30+ _ -> fail "malformed repo"
31+ checkName <-
32+ envMaybe "PROJECT_NAME" <&> \case
33+ Just pName | not . T.null . T.strip $ pName -> "compaREST – " <> pName
34+ _ -> "compaREST"
35+ footerText <- env "FOOTER"
36+ root <- envMaybe "ROOT" .!= "."
37+ sha <- env "SHA"
38+ pure $
39+ Config
40+ { githubToken = token
41+ , repoOwner = GH.mkName Proxy owner
42+ , repoName = GH.mkName Proxy repo
43+ , checkName = checkName
44+ , footerText = footerText
45+ , root = root
46+ , sha = GH.mkName Proxy sha
47+ }
github-action/Control/Monad/Freer/GitHub.hsnew file+27
@@ -0,0 +1,27 @@
1+module Control.Monad.Freer.GitHub
2+ ( GitHub (..),
3+ runGitHub,
4+ sendGitHub,
5+ )
6+where
7+
8+import Control.Monad.Freer
9+import Control.Monad.Freer.Error
10+import Control.Monad.IO.Class
11+import Data.Aeson
12+import GitHub hiding (Error)
13+import qualified GitHub as GH
14+
15+data GitHub r where
16+ SendGHRequest :: FromJSON x => Request 'RW x -> GitHub x
17+
18+runGitHub :: (Member (Error GH.Error) effs, MonadIO (Eff effs)) => Auth -> Eff (GitHub ': effs) ~> Eff effs
19+runGitHub auth =
20+ interpret
21+ ( \(SendGHRequest req) ->
22+ liftIO (executeRequest auth req)
23+ >>= either throwError pure
24+ )
25+
26+sendGitHub :: (FromJSON x, Member GitHub effs) => Request 'RW x -> Eff effs x
27+sendGitHub req = send $ SendGHRequest req
github-action/GitHub/Data/Checks.hsnew file+173
@@ -0,0 +1,173 @@
1+module GitHub.Data.Checks
2+ ( Check (..),
3+ CheckStatus (..),
4+ CheckConclusion (..),
5+ CheckOutput (..),
6+ CheckAnnotation (..),
7+ CheckAnnotationLevel (..),
8+ CheckImage (..),
9+ CheckAction (..),
10+ )
11+where
12+
13+import Data.Aeson
14+import Data.Aeson.Types
15+import Data.Text (Text)
16+import Data.Vector (Vector)
17+import GHC.Generics (Generic)
18+import GitHub
19+import GitHub.Internal.Prelude (UTCTime)
20+
21+data Check = Check
22+ { checkName :: !(Name Check)
23+ , checkSha :: !(Name Commit)
24+ , checkDetailsURL :: !(Maybe URL)
25+ , checkExternalId :: !(Maybe (Id Check))
26+ , checkStatus :: !(Maybe CheckStatus)
27+ , checkStartedAt :: !(Maybe UTCTime)
28+ , checkConclusion :: !(Maybe CheckConclusion)
29+ , checkCompletedAt :: !(Maybe UTCTime)
30+ , checkOutput :: !(Maybe CheckOutput)
31+ , checkActions :: !(Maybe (Vector CheckAction))
32+ }
33+ deriving stock (Show, Eq, Ord, Generic)
34+
35+instance ToJSON Check where
36+ toJSON (Check n sha durl eid s sa c ca o a) =
37+ object'
38+ [ "name" .= n
39+ , "head_sha" .= sha
40+ , "details_url" .= durl
41+ , "external_id" .= eid
42+ , "status" .= s
43+ , "started_at" .= sa
44+ , "conclusion" .= c
45+ , "completed_at" .= ca
46+ , "output" .= o
47+ , "actions" .= a
48+ ]
49+
50+data CheckStatus
51+ = CheckQueued
52+ | CheckInProgress
53+ | CheckCompleted
54+ deriving stock (Show, Enum, Bounded, Eq, Ord, Generic)
55+
56+instance ToJSON CheckStatus where
57+ toJSON CheckQueued = String "queued"
58+ toJSON CheckInProgress = String "in_progress"
59+ toJSON CheckCompleted = String "completed"
60+
61+data CheckConclusion
62+ = CheckActionRequired
63+ | CheckCancelled
64+ | CheckFailure
65+ | CheckNeutral
66+ | CheckSuccess
67+ | CheckSkipped
68+ | CheckStale
69+ | CheckTimedOut
70+ deriving stock (Show, Enum, Bounded, Eq, Ord, Generic)
71+
72+instance ToJSON CheckConclusion where
73+ toJSON CheckActionRequired = String "action_required"
74+ toJSON CheckCancelled = String "cancelled"
75+ toJSON CheckFailure = String "failure"
76+ toJSON CheckNeutral = String "neutral"
77+ toJSON CheckSuccess = String "success"
78+ toJSON CheckSkipped = String "skipped"
79+ toJSON CheckStale = String "stale"
80+ toJSON CheckTimedOut = String "timed_out"
81+
82+data CheckOutput = CheckOutput
83+ { checkTitle :: !Text
84+ , checkSummary :: !Text
85+ , checkText :: !(Maybe Text)
86+ , checkAnnotations :: !(Maybe (Vector CheckAnnotation))
87+ , checkImages :: !(Maybe (Vector CheckImage))
88+ }
89+ deriving stock (Show, Eq, Ord, Generic)
90+
91+instance ToJSON CheckOutput where
92+ toJSON (CheckOutput t s txt a i) =
93+ object'
94+ [ "title" .= t
95+ , "summary" .= s
96+ , "text" .= txt
97+ , "annotations" .= a
98+ , "images" .= i
99+ ]
100+
101+data CheckAnnotation = CheckAnnotation
102+ { checkPath :: !Text
103+ , checkStartLine :: !Int
104+ , checkEndLine :: !Int
105+ , checkStartColumn :: !(Maybe Int)
106+ , checkEndColumn :: !(Maybe Int)
107+ , checkAnnotationLevel :: !CheckAnnotationLevel
108+ , checkMessage :: !Text
109+ , checkTitle :: !(Maybe Text)
110+ , checkRawDetails :: !(Maybe Text)
111+ }
112+ deriving stock (Show, Eq, Ord, Generic)
113+
114+instance ToJSON CheckAnnotation where
115+ toJSON (CheckAnnotation p sl el sc ec al m t rd) =
116+ object'
117+ [ "path" .= p
118+ , "start_line" .= sl
119+ , "end_line" .= el
120+ , "start_column" .= sc
121+ , "end_column" .= ec
122+ , "annotation_level" .= al
123+ , "message" .= m
124+ , "title" .= t
125+ , "raw_details" .= rd
126+ ]
127+
128+data CheckAnnotationLevel
129+ = NoticeAnnotation
130+ | WarningAnnotation
131+ | FailureAnnotation
132+ deriving stock (Show, Enum, Bounded, Eq, Ord, Generic)
133+
134+instance ToJSON CheckAnnotationLevel where
135+ toJSON NoticeAnnotation = String "notice"
136+ toJSON WarningAnnotation = String "warning"
137+ toJSON FailureAnnotation = String "failure"
138+
139+data CheckImage = CheckImage
140+ { checkImageAlt :: !Text
141+ , checkImageURL :: !URL
142+ , checkImageCaption :: !(Maybe Text)
143+ }
144+ deriving stock (Show, Eq, Ord, Generic)
145+
146+instance ToJSON CheckImage where
147+ toJSON (CheckImage a url c) =
148+ object'
149+ [ "alt" .= a
150+ , "image_url" .= url
151+ , "caption" .= c
152+ ]
153+
154+data CheckAction = CheckAction
155+ { checkActionLabel :: !Text
156+ , checkActionDescription :: !Text
157+ , checkActionIdentifier :: !Text
158+ }
159+ deriving stock (Show, Eq, Ord, Generic)
160+
161+instance ToJSON CheckAction where
162+ toJSON (CheckAction l d i) =
163+ object'
164+ [ "label" .= l
165+ , "description" .= d
166+ , "identifier" .= i
167+ ]
168+
169+object' :: [Pair] -> Value
170+object' = object . filter notNull
171+ where
172+ notNull (_, Null) = False
173+ notNull (_, _) = True
github-action/GitHub/Endpoints/Checks.hsnew file+12
@@ -0,0 +1,12 @@
1+module GitHub.Endpoints.Checks
2+ ( checkR,
3+ )
4+where
5+
6+import Data.Aeson
7+import GitHub
8+import GitHub.Data.Checks
9+
10+checkR :: Name Owner -> Name Repo -> Check -> Request 'RW Value
11+checkR user repo =
12+ command Post ["repos", toPathPart user, toPathPart repo, "check-runs"] . encode
github-action/Main.hsnew file+72
@@ -0,0 +1,72 @@
1+module Main (main) where
2+
3+import CompaREST.GitHub.API
4+import CompaREST.GitHub.Action.Config
5+import Control.Exception
6+import Control.Monad.Freer
7+import Control.Monad.Freer.Error
8+import Control.Monad.Freer.GitHub
9+import Control.Monad.Freer.Reader
10+import Data.OpenApi.Compare.Run
11+import Data.Text (Text)
12+import qualified Data.Text as T
13+import qualified Data.Yaml.Aeson as Yaml
14+import qualified GitHub as GH
15+import System.Environment
16+import System.Envy (decodeEnv)
17+import System.FilePath ((</>))
18+import Text.Pandoc (runPure)
19+import Text.Pandoc.Builder
20+import Text.Pandoc.Options
21+import Text.Pandoc.Writers
22+
23+main :: IO ()
24+main = do
25+ cfg <- decodeEnv >>= either error pure
26+ case sha cfg of
27+ x | T.null . T.strip . GH.untagName $ x -> do
28+ putStrLn "SHA not specified. Exiting without doing anything."
29+ pure ()
30+ _ ->
31+ getArgs >>= \case
32+ ["pre"] -> runPre cfg
33+ ["run"] -> do
34+ oldFile <- getEnv "OLD"
35+ newFile <- getEnv "NEW"
36+ runRun cfg (root cfg </> oldFile) (root cfg </> newFile)
37+ _ -> error "Invalid arguments."
38+
39+runner :: Config -> Eff '[GitHub, Error GH.Error, Reader Config, IO] a -> IO a
40+runner cfg =
41+ runM @IO . runReader cfg
42+ . flip (handleError @GH.Error) (error . displayException)
43+ . runGitHub (githubToken cfg)
44+
45+runPre :: Config -> IO ()
46+runPre cfg = runner cfg postStatusProcessing
47+
48+runRun :: Config -> FilePath -> FilePath -> IO ()
49+runRun cfg old' new' = runner cfg $ do
50+ old <- Yaml.decodeFileThrow old'
51+ new <- Yaml.decodeFileThrow new'
52+ let reportConfig =
53+ ReportConfig
54+ { treeStyle = FoldingBlockquotesTreeStyle
55+ , reportMode = All
56+ }
57+ (report, status) = runReport reportConfig (old, new)
58+
59+ body = markdown report <> "\n\n" <> footerText cfg
60+
61+ result =
62+ if old == new
63+ then Nothing
64+ else Just (body, status)
65+ postStatus result
66+
67+markdown :: Blocks -> Text
68+markdown =
69+ either (error . displayException) id
70+ . runPure
71+ . writeHtml5String def
72+ . doc
nix/ci.nixnew file+11
@@ -0,0 +1,11 @@
1+{ sources ? import ./sources.nix
2+, pkgs ? import sources.nixpkgs { }
3+}:
4+
5+let
6+ yq = import ./yq.nix { inherit pkgs; };
7+ diff-yaml = import ./diff-yaml.nix { inherit pkgs; };
8+in
9+pkgs.mkShell {
10+ packages = [ pkgs.haskellPackages.fourmolu diff-yaml yq ];
11+}
nix/diff-yaml.nixnew file+20
@@ -0,0 +1,20 @@
1+{ pkgs ? import <nixpkgs> {} }:
2+
3+let
4+ inherit (pkgs) stdenv;
5+ yq = import ./yq.nix { inherit pkgs; };
6+in
7+stdenv.mkDerivation {
8+ name = "diff-yaml";
9+
10+ src = ../scripts/diff-yaml;
11+
12+ buildInputs = [ pkgs.bash yq ];
13+
14+ phases = [ "installPhase" ];
15+
16+ installPhase = ''
17+ mkdir -p $out/bin
18+ cp $src $out/bin/diff-yaml
19+ '';
20+}
nix/shell.nixnew file+11
@@ -0,0 +1,11 @@
1+{ sources ? import ./sources.nix
2+, pkgs ? import sources.nixpkgs { }
3+}:
4+
5+let
6+ patched-yq = import ./yq.nix { inherit pkgs; };
7+ diffYaml = import ./diff-yaml.nix { inherit pkgs; };
8+in
9+pkgs.mkShell {
10+ buildInputs = with pkgs; [ pkg-config diffYaml patched-yq zlib ];
11+}
nix/sources.jsonnew file+50
@@ -0,0 +1,50 @@
1+{
2+ "haskellNix": {
3+ "branch": "master",
4+ "description": "Alternative Haskell Infrastructure for Nixpkgs",
5+ "homepage": "https://input-output-hk.github.io/haskell.nix",
6+ "owner": "input-output-hk",
7+ "repo": "haskell.nix",
8+ "rev": "d7f95903c84a5a50d6fe0844280da82c3c81680a",
9+ "sha256": "02870rk4sx44lwwa0k20iwdhqcx0ayz34yb3b0qy045p22q45815",
10+ "type": "tarball",
11+ "url": "https://github.com/input-output-hk/haskell.nix/archive/d7f95903c84a5a50d6fe0844280da82c3c81680a.tar.gz",
12+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
13+ },
14+ "niv": {
15+ "branch": "master",
16+ "description": "Easy dependency management for Nix projects",
17+ "homepage": "https://github.com/nmattia/niv",
18+ "owner": "nmattia",
19+ "repo": "niv",
20+ "rev": "f7c538837892dd2eb83567c9f380a11efb59b53f",
21+ "sha256": "0xl33k24vfc29cg9lnp95kvcq69qbq5fzb7jk9ig4lgrhaarh651",
22+ "type": "tarball",
23+ "url": "https://github.com/nmattia/niv/archive/f7c538837892dd2eb83567c9f380a11efb59b53f.tar.gz",
24+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
25+ },
26+ "nix-filter": {
27+ "branch": "main",
28+ "description": "a small self-container source filtering lib",
29+ "homepage": "",
30+ "owner": "numtide",
31+ "repo": "nix-filter",
32+ "rev": "3342559a24e85fc164b295c3444e8a139924675b",
33+ "sha256": "08xia32g5jzaiyhfl3kzxslzbr4w1i56i0mhy5gpfggv8gqy8sym",
34+ "type": "tarball",
35+ "url": "https://github.com/numtide/nix-filter/archive/3342559a24e85fc164b295c3444e8a139924675b.tar.gz",
36+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
37+ },
38+ "nixpkgs": {
39+ "branch": "master",
40+ "description": "Nix Packages collection",
41+ "homepage": "",
42+ "owner": "NixOS",
43+ "repo": "nixpkgs",
44+ "rev": "bf038bd724982de67a2ee9a6b2f1fcb7d6a87bd7",
45+ "sha256": "0ivvwdmg9fipj6ylq5ipxqpb0xnz6hsx3lyn377jacfpiqi9w0cv",
46+ "type": "tarball",
47+ "url": "https://github.com/NixOS/nixpkgs/archive/bf038bd724982de67a2ee9a6b2f1fcb7d6a87bd7.tar.gz",
48+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
49+ }
50+}
nix/sources.nixnew file+174
@@ -0,0 +1,174 @@
1+# This file has been generated by Niv.
2+
3+let
4+
5+ #
6+ # The fetchers. fetch_<type> fetches specs of type <type>.
7+ #
8+
9+ fetch_file = pkgs: name: spec:
10+ let
11+ name' = sanitizeName name + "-src";
12+ in
13+ if spec.builtin or true then
14+ builtins_fetchurl { inherit (spec) url sha256; name = name'; }
15+ else
16+ pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
17+
18+ fetch_tarball = pkgs: name: spec:
19+ let
20+ name' = sanitizeName name + "-src";
21+ in
22+ if spec.builtin or true then
23+ builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
24+ else
25+ pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
26+
27+ fetch_git = name: spec:
28+ let
29+ ref =
30+ if spec ? ref then spec.ref else
31+ if spec ? branch then "refs/heads/${spec.branch}" else
32+ if spec ? tag then "refs/tags/${spec.tag}" else
33+ abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
34+ in
35+ builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
36+
37+ fetch_local = spec: spec.path;
38+
39+ fetch_builtin-tarball = name: throw
40+ ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
41+ $ niv modify ${name} -a type=tarball -a builtin=true'';
42+
43+ fetch_builtin-url = name: throw
44+ ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
45+ $ niv modify ${name} -a type=file -a builtin=true'';
46+
47+ #
48+ # Various helpers
49+ #
50+
51+ # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
52+ sanitizeName = name:
53+ (
54+ concatMapStrings (s: if builtins.isList s then "-" else s)
55+ (
56+ builtins.split "[^[:alnum:]+._?=-]+"
57+ ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
58+ )
59+ );
60+
61+ # The set of packages used when specs are fetched using non-builtins.
62+ mkPkgs = sources: system:
63+ let
64+ sourcesNixpkgs =
65+ import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
66+ hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
67+ hasThisAsNixpkgsPath = <nixpkgs> == ./.;
68+ in
69+ if builtins.hasAttr "nixpkgs" sources
70+ then sourcesNixpkgs
71+ else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
72+ import <nixpkgs> {}
73+ else
74+ abort
75+ ''
76+ Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
77+ add a package called "nixpkgs" to your sources.json.
78+ '';
79+
80+ # The actual fetching function.
81+ fetch = pkgs: name: spec:
82+
83+ if ! builtins.hasAttr "type" spec then
84+ abort "ERROR: niv spec ${name} does not have a 'type' attribute"
85+ else if spec.type == "file" then fetch_file pkgs name spec
86+ else if spec.type == "tarball" then fetch_tarball pkgs name spec
87+ else if spec.type == "git" then fetch_git name spec
88+ else if spec.type == "local" then fetch_local spec
89+ else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
90+ else if spec.type == "builtin-url" then fetch_builtin-url name
91+ else
92+ abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
93+
94+ # If the environment variable NIV_OVERRIDE_${name} is set, then use
95+ # the path directly as opposed to the fetched source.
96+ replace = name: drv:
97+ let
98+ saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
99+ ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
100+ in
101+ if ersatz == "" then drv else
102+ # this turns the string into an actual Nix path (for both absolute and
103+ # relative paths)
104+ if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
105+
106+ # Ports of functions for older nix versions
107+
108+ # a Nix version of mapAttrs if the built-in doesn't exist
109+ mapAttrs = builtins.mapAttrs or (
110+ f: set: with builtins;
111+ listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
112+ );
113+
114+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
115+ range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
116+
117+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
118+ stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
119+
120+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
121+ stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
122+ concatMapStrings = f: list: concatStrings (map f list);
123+ concatStrings = builtins.concatStringsSep "";
124+
125+ # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
126+ optionalAttrs = cond: as: if cond then as else {};
127+
128+ # fetchTarball version that is compatible between all the versions of Nix
129+ builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
130+ let
131+ inherit (builtins) lessThan nixVersion fetchTarball;
132+ in
133+ if lessThan nixVersion "1.12" then
134+ fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
135+ else
136+ fetchTarball attrs;
137+
138+ # fetchurl version that is compatible between all the versions of Nix
139+ builtins_fetchurl = { url, name ? null, sha256 }@attrs:
140+ let
141+ inherit (builtins) lessThan nixVersion fetchurl;
142+ in
143+ if lessThan nixVersion "1.12" then
144+ fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
145+ else
146+ fetchurl attrs;
147+
148+ # Create the final "sources" from the config
149+ mkSources = config:
150+ mapAttrs (
151+ name: spec:
152+ if builtins.hasAttr "outPath" spec
153+ then abort
154+ "The values in sources.json should not have an 'outPath' attribute"
155+ else
156+ spec // { outPath = replace name (fetch config.pkgs name spec); }
157+ ) config.sources;
158+
159+ # The "config" used by the fetchers
160+ mkConfig =
161+ { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
162+ , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
163+ , system ? builtins.currentSystem
164+ , pkgs ? mkPkgs sources system
165+ }: rec {
166+ # The sources, i.e. the attribute set of spec name to spec
167+ inherit sources;
168+
169+ # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
170+ inherit pkgs;
171+ };
172+
173+in
174+mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
nix/yq.nixnew file+26
@@ -0,0 +1,26 @@
1+{ pkgs ? import <nixpkgs> {} }:
2+
3+# Make yq work inside whatever `stack test` launches.
4+# Nuke this when https://github.com/kislyuk/yq/pull/193 is merged
5+# and nixpkgs includes that fix.
6+
7+let
8+ yq-patch = pkgs.writeText "yq-tty-fix.patch" ''
9+diff --git a/yq/__init__.py b/yq/__init__.py
10+index d95ac4b..ae7d59e 100644
11+--- a/yq/__init__.py
12++++ b/yq/__init__.py
13+@@ -110,7 +110,7 @@ def cli(args=None, input_format="yaml", program_name="yq"):
14+ in_place = args.in_place
15+ delattr(args, "in_place")
16+
17+- if sys.stdin.isatty() and not args.input_streams:
18++ if (sys.stdin is None or sys.stdin.isatty()) and not args.input_streams:
19+ parser.print_help()
20+ sys.exit(2)
21+ elif not args.input_streams:
22+ '';
23+in
24+pkgs.yq.overrideAttrs (final: old: {
25+ patches = old.patches ++ [ yq-patch ];
26+})
scripts/diff-yamlnew file+35
@@ -0,0 +1,35 @@
1+#!/usr/bin/env bash
2+set -e
3+
4+# Normalise YAML files passed as inputs, then run diff on them.
5+
6+check_util() {
7+ local util=$1
8+
9+ if [ -z $util ]; then
10+ echo "Error: $util not found in PATH"
11+ exit 1
12+ fi
13+}
14+
15+DIFF=`which diff`
16+check_util $DIFF
17+
18+YQ=`which yq`
19+check_util $YQ
20+
21+# Only normalise YAML files.
22+# Print anything else as is.
23+normalise() {
24+ local file=$1
25+ local contents=($YQ -y -S '.' $file)
26+
27+ # if $contents contains actual YAML data, print it
28+ if [[ $? == 0 ]]; then
29+ echo $contents
30+ else
31+ cat $file
32+ fi
33+}
34+
35+$DIFF -u <(normalise $1) <(normalise $2)
src/Data/HList.hs+11−11
@@ -1,11 +1,11 @@
1module Data.HList
1module Data.HList
2− ( Has
2+ ( Has,
3− , HasAll
3+ HasAll,
4− , getH
4+ getH,
5− , HList (..)
5+ HList (..),
6− , singletonH
6+ singletonH,
7− , ReassembleHList
7+ ReassembleHList,
8− , reassemble
8+ reassemble,
9 )
9 )
10where
10where
11
11
@@ -40,8 +40,8 @@ instance (Has' x xs t, HeadEq x (y : xs) ~ 'False) => Has' x (y ': xs) 'False wh
40 {-# INLINE getH #-}
40 {-# INLINE getH #-}
41
41
42instance
42instance
43− TypeError ('ShowType x ':<>: 'Text " is not a part of the list.")
43+ TypeError ( 'ShowType x ':<>: 'Text " is not a part of the list.") =>
44− => Has' x '[] 'False
44+ Has' x '[] 'False
45 where
45 where
46 getH HNil = undefined
46 getH HNil = undefined
47 {-# INLINE getH #-}
47 {-# INLINE getH #-}
@@ -74,8 +74,8 @@ instance ReassembleHList' (x ': xs) '[] 'False where
74 {-# INLINE reassemble #-}
74 {-# INLINE reassemble #-}
75
75
76instance
76instance
77− (Has y xs, ReassembleHList' xs ys f, (xs == (y ': ys)) ~ 'False)
77+ (Has y xs, ReassembleHList' xs ys f, (xs == (y ': ys)) ~ 'False) =>
78− => ReassembleHList' xs (y ': ys) 'False
78+ ReassembleHList' xs (y ': ys) 'False
79 where
79 where
80 reassemble xs = getH @y xs `HCons` reassemble xs
80 reassemble xs = getH @y xs `HCons` reassemble xs
81 {-# INLINE reassemble #-}
81 {-# INLINE reassemble #-}
src/OpenAPI/Checker/Behavior.hs → src/Data/OpenApi/Compare/Behavior.hsrenamed+49−11
@@ -1,18 +1,22 @@
1−module OpenAPI.Checker.Behavior
1+module Data.OpenApi.Compare.Behavior
2− ( BehaviorLevel (..)
2+ ( BehaviorLevel (..),
3− , Behavable (..)
3+ Behavable (..),
4+ IssueKind (..),
4− , Issuable (..)
5+ Issuable (..),
5− , Orientation (..)
6+ Orientation (..),
6− , toggleOrientation
7+ toggleOrientation,
7− , Behavior
8+ Behavior,
8− , AnIssue (..)
9+ AnIssue (..),
10+ withClass,
11+ anIssueKind,
12+ relatedAnIssues,
9 )
13 )
10where
14where
11
15
12import Data.Aeson
16import Data.Aeson
13import Data.Kind
17import Data.Kind
18+import Data.OpenApi.Compare.Paths
14import Data.Typeable
19import Data.Typeable
15−import OpenAPI.Checker.Paths
16import Text.Pandoc.Builder
20import Text.Pandoc.Builder
17
21
18-- | Kind
22-- | Kind
@@ -39,10 +43,23 @@ class
39 Behavable (a :: BehaviorLevel) (b :: BehaviorLevel)
43 Behavable (a :: BehaviorLevel) (b :: BehaviorLevel)
40 where
44 where
41 data Behave a b
45 data Behave a b
42− describeBehaviour :: Behave a b -> Inlines
46+ describeBehavior :: Behave a b -> Inlines
43
47
44type instance AdditionalQuiverConstraints Behave a b = Behavable a b
48type instance AdditionalQuiverConstraints Behave a b = Behavable a b
45
49
50+data IssueKind
51+ = -- | This is certainly an issue, we can demonstrate a "counterexample"
52+ CertainIssue
53+ | -- | Change looks breaking but we don't have a complete comprehension of the problem
54+ ProbablyIssue
55+ | -- | We don't really support this feature at all, outside structural comparison
56+ Unsupported
57+ | -- | This is not an issue in itself, but a clarifying comment providing context for some other issues
58+ Comment
59+ | -- | We detected an issue with one of the input schemata itself
60+ SchemaInvalid
61+ deriving stock (Eq, Ord, Show)
62+
46class (Typeable l, Ord (Issue l), Show (Issue l)) => Issuable (l :: BehaviorLevel) where
63class (Typeable l, Ord (Issue l), Show (Issue l)) => Issuable (l :: BehaviorLevel) where
47 data Issue l :: Type
64 data Issue l :: Type
48
65
@@ -58,7 +75,22 @@ class (Typeable l, Ord (Issue l), Show (Issue l)) => Issuable (l :: BehaviorLeve
58 -- If _producer_ doesn't have something, the element was "added".
75 -- If _producer_ doesn't have something, the element was "added".
59 describeIssue :: Orientation -> Issue l -> Blocks
76 describeIssue :: Orientation -> Issue l -> Blocks
60
77
61− issueIsUnsupported :: Issue l -> Bool
78+ issueKind :: Issue l -> IssueKind
79+
80+ -- | An equivalence relation designating whether two issues are talking about the aspect of the schema. This is used
81+ -- to remove duplicates from the "reverse" error tree we get when we look for non-breaking changes.
82+ -- Generally if checking X->Y raises issue I, and checking Y->X raises issue J, I and J should be related.
83+ relatedIssues :: Issue l -> Issue l -> Bool
84+ relatedIssues = (==)
85+
86+-- Utility function for 'relatedIssues'. In @withClass eq f@, @f@ attempts to partition inputs into equivalence classes,
87+-- and two items in the same equivalence class are related. If both items aren't assigned to a class by @f@, instead
88+-- @eq@ is used to compare them.
89+withClass :: Eq b => (a -> a -> Bool) -> (a -> Maybe b) -> a -> a -> Bool
90+withClass eq f = \x y -> case (f x, f y) of
91+ (Just fx, Just fy) -> fx == fy
92+ (Nothing, Nothing) -> eq x y
93+ (_, _) -> False
62
94
63data Orientation = Forward | Backward
95data Orientation = Forward | Backward
64 deriving stock (Eq, Ord, Show)
96 deriving stock (Eq, Ord, Show)
@@ -84,3 +116,9 @@ deriving stock instance Ord (AnIssue l)
84
116
85instance ToJSON (AnIssue l) where
117instance ToJSON (AnIssue l) where
86 toJSON (AnIssue _ issue) = toJSON issue
118 toJSON (AnIssue _ issue) = toJSON issue
119+
120+anIssueKind :: AnIssue l -> IssueKind
121+anIssueKind (AnIssue _ i) = issueKind i
122+
123+relatedAnIssues :: AnIssue l -> AnIssue l -> Bool
124+relatedAnIssues (AnIssue _ x) (AnIssue _ y) = relatedIssues x y
src/OpenAPI/Checker/Common.hs → src/Data/OpenApi/Compare/Common.hsrenamed+2−2
@@ -1,5 +1,5 @@
1−module OpenAPI.Checker.Common
1+module Data.OpenApi.Compare.Common
2− ( zipAll
2+ ( zipAll,
3 )
3 )
4where
4where
5
5
src/OpenAPI/Checker/Formula.hs → src/Data/OpenApi/Compare/Formula.hsrenamed+35−25
@@ -1,20 +1,21 @@
1−module OpenAPI.Checker.Formula
1+module Data.OpenApi.Compare.Formula
2− ( FormulaF
2+ ( FormulaF,
3− , VarRef
3+ VarRef,
4− , variable
4+ variable,
5− , eitherOf
5+ eitherOf,
6− , anError
6+ anError,
7− , errors
7+ errors,
8− , calculate
8+ calculate,
9− , maxFixpoint
9+ maxFixpoint,
10− , mapErrors
10+ mapErrors,
11+ )
11− ) where
12+where
12
13
13import Data.Kind
14import Data.Kind
14−import Data.Monoid
15import qualified Data.List.NonEmpty as NE
15import qualified Data.List.NonEmpty as NE
16−import OpenAPI.Checker.Paths
16+import Data.Monoid
17+import Data.OpenApi.Compare.Paths
17−import qualified OpenAPI.Checker.PathsPrefixTree as P
18+import qualified Data.OpenApi.Compare.PathsPrefixTree as P
18
19
19type VarRef = Int
20type VarRef = Int
20
21
@@ -24,15 +25,24 @@ type VarRef = Int
24-- fixpoints always exist, i.e. that @x = f x@ has at least one solution.
25-- fixpoints always exist, i.e. that @x = f x@ has at least one solution.
25data FormulaF (q :: k -> k -> Type) (f :: k -> Type) (r :: k) (a :: Type) where
26data FormulaF (q :: k -> k -> Type) (f :: k -> Type) (r :: k) (a :: Type) where
26 Result :: a -> FormulaF q f r a
27 Result :: a -> FormulaF q f r a
28+ Errors ::
27− Errors :: !(P.PathsPrefixTree q f r) -> FormulaF q f r a
29+ !(P.PathsPrefixTree q f r) ->
28− -- ^ invariant: never empty
30+ -- | invariant: never empty
31+ FormulaF q f r a
32+ Apply ::
29− Apply :: FormulaF q f r (b -> c) -> FormulaF q f r b -> (c -> a) -> FormulaF q f r a
33+ FormulaF q f r (b -> c) ->
34+ FormulaF q f r b ->
35+ (c -> a) ->
30− -- ^ invariant: at least one of LHS and RHS is not 'Errors', and they are
36+ -- | invariant: at least one of LHS and RHS is not 'Errors', and they are
31 -- both not 'Result'
37 -- both not 'Result'
38+ FormulaF q f r a
39+ SelectFirst ::
32− SelectFirst :: NE.NonEmpty (SomeFormulaF b)
40+ NE.NonEmpty (SomeFormulaF b) ->
33− -> !(P.PathsPrefixTree q f r) -> (b -> a) -> FormulaF q f r a
41+ !(P.PathsPrefixTree q f r) ->
42+ (b -> a) ->
34− -- ^ invariant: the list doesn't contain any 'Result's, 'Errors' or
43+ -- | invariant: the list doesn't contain any 'Result's, 'Errors' or
35 -- 'SelectFirst'
44 -- 'SelectFirst'
45+ FormulaF q f r a
36 Variable :: !VarRef -> a -> FormulaF q f r a
46 Variable :: !VarRef -> a -> FormulaF q f r a
37
47
38mkApply :: FormulaF q f r (b -> c) -> FormulaF q f r b -> (c -> a) -> FormulaF q f r a
48mkApply :: FormulaF q f r (b -> c) -> FormulaF q f r b -> (c -> a) -> FormulaF q f r a
@@ -44,13 +54,13 @@ mkApply f x h = Apply f x h
44mkSelectFirst :: [SomeFormulaF b] -> P.PathsPrefixTree q f r -> (b -> a) -> FormulaF q f r a
54mkSelectFirst :: [SomeFormulaF b] -> P.PathsPrefixTree q f r -> (b -> a) -> FormulaF q f r a
45mkSelectFirst fs allE h = case foldMap check fs of
55mkSelectFirst fs allE h = case foldMap check fs of
46 (First (Just x), _) -> Result (h x)
56 (First (Just x), _) -> Result (h x)
47− (First Nothing, x:xs) -> SelectFirst (x NE.:| xs) allE h
57+ (First Nothing, x : xs) -> SelectFirst (x NE.:| xs) allE h
48 (First Nothing, []) -> Errors allE
58 (First Nothing, []) -> Errors allE
49 where
59 where
50 check (SomeFormulaF (Result x)) = (First (Just x), mempty)
60 check (SomeFormulaF (Result x)) = (First (Just x), mempty)
51 check (SomeFormulaF (Errors _)) = (mempty, mempty)
61 check (SomeFormulaF (Errors _)) = (mempty, mempty)
52− check (SomeFormulaF (SelectFirst xs _ h'))
62+ check (SomeFormulaF (SelectFirst xs _ h')) =
53− = (mempty, NE.toList (fmap (fmap h') xs))
63+ (mempty, NE.toList (fmap (fmap h') xs))
54 check x = (mempty, [x])
64 check x = (mempty, [x])
55
65
56data SomeFormulaF (a :: Type) where
66data SomeFormulaF (a :: Type) where
@@ -96,7 +106,7 @@ calculate (Apply f x h) = case calculate f of
96 Right x' -> Right (h (f' x'))
106 Right x' -> Right (h (f' x'))
97calculate (SelectFirst xs e h) = go (NE.toList xs)
107calculate (SelectFirst xs e h) = go (NE.toList xs)
98 where
108 where
99− go (SomeFormulaF r:rs) = case calculate r of
109+ go (SomeFormulaF r : rs) = case calculate r of
100 Left _ -> go rs
110 Left _ -> go rs
101 Right x -> Right (h x)
111 Right x -> Right (h x)
102 go [] = Left e
112 go [] = Left e
src/Data/OpenApi/Compare/Memo.hsnew file+164
@@ -0,0 +1,164 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
2+
3+-- | Utilities for effectfully memoizing other, more effectful functions.
4+module Data.OpenApi.Compare.Memo
5+ ( MonadMemo,
6+ MemoState,
7+ runMemo,
8+ modifyMemoNonce,
9+ KnotTier (..),
10+ unknot,
11+ memoWithKnot,
12+ memoTaggedWithKnot,
13+ )
14+where
15+
16+import Control.Monad.State
17+import Data.Dynamic
18+import qualified Data.Map as M
19+import Data.Tagged
20+import qualified Data.TypeRepMap as T
21+import Data.Void
22+import Type.Reflection
23+
24+data Progress a = Finished a | Started | TyingKnot Dynamic
25+
26+data MemoMap a where
27+ MemoMap :: !(M.Map k (Progress v)) -> MemoMap (k, v)
28+
29+data MemoState s = MemoState s (T.TypeRepMap MemoMap)
30+
31+-- | An effectful memoization monad.
32+type MonadMemo s m = MonadState (MemoState s) m
33+
34+memoStateLookup ::
35+ forall k v s.
36+ (Typeable k, Typeable v, Ord k) =>
37+ k ->
38+ MemoState s ->
39+ Maybe (Progress v)
40+memoStateLookup k (MemoState _ t) = case T.lookup @(k, v) t of
41+ Just (MemoMap m) -> M.lookup k m
42+ Nothing -> Nothing
43+
44+memoStateInsert ::
45+ forall k v s.
46+ (Typeable k, Typeable v, Ord k) =>
47+ k ->
48+ Progress v ->
49+ MemoState s ->
50+ MemoState s
51+memoStateInsert k x (MemoState s t) = MemoState s $ T.insert (MemoMap m'') t
52+ where
53+ m'' = M.insert k x m'
54+ m' = case T.lookup @(k, v) t of
55+ Just (MemoMap m) -> m
56+ Nothing -> M.empty
57+
58+modifyMemoNonce :: MonadMemo s m => (s -> s) -> m s
59+modifyMemoNonce f = do
60+ MemoState s t <- get
61+ put $ MemoState (f s) t
62+ pure s
63+
64+-- | Run a memoized computation.
65+runMemo :: Monad m => s -> StateT (MemoState s) m a -> m a
66+runMemo s = (`evalStateT` MemoState s T.empty)
67+
68+-- | A description of how to effectfully tie knots in type @v@, using the @m@
69+-- monad, and by sharing some @d@ data among the recursive instances.
70+data KnotTier v d m = KnotTier
71+ { -- | Create some data that will be connected to this knot
72+ onKnotFound :: m d
73+ , -- | This is what the knot will look like as a value
74+ -- to the inner computations
75+ onKnotUsed :: d -> m v
76+ , -- | Once we're done and we're outside, tie the
77+ -- knot using the datum
78+ tieKnot :: d -> v -> m v
79+ }
80+
81+unknot :: KnotTier v Void m
82+unknot =
83+ KnotTier
84+ { onKnotFound = error "Recursion detected"
85+ , onKnotUsed = absurd
86+ , tieKnot = absurd
87+ }
88+
89+-- | Run a potentially recursive computation. The provided key will be used to
90+-- refer to the result of this computation. If during the computation, another
91+-- attempt to run the computation with the same key is made, we run a
92+-- tying-the-knot procedure.
93+--
94+-- If another attempt to run the computation with the same key is made
95+-- *after we're done*, we will return the memoized value.
96+memoWithKnot ::
97+ forall k v d m s.
98+ (Typeable k, Typeable v, Typeable d, Ord k, MonadMemo s m) =>
99+ KnotTier v d m ->
100+ -- | the computation to memoize
101+ m v ->
102+ -- | key for memoization
103+ k ->
104+ m v
105+memoWithKnot tier f k =
106+ memoStateLookup @k @v k <$> get >>= \case
107+ Just (Finished v) -> pure v
108+ Just Started -> do
109+ d <- onKnotFound tier
110+ modify $ memoStateInsert @k @v k (TyingKnot $ toDyn d)
111+ onKnotUsed tier d
112+ Just (TyingKnot dyn) -> case fromDynamic dyn of
113+ Just d -> onKnotUsed tier d
114+ Nothing ->
115+ error $
116+ "Type mismatch when examining the knot of "
117+ <> show (typeRep @(k -> v))
118+ <> ": expected "
119+ <> show (typeRep @d)
120+ <> ", got "
121+ <> show (dynTypeRep dyn)
122+ Nothing -> do
123+ modify $ memoStateInsert @k @v k Started
124+ v <- f
125+ v' <-
126+ memoStateLookup @k @v k <$> get >>= \case
127+ Just Started -> pure v
128+ Just (TyingKnot dyn) -> case fromDynamic dyn of
129+ Just d -> tieKnot tier d v
130+ Nothing ->
131+ error $
132+ "Type mismatch when tying the knot of "
133+ <> show (typeRep @(k -> v))
134+ <> ": expected "
135+ <> show (typeRep @d)
136+ <> ", got "
137+ <> show (dynTypeRep dyn)
138+ Just (Finished _) ->
139+ error $
140+ "Unexpected Finished when memoizing "
141+ <> show (typeRep @(k -> v))
142+ Nothing -> pure v
143+ -- Normally this would be an error, but the underlying monad can refuse
144+ -- to remember memoization state
145+ modify $ memoStateInsert @k @v k (Finished v')
146+ pure v'
147+
148+-- | Disambiguate memoized computations with an arbitrary tag.
149+memoTaggedWithKnot ::
150+ forall t k v d m s.
151+ ( Typeable t
152+ , Typeable k
153+ , Typeable v
154+ , Typeable d
155+ , Ord k
156+ , MonadMemo s m
157+ ) =>
158+ KnotTier v d m ->
159+ m v ->
160+ k ->
161+ m v
162+memoTaggedWithKnot tier f k =
163+ withTypeable (typeRepKind $ typeRep @t) $
164+ memoWithKnot tier f (Tagged @t k)
src/OpenAPI/Checker/Orphans.hs → src/Data/OpenApi/Compare/Orphans.hsrenamed+1−1
@@ -1,6 +1,6 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Orphans () where
3+module Data.OpenApi.Compare.Orphans () where
4
4
5import Control.Comonad.Env
5import Control.Comonad.Env
6import qualified Data.HashMap.Strict.InsOrd as IOHM
6import qualified Data.HashMap.Strict.InsOrd as IOHM
src/OpenAPI/Checker/Paths.hs → src/Data/OpenApi/Compare/Paths.hsrenamed+10−10
@@ -1,18 +1,18 @@
1-- | Utilities for traversing heterogeneous trees. A heterogeneous tree is a
1-- | Utilities for traversing heterogeneous trees. A heterogeneous tree is a
2-- collection of datatypes that "contain" eachother in some form of tree
2-- collection of datatypes that "contain" eachother in some form of tree
3-- structure.
3-- structure.
4−module OpenAPI.Checker.Paths
4+module Data.OpenApi.Compare.Paths
5− ( NiceQuiver
5+ ( NiceQuiver,
6− , AdditionalQuiverConstraints
6+ AdditionalQuiverConstraints,
7− , Paths (..)
7+ Paths (..),
8− , DiffPaths (..)
8+ DiffPaths (..),
9− , catDiffPaths
9+ catDiffPaths,
10− , AnItem (..)
10+ AnItem (..),
11− , step
11+ step,
12
12
13 -- * Reexports
13 -- * Reexports
14− , (>>>)
14+ (>>>),
15− , (<<<)
15+ (<<<),
16 )
16 )
17where
17where
18
18
src/OpenAPI/Checker/PathsPrefixTree.hs → src/Data/OpenApi/Compare/PathsPrefixTree.hsrenamed+65−42
@@ -1,39 +1,42 @@
1{-# LANGUAGE QuantifiedConstraints #-}
1{-# LANGUAGE QuantifiedConstraints #-}
2
2
3−module OpenAPI.Checker.PathsPrefixTree
3+module Data.OpenApi.Compare.PathsPrefixTree
4− ( PathsPrefixTree (PathsPrefixNode)
4+ ( PathsPrefixTree (PathsPrefixNode),
5− , AStep (..)
5+ AStep (..),
6− , empty
6+ empty,
7− , singleton
7+ singleton,
8− , fromList
8+ fromList,
9− , null
9+ null,
10− , foldWith
10+ foldWith,
11− , toList
11+ toList,
12− , filter
12+ filter,
13+ filterWithKey,
13− , takeSubtree
14+ takeSubtree,
15+ lookup,
14− , embed
16+ embed,
15− , size
17+ size,
16− , partition
18+ partition,
17− , map
19+ map,
18 )
20 )
19where
21where
20
22
21import Control.Monad
23import Control.Monad
22import Data.Aeson
24import Data.Aeson
23import Data.Foldable hiding (null, toList)
25import Data.Foldable hiding (null, toList)
24−import qualified Data.HashMap.Strict as HM
26+import qualified Data.Aeson.Key as Key
27+import qualified Data.Aeson.KeyMap as KeyMap
25import Data.Kind
28import Data.Kind
26import qualified Data.Map as M
29import qualified Data.Map as M
27import Data.Monoid
30import Data.Monoid
31+import Data.OpenApi.Compare.Paths
28import qualified Data.Set as S
32import qualified Data.Set as S
29import qualified Data.Text as T
33import qualified Data.Text as T
30import Data.Type.Equality
34import Data.Type.Equality
31import qualified Data.TypeRepMap as TRM
35import qualified Data.TypeRepMap as TRM
32import qualified Data.Vector as V
36import qualified Data.Vector as V
33import qualified GHC.Exts as Exts
37import qualified GHC.Exts as Exts
34−import OpenAPI.Checker.Paths
35import Type.Reflection
38import Type.Reflection
36−import Prelude hiding (filter, map, null)
39+import Prelude hiding (filter, lookup, map, null)
37
40
38-- | A list of @AnItem r f@, but optimized into a prefix tree.
41-- | A list of @AnItem r f@, but optimized into a prefix tree.
39data PathsPrefixTree (q :: k -> k -> Type) (f :: k -> Type) (r :: k) = PathsPrefixTree
42data PathsPrefixTree (q :: k -> k -> Type) (f :: k -> Type) (r :: k) = PathsPrefixTree
@@ -56,10 +59,27 @@ filter f (PathsPrefixTree roots branches) = PathsPrefixTree roots' branches'
56 roots' = filterASet f roots
59 roots' = filterASet f roots
57 branches' =
60 branches' =
58 Exts.fromList
61 Exts.fromList
59− . fmap (\(TRM.WrapTypeable (AStep x)) -> TRM.WrapTypeable . AStep . M.filter (not . null) . fmap (filter f) $ x)
62+ . fmap (\(TRM.WrapTypeable (AStep x)) -> TRM.WrapTypeable . AStep . M.mapMaybe (maybeNonEmpty . filter f) $ x)
60 . Exts.toList
63 . Exts.toList
61 $ branches
64 $ branches
62
65
66+ maybeNonEmpty = mfilter (not . null) . Just
67+
68+filterWithKey :: (forall a. Paths q r a -> f a -> Bool) -> PathsPrefixTree q f r -> PathsPrefixTree q f r
69+filterWithKey = go Root
70+ where
71+ go :: Paths q r b -> (forall a. Paths q r a -> f a -> Bool) -> PathsPrefixTree q f b -> PathsPrefixTree q f b
72+ go xs f (PathsPrefixTree roots branches) = PathsPrefixTree roots' branches'
73+ where
74+ roots' = filterASet (f xs) roots
75+ branches' =
76+ Exts.fromList
77+ . fmap (\(TRM.WrapTypeable (AStep x)) -> TRM.WrapTypeable . AStep . M.mapMaybeWithKey (\k -> maybeNonEmpty . go (xs `Snoc` k) f) $ x)
78+ . Exts.toList
79+ $ branches
80+
81+ maybeNonEmpty = mfilter (not . null) . Just
82+
63-- | The number of leaves.
83-- | The number of leaves.
64size :: PathsPrefixTree q f r -> Int
84size :: PathsPrefixTree q f r -> Int
65size (PathsPrefixTree root branches) =
85size (PathsPrefixTree root branches) =
@@ -83,11 +103,11 @@ deriving stock instance Eq (PathsPrefixTree q f a)
83
103
84-- Kind of orphan. Treat the map as an infinite tuple of @Maybe (f a)@'s, where
104-- Kind of orphan. Treat the map as an infinite tuple of @Maybe (f a)@'s, where
85-- the components are ordered by the @SomeTypeRep@ of the @a@.
105-- the components are ordered by the @SomeTypeRep@ of the @a@.
86−compareTRM
106+compareTRM ::
87− :: (forall a. Typeable a => Ord (f a))
107+ (forall a. Typeable a => Ord (f a)) =>
88− => TRM.TypeRepMap f
108+ TRM.TypeRepMap f ->
89− -> TRM.TypeRepMap f
109+ TRM.TypeRepMap f ->
90− -> Ordering
110+ Ordering
91compareTRM s1 s2 =
111compareTRM s1 s2 =
92 foldMap (\k -> compareMaybe compareW (M.lookup k m1) (M.lookup k m2)) mKeys
112 foldMap (\k -> compareMaybe compareW (M.lookup k m1) (M.lookup k m2)) mKeys
93 where
113 where
@@ -97,11 +117,11 @@ compareTRM s1 s2 =
97 compareMaybe _ Nothing (Just _) = LT
117 compareMaybe _ Nothing (Just _) = LT
98 compareMaybe _ (Just _) Nothing = GT
118 compareMaybe _ (Just _) Nothing = GT
99 compareMaybe cmp (Just x) (Just y) = cmp x y
119 compareMaybe cmp (Just x) (Just y) = cmp x y
100− compareW
120+ compareW ::
101− :: (forall a. Typeable a => Ord (f a))
121+ (forall a. Typeable a => Ord (f a)) =>
102− => TRM.WrapTypeable f
122+ TRM.WrapTypeable f ->
103− -> TRM.WrapTypeable f
123+ TRM.WrapTypeable f ->
104− -> Ordering
124+ Ordering
105 compareW (TRM.WrapTypeable (x :: f a)) (TRM.WrapTypeable (y :: f b))
125 compareW (TRM.WrapTypeable (x :: f a)) (TRM.WrapTypeable (y :: f b))
106 | Just Refl <- testEquality (typeRep @a) (typeRep @b) = compare x y
126 | Just Refl <- testEquality (typeRep @a) (typeRep @b) = compare x y
107 | otherwise = EQ -- unreachable
127 | otherwise = EQ -- unreachable
@@ -153,10 +173,10 @@ instance Monoid (ASet a) where
153 mempty = AnEmptySet
173 mempty = AnEmptySet
154
174
155data AStep (q :: k -> k -> Type) (f :: k -> Type) (r :: k) (a :: k) where
175data AStep (q :: k -> k -> Type) (f :: k -> Type) (r :: k) (a :: k) where
156− AStep
176+ AStep ::
157− :: NiceQuiver q r a =>
177+ NiceQuiver q r a =>
158− !(M.Map (q r a) (PathsPrefixTree q f a))
178+ !(M.Map (q r a) (PathsPrefixTree q f a)) ->
159− -> AStep q f r a
179+ AStep q f r a
160
180
161mapAStep :: (forall x. f x -> f x) -> AStep q f r a -> AStep q f r a
181mapAStep :: (forall x. f x -> f x) -> AStep q f r a -> AStep q f r a
162mapAStep f (AStep m) = AStep $ M.map (map f) m
182mapAStep f (AStep m) = AStep $ M.map (map f) m
@@ -196,12 +216,12 @@ null :: PathsPrefixTree q f r -> Bool
196null (PathsPrefixTree AnEmptySet s) = all (\(TRM.WrapTypeable (AStep x)) -> all null x) (Exts.toList s)
216null (PathsPrefixTree AnEmptySet s) = all (\(TRM.WrapTypeable (AStep x)) -> all null x) (Exts.toList s)
197null _ = False
217null _ = False
198
218
199−foldWith
219+foldWith ::
200− :: forall q f m r.
220+ forall q f m r.
201− Monoid m
221+ Monoid m =>
202− => (forall a. Ord (f a) => Paths q r a -> f a -> m)
222+ (forall a. Ord (f a) => Paths q r a -> f a -> m) ->
203− -> PathsPrefixTree q f r
223+ PathsPrefixTree q f r ->
204− -> m
224+ m
205foldWith k = goTPT Root
225foldWith k = goTPT Root
206 where
226 where
207 goTPT :: forall a. Paths q r a -> PathsPrefixTree q f a -> m
227 goTPT :: forall a. Paths q r a -> PathsPrefixTree q f a -> m
@@ -225,6 +245,9 @@ takeSubtree (Snoc xs x) t =
225 foldMap (\(AStep m) -> fold $ M.lookup x m) $
245 foldMap (\(AStep m) -> fold $ M.lookup x m) $
226 TRM.lookup @a $ snocItems $ takeSubtree xs t
246 TRM.lookup @a $ snocItems $ takeSubtree xs t
227
247
248+lookup :: Paths q r a -> PathsPrefixTree q f r -> S.Set (f a)
249+lookup xs = toSet . rootItems . takeSubtree xs
250+
228-- | Embed a subtree in a larger tree with given prefix
251-- | Embed a subtree in a larger tree with given prefix
229embed :: Paths q r a -> PathsPrefixTree q f a -> PathsPrefixTree q f r
252embed :: Paths q r a -> PathsPrefixTree q f a -> PathsPrefixTree q f r
230embed Root t = t
253embed Root t = t
@@ -234,7 +257,7 @@ newtype MergableObject = MergableObject {getMergableObject :: Object}
234
257
235instance Semigroup MergableObject where
258instance Semigroup MergableObject where
236 (MergableObject x) <> (MergableObject y) =
259 (MergableObject x) <> (MergableObject y) =
237− MergableObject $ HM.unionWith mergeValue x y
260+ MergableObject $ KeyMap.unionWith mergeValue x y
238 where
261 where
239 mergeValue :: Value -> Value -> Value
262 mergeValue :: Value -> Value -> Value
240 mergeValue (Object a) (Object b) =
263 mergeValue (Object a) (Object b) =
@@ -249,6 +272,6 @@ instance Monoid MergableObject where
249
272
250traceObject :: Paths q r a -> Value -> Object
273traceObject :: Paths q r a -> Value -> Object
251traceObject Root (Object o) = o
274traceObject Root (Object o) = o
252−traceObject Root v = HM.singleton "root" v
275+traceObject Root v = KeyMap.singleton "root" v
253traceObject (root `Snoc` s) v =
276traceObject (root `Snoc` s) v =
254− traceObject root . Object $ HM.singleton (T.pack . show $ s) v
277+ traceObject root . Object $ KeyMap.singleton (Key.fromText . T.pack . show $ s) v
src/OpenAPI/Checker/References.hs → src/Data/OpenApi/Compare/References.hsrenamed+11−12
@@ -1,9 +1,9 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.References
3+module Data.OpenApi.Compare.References
4− ( Step (..)
4+ ( Step (..),
5− , dereference
5+ dereference,
6− , Typeable
6+ Typeable,
7 )
7 )
8where
8where
9
9
@@ -11,19 +11,18 @@ import Data.HList
11import qualified Data.HashMap.Strict.InsOrd as IOHM
11import qualified Data.HashMap.Strict.InsOrd as IOHM
12import Data.Maybe
12import Data.Maybe
13import Data.OpenApi
13import Data.OpenApi
14−import Data.Typeable
14+import Data.OpenApi.Compare.Orphans ()
15−import OpenAPI.Checker.Orphans ()
16−import OpenAPI.Checker.Subtree
15+import Data.OpenApi.Compare.Subtree
17
16
18instance Typeable a => Steppable (Referenced a) a where
17instance Typeable a => Steppable (Referenced a) a where
19 data Step (Referenced a) a = InlineStep
18 data Step (Referenced a) a = InlineStep
20 deriving stock (Eq, Ord, Show)
19 deriving stock (Eq, Ord, Show)
21
20
22−dereference
21+dereference ::
23− :: Typeable a
22+ Typeable a =>
24− => Traced (Definitions a)
23+ Traced (Definitions a) ->
25− -> Traced (Referenced a)
24+ Traced (Referenced a) ->
26− -> Traced a
25+ Traced a
27dereference defs x = case extract x of
26dereference defs x = case extract x of
28 Inline a ->
27 Inline a ->
29 traced (ask x >>> step InlineStep) a
28 traced (ask x >>> step InlineStep) a
src/OpenAPI/Checker/Report.hs → src/Data/OpenApi/Compare/Report.hsrenamed+177−117
@@ -1,11 +1,13 @@
1−module OpenAPI.Checker.Report
1+module Data.OpenApi.Compare.Report
2− ( generateReport
2+ ( generateReport,
3+ CheckerOutput (..),
3− , ReportInput (..)
4+ ReportInput (..),
5+ segregateIssues,
4− , ReportStatus (..)
6+ ReportStatus (..),
5− , Pandoc
7+ Pandoc,
6− , ReportConfig (..)
8+ ReportConfig (..),
7− , ReportTreeStyle (..)
9+ ReportTreeStyle (..),
8− , ReportMode (..)
10+ ReportMode (..),
9 )
11 )
10where
12where
11
13
@@ -21,6 +23,15 @@ import qualified Data.Map as M
21import Data.Map.Ordered (OMap)
23import Data.Map.Ordered (OMap)
22import qualified Data.Map.Ordered as OM
24import qualified Data.Map.Ordered as OM
23import Data.Maybe
25import Data.Maybe
26+import Data.OpenApi.Compare.Behavior
27+import Data.OpenApi.Compare.Paths
28+import Data.OpenApi.Compare.PathsPrefixTree hiding (empty)
29+import qualified Data.OpenApi.Compare.PathsPrefixTree as P hiding (empty)
30+import Data.OpenApi.Compare.Report.Jet
31+import Data.OpenApi.Compare.Subtree (invertIssueOrientationP)
32+import Data.OpenApi.Compare.Validate.OpenApi
33+import Data.OpenApi.Compare.Validate.Schema.Issues
34+import Data.OpenApi.Compare.Validate.Schema.TypedJson
24import Data.OpenUnion
35import Data.OpenUnion
25import Data.OpenUnion.Extra
36import Data.OpenUnion.Extra
26import Data.Set
37import Data.Set
@@ -30,26 +41,47 @@ import qualified Data.Text as T
30import Data.TypeRepMap hiding (empty)
41import Data.TypeRepMap hiding (empty)
31import Data.Typeable
42import Data.Typeable
32import Generic.Data
43import Generic.Data
33−import OpenAPI.Checker.Behavior
34−import OpenAPI.Checker.Paths
35−import OpenAPI.Checker.PathsPrefixTree hiding (empty)
36−import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
37−import OpenAPI.Checker.Report.Jet
38−import OpenAPI.Checker.Subtree (invertIssueOrientationP)
39−import OpenAPI.Checker.Validate.OpenApi
40−import OpenAPI.Checker.Validate.Schema
41import Text.Pandoc.Builder
44import Text.Pandoc.Builder
42
45
43type Changes = P.PathsPrefixTree Behave AnIssue 'APILevel
46type Changes = P.PathsPrefixTree Behave AnIssue 'APILevel
44
47
48+data CheckerOutput = CheckerOutput
49+ { forwardChanges :: Changes
50+ , backwardChanges :: Changes
51+ }
52+ deriving stock (Generic)
53+ deriving (Semigroup, Monoid) via (Generically CheckerOutput)
54+ deriving anyclass (ToJSON)
55+
45data ReportInput = ReportInput
56data ReportInput = ReportInput
57+ { -- | forward 'CertainIssue', 'ProbablyIssue' and 'Comment'
46− { breakingChanges :: Changes
58+ breakingChanges :: Changes
59+ , -- | backward 'CertainIssue', 'ProbablyIssue' and 'Comment', except those shadowed by 'relatedIssues'
47− , nonBreakingChanges :: Changes
60+ nonBreakingChanges :: Changes
61+ , -- | forward and backward 'Unsupported' (assumed to be the same anyway)
62+ unsupportedChanges :: Changes
63+ , -- | forward and backward 'SchemaInvalid' (assumed to be the same anyway)
64+ schemaIssues :: Changes
48 }
65 }
49 deriving stock (Generic)
66 deriving stock (Generic)
50 deriving (Semigroup, Monoid) via (Generically ReportInput)
67 deriving (Semigroup, Monoid) via (Generically ReportInput)
51 deriving anyclass (ToJSON)
68 deriving anyclass (ToJSON)
52
69
70+segregateIssues :: CheckerOutput -> ReportInput
71+segregateIssues CheckerOutput {forwardChanges = fwd, backwardChanges = bck} =
72+ ReportInput
73+ { breakingChanges = P.filter isBreaking fwd
74+ , nonBreakingChanges = invertIssueOrientationP $ P.filterWithKey isNonBreaking bck
75+ , unsupportedChanges = P.filter isUnsupported fwd <> P.filter isUnsupported bck
76+ , schemaIssues = P.filter isSchemaIssue fwd <> P.filter isSchemaIssue bck
77+ }
78+ where
79+ isBreaking i = anIssueKind i `elem` [CertainIssue, ProbablyIssue, Comment]
80+ isNonBreaking :: Paths Behave 'APILevel a -> AnIssue a -> Bool
81+ isNonBreaking xs i = isBreaking i && all (\j -> not $ relatedAnIssues i j) (P.lookup xs fwd)
82+ isUnsupported i = anIssueKind i == Unsupported
83+ isSchemaIssue i = anIssueKind i == SchemaInvalid
84+
53data ReportStatus
85data ReportStatus
54 = BreakingChanges
86 = BreakingChanges
55 | NoBreakingChanges
87 | NoBreakingChanges
@@ -77,49 +109,66 @@ data ReportTreeStyle = HeadersTreeStyle | FoldingBlockquotesTreeStyle
77twoRowTable :: [(Inlines, Inlines)] -> Blocks
109twoRowTable :: [(Inlines, Inlines)] -> Blocks
78twoRowTable x = simpleTable (para . fst <$> x) [para . snd <$> x]
110twoRowTable x = simpleTable (para . fst <$> x) [para . snd <$> x]
79
111
80−generateReport :: ReportConfig -> ReportInput -> (Pandoc, ReportStatus)
112+generateReport :: ReportConfig -> ReportInput -> (Blocks, ReportStatus)
81generateReport cfg inp =
113generateReport cfg inp =
82− let partitionUnsupported = P.partition (\(AnIssue _ i) -> issueIsUnsupported i)
114+ let schemaIssuesPresent = not $ P.null $ schemaIssues inp
83− (bUnsupported, breaking) = partitionUnsupported $ breakingChanges inp
115+ breakingChangesPresent = not $ P.null $ breakingChanges inp
84− (nbUnsupported, invertIssueOrientationP -> nonBreaking) = partitionUnsupported $ nonBreakingChanges inp
85− unsupported = bUnsupported <> nbUnsupported
86− breakingChangesPresent = not $ P.null breaking
87− nonBreakingChangesPresent = not $ P.null nonBreaking
116+ nonBreakingChangesPresent = not $ P.null $ nonBreakingChanges inp
88− unsupportedChangesPresent = not $ P.null unsupported
117+ unsupportedChangesPresent = not $ P.null $ unsupportedChanges inp
89 nonBreakingChangesShown = case reportMode cfg of
118 nonBreakingChangesShown = case reportMode cfg of
90 All -> True
119 All -> True
91 OnlyErrors -> False
120 OnlyErrors -> False
92 builder = buildReport cfg
121 builder = buildReport cfg
93 report =
122 report =
94− doc $
95− header 1 "Summary"
123+ header 1 "Summary"
96− <> twoRowTable
124+ <> twoRowTable
97− ([ ( refOpt breakingChangesPresent breakingChangesId "⚠️ Breaking changes"
125+ ( when'
126+ schemaIssuesPresent
127+ [
128+ ( refOpt schemaIssuesPresent schemaIssuesId "‼️ Schema issues"
98− , show' $ P.size breaking
129+ , show' $ P.size $ schemaIssues inp
99− )
130+ )
100− ]
131+ ]
101− ++ when'
132+ ++ [
102− nonBreakingChangesShown
133+ ( refOpt breakingChangesPresent breakingChangesId "❌ Breaking changes"
103− [ ( refOpt nonBreakingChangesPresent nonBreakingChangesId "🙆 Non-breaking changes"
134+ , show' $ P.size $ breakingChanges inp
104− , show' $ P.size nonBreaking
105 )
135 )
106 ]
136 ]
137+ ++ when'
138+ nonBreakingChangesShown
139+ [
140+ ( refOpt nonBreakingChangesPresent nonBreakingChangesId "⚠️ Non-breaking changes"
141+ , show' $ P.size $ nonBreakingChanges inp
142+ )
143+ ]
144+ ++ when'
145+ unsupportedChangesPresent
146+ [
107− ++ [ ( refOpt unsupportedChangesPresent unsupportedChangesId "🤷 Unsupported feature changes"
147+ ( refOpt unsupportedChangesPresent unsupportedChangesId "❓ Unsupported feature changes"
108− , show' $ P.size unsupported
148+ , show' $ P.size $ unsupportedChanges inp
109− )
149+ )
110− ])
150+ ]
151+ )
152+ <> when'
153+ schemaIssuesPresent
154+ ( header 1 (anchor schemaIssuesId <> "‼️ Schema issues")
155+ <> builder (showErrs $ schemaIssues inp)
156+ )
111− <> when'
157+ <> when'
112− breakingChangesPresent
158+ breakingChangesPresent
113− (header 1 (anchor breakingChangesId <> "⚠️ Breaking changes")
159+ ( header 1 (anchor breakingChangesId <> "❌ Breaking changes")
114− <> builder (showErrs breaking))
160+ <> builder (showErrs $ breakingChanges inp)
161+ )
115− <> when'
162+ <> when'
116− (nonBreakingChangesPresent && nonBreakingChangesShown)
163+ (nonBreakingChangesPresent && nonBreakingChangesShown)
117− (header 1 (anchor nonBreakingChangesId <> "🙆 Non-breaking changes")
164+ ( header 1 (anchor nonBreakingChangesId <> "⚠️ Non-breaking changes")
118− <> builder (showErrs nonBreaking))
165+ <> builder (showErrs $ nonBreakingChanges inp)
166+ )
119− <> when'
167+ <> when'
120− unsupportedChangesPresent
168+ unsupportedChangesPresent
121− (header 1 (anchor unsupportedChangesId <> "🤷 Unsupported feature changes")
169+ ( header 1 (anchor unsupportedChangesId <> "❓ Unsupported feature changes")
122− <> builder (showErrs unsupported))
170+ <> builder (showErrs $ unsupportedChanges inp)
171+ )
123 status =
172 status =
124 if
173 if
125 | breakingChangesPresent -> BreakingChanges
174 | breakingChangesPresent -> BreakingChanges
@@ -134,10 +183,11 @@ generateReport cfg inp =
134 refOpt False _ i = i
183 refOpt False _ i = i
135 refOpt True a i = link ("#" <> a) "" i
184 refOpt True a i = link ("#" <> a) "" i
136
185
137− breakingChangesId, nonBreakingChangesId, unsupportedChangesId :: Text
186+ breakingChangesId, nonBreakingChangesId, unsupportedChangesId, schemaIssuesId :: Text
138 breakingChangesId = "breaking-changes"
187 breakingChangesId = "breaking-changes"
139 unsupportedChangesId = "unsupported-changes"
188 unsupportedChangesId = "unsupported-changes"
140 nonBreakingChangesId = "non-breaking-changes"
189 nonBreakingChangesId = "non-breaking-changes"
190+ schemaIssuesId = "schema-issues"
141
191
142 when' :: Monoid m => Bool -> m -> m
192 when' :: Monoid m => Bool -> m -> m
143 when' True m = m
193 when' True m = m
@@ -152,9 +202,10 @@ showErrs x@(P.PathsPrefixNode currentIssues _) =
152 Just Refl
202 Just Refl
153 | (S.toList -> p@((AnIssue ori _) : _), o) <-
203 | (S.toList -> p@((AnIssue ori _) : _), o) <-
154 S.partition
204 S.partition
155− (\((AnIssue _ u)) -> case u of
205+ ( \((AnIssue _ u)) -> case u of
156− NoPathsMatched {} -> True
206+ NoPathsMatched {} -> True
157− AllPathsFailed {} -> True)
207+ AllPathsFailed {} -> True
208+ )
158 currentIssues ->
209 currentIssues ->
159 let p' = p <&> (\(AnIssue _ i) -> i)
210 let p' = p <&> (\(AnIssue _ i) -> i)
160 in (Just (ori, p'), o)
211 in (Just (ori, p'), o)
@@ -165,9 +216,10 @@ showErrs x@(P.PathsPrefixNode currentIssues _) =
165 paths = case removedPaths of
216 paths = case removedPaths of
166 Just (ori, ps) -> do
217 Just (ori, ps) -> do
167 singletonHeader
218 singletonHeader
168− (case ori of
219+ ( case ori of
169− Forward -> "Removed paths"
220+ Forward -> "Removed paths"
170− Backward -> "Added paths")
221+ Backward -> "Added paths"
222+ )
171 $ singletonBody $
223 $ singletonBody $
172 bulletList $
224 bulletList $
173 ps <&> \case
225 ps <&> \case
@@ -179,7 +231,7 @@ showErrs x@(P.PathsPrefixNode currentIssues _) =
179 flip foldMap (M.toList m) $ \(bhv, subErrors) ->
231 flip foldMap (M.toList m) $ \(bhv, subErrors) ->
180 if P.null subErrors
232 if P.null subErrors
181 then mempty
233 then mempty
182− else singletonHeader (describeBehaviour bhv) $ showErrs subErrors
234+ else singletonHeader (describeBehavior bhv) $ showErrs subErrors
183 in issues <> paths <> rest
235 in issues <> paths <> rest
184
236
185unfoldFunctions :: forall m a. (Monoid m, Eq a) => a -> [a -> (m, a)] -> (a -> m) -> m
237unfoldFunctions :: forall m a. (Monoid m, Eq a) => a -> [a -> (m, a)] -> (a -> m) -> m
@@ -197,64 +249,68 @@ jets =
197 unwrapReportJetResult
249 unwrapReportJetResult
198 <$> [ constructReportJet $
250 <$> [ constructReportJet $
199 curry $ \case
251 curry $ \case
200− (OfType Object, p@(InPartition _)) -> Just $ describeBehaviour p :: Maybe Inlines
252+ (OfType Object, p@(InPartition _)) -> Just $ describeBehavior p :: Maybe Inlines
201 _ -> Nothing
253 _ -> Nothing
202 , constructReportJet jsonPathJet
254 , constructReportJet jsonPathJet
203 , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
255 , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
204− strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
256+ strong (describeBehavior op) <> " " <> describeBehavior p :: Inlines
205 , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
257 , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
206− "📱⬅️ JSON Response – " <> str (T.pack . show $ c) :: Inlines
258+ "⬅️☁️ JSON Response – " <> str (T.pack . show $ c) :: Inlines
207− , constructReportJet $ \InRequest InPayload PayloadSchema -> "📱➡️ JSON Request" :: Inlines
259+ , constructReportJet $ \InRequest InPayload PayloadSchema -> "➡️☁️ JSON Request" :: Inlines
208 ]
260 ]
209 where
261 where
210 unwrapReportJetResult :: ReportJetResult Behave x -> ReportJet' Behave x
262 unwrapReportJetResult :: ReportJetResult Behave x -> ReportJet' Behave x
211 unwrapReportJetResult (Pure _) = error "There really shouldn't be any results here."
263 unwrapReportJetResult (Pure _) = error "There really shouldn't be any results here."
212 unwrapReportJetResult (Free f) = f
264 unwrapReportJetResult (Free f) = f
213
265
214− jsonPathJet
266+ jsonPathJet ::
215− :: NonEmpty
267+ NonEmpty
216− ( Union
268+ ( Union
217− '[ Behave 'SchemaLevel 'TypedSchemaLevel
269+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
218− , Behave 'TypedSchemaLevel 'SchemaLevel
270+ , Behave 'TypedSchemaLevel 'SchemaLevel
219− ]
271+ ]
220− )
272+ ) ->
221− -> Inlines
273+ Inlines
222 jsonPathJet x = code $ "$" <> showParts (NE.toList x)
274 jsonPathJet x = code $ "$" <> showParts (NE.toList x)
223 where
275 where
224− showParts
276+ showParts ::
225− :: [ Union
277+ [ Union
226− '[ Behave 'SchemaLevel 'TypedSchemaLevel
278+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
227− , Behave 'TypedSchemaLevel 'SchemaLevel
279+ , Behave 'TypedSchemaLevel 'SchemaLevel
228− ]
229− ]
280+ ]
281+ ] ->
230− -> Text
282+ Text
231 showParts [] = mempty
283 showParts [] = mempty
232 showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion (InProperty _)) : _)) = showParts xs
284 showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion (InProperty _)) : _)) = showParts xs
233 showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion InAdditionalProperty) : _)) = showParts xs
285 showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion InAdditionalProperty) : _)) = showParts xs
234 showParts (SingletonUnion (OfType Array) : xs@(SingletonUnion InItems : _)) = showParts xs
286 showParts (SingletonUnion (OfType Array) : xs@(SingletonUnion InItems : _)) = showParts xs
287+ showParts (SingletonUnion (OfType Array) : xs@(SingletonUnion (InItem _) : _)) = showParts xs
235 showParts (y : ys) =
288 showParts (y : ys) =
236− ((\(OfType t) -> "(" <> describeJSONType t <> ")")
289+ ( (\(OfType t) -> "(" <> describeJSONType t <> ")")
237− @@> (\case
290+ @@> ( \case
238− InItems -> "[*]"
291+ InItems -> "[*]"
292+ InItem i -> "[" <> T.pack (show i) <> "]"
239− InProperty p -> "." <> p
293+ InProperty p -> "." <> p
240− InAdditionalProperty -> ".*")
294+ InAdditionalProperty -> ".*"
295+ )
241− @@> typesExhausted)
296+ @@> typesExhausted
297+ )
242 y
298 y
243 <> showParts ys
299 <> showParts ys
244
300
245−observeJetShowErrs
301+observeJetShowErrs ::
246− :: ReportJet' Behave (Maybe Inlines)
302+ ReportJet' Behave (Maybe Inlines) ->
247− -> P.PathsPrefixTree Behave AnIssue a
303+ P.PathsPrefixTree Behave AnIssue a ->
248− -> (Report, P.PathsPrefixTree Behave AnIssue a)
304+ (Report, P.PathsPrefixTree Behave AnIssue a)
249observeJetShowErrs jet p = case observeJetShowErrs' jet p of
305observeJetShowErrs jet p = case observeJetShowErrs' jet p of
250 Just m -> m
306 Just m -> m
251 Nothing -> (mempty, p)
307 Nothing -> (mempty, p)
252
308
253−observeJetShowErrs'
309+observeJetShowErrs' ::
254− :: forall a.
310+ forall a.
255− ReportJet' Behave (Maybe Inlines)
311+ ReportJet' Behave (Maybe Inlines) ->
256− -> P.PathsPrefixTree Behave AnIssue a
312+ P.PathsPrefixTree Behave AnIssue a ->
257− -> Maybe (Report, P.PathsPrefixTree Behave AnIssue a)
313+ Maybe (Report, P.PathsPrefixTree Behave AnIssue a)
258observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
314observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
259 let results =
315 let results =
260 subIssues >>= \(WrapTypeable (AStep m)) ->
316 subIssues >>= \(WrapTypeable (AStep m)) ->
@@ -262,21 +318,23 @@ observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues)
262 maybe (Left $ embed (step bhv) subErrs) Right . listToMaybe $
318 maybe (Left $ embed (step bhv) subErrs) Right . listToMaybe $
263 jet @_ @_ @[] bhv
319 jet @_ @_ @[] bhv
264 & mapMaybe
320 & mapMaybe
265− (\case
321+ ( \case
266− Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
322+ Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
267− Pure (Just h) ->
323+ Pure (Just h) ->
268− if P.null subErrs
324+ if P.null subErrs
269− then Just mempty
325+ then Just mempty
270− else Just (singletonHeader h (showErrs subErrs), mempty)
326+ else Just (singletonHeader h (showErrs subErrs), mempty)
271− Pure Nothing -> Nothing)
327+ Pure Nothing -> Nothing
328+ )
272 in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
329 in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
273 if any isRight results
330 if any isRight results
274 then
331 then
275 Just $
332 Just $
276 foldMap
333 foldMap
277− (\case
334+ ( \case
278− Left e -> (mempty, e)
335+ Left e -> (mempty, e)
279− Right m -> m)
336+ Right m -> m
337+ )
280 results
338 results
281 else Nothing
339 else Nothing
282
340
@@ -299,8 +357,9 @@ buildReport cfg = case treeStyle cfg of
299 body rprt
357 body rprt
300 <> foldOMapWithKey
358 <> foldOMapWithKey
301 (headers rprt)
359 (headers rprt)
302− (\k v ->
360+ ( \k v ->
303− header level k <> subBuilder v)
361+ header level k <> subBuilder v
362+ )
304 where
363 where
305 subBuilder = headerStyleBuilder (level + 1)
364 subBuilder = headerStyleBuilder (level + 1)
306
365
@@ -309,16 +368,17 @@ buildReport cfg = case treeStyle cfg of
309 body rprt
368 body rprt
310 <> foldOMapWithKey
369 <> foldOMapWithKey
311 (headers rprt)
370 (headers rprt)
312− (\k v ->
371+ ( \k v ->
313− if (OM.size . headers $ rprt) < 2
372+ if (OM.size . headers $ rprt) < 2
314− then para k <> blockQuote (subBuilder v)
373+ then para k <> blockQuote (subBuilder v)
315− else
374+ else
316− rawHtml "<details>"
375+ rawHtml "<details>"
317− <> rawHtml "<summary>"
376+ <> rawHtml "<summary>"
318− <> plain k
377+ <> plain k
319− <> rawHtml "</summary>"
378+ <> rawHtml "</summary>"
320− <> blockQuote (subBuilder v)
379+ <> blockQuote (subBuilder v)
321− <> rawHtml "</details>")
380+ <> rawHtml "</details>"
381+ )
322 where
382 where
323 subBuilder = foldingStyleBuilder
383 subBuilder = foldingStyleBuilder
324
384
src/OpenAPI/Checker/Report/Html/Template.hs → src/Data/OpenApi/Compare/Report/Html/Template.hsrenamed+5−5
@@ -1,7 +1,7 @@
1{-# LANGUAGE TemplateHaskell #-}
1{-# LANGUAGE TemplateHaskell #-}
2
2
3−module OpenAPI.Checker.Report.Html.Template
3+module Data.OpenApi.Compare.Report.Html.Template
4− ( template
4+ ( template,
5 )
5 )
6where
6where
7
7
@@ -24,11 +24,11 @@ template =
24 \<meta charset=\"utf-8\">\
24 \<meta charset=\"utf-8\">\
25 \<title></title>\
25 \<title></title>\
26 \<meta name=\"description\" content=\"\">\
26 \<meta name=\"description\" content=\"\">\
27− \<meta name=\"generator\" content=\"OpenApi Diff\" />\
27+ \<meta name=\"generator\" content=\"CompaREST\" />\
28 \<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\" />\
28 \<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\" />\
29 \</head>\
29 \</head>\
30 \<body>\
30 \<body>\
31− \<header><h1>⛄ OpenApi Diff</h1></header>\
31+ \<header><h1>CompaREST</h1></header>\
32 \<main>\
32 \<main>\
33 \$body$\
33 \$body$\
34 \</main>\
34 \</main>\
@@ -36,4 +36,4 @@ template =
36 \</html>"
36 \</html>"
37
37
38awsmCss :: ByteString
38awsmCss :: ByteString
39−awsmCss = $(makeRelativeToProject "awsm-css/dist/awsm.min.css" >>= embedFile)
39+awsmCss = $(makeRelativeToProject "css/awsm.min.css" >>= embedFile)
src/OpenAPI/Checker/Report/Jet.hs → src/Data/OpenApi/Compare/Report/Jet.hsrenamed+6−6
@@ -1,8 +1,8 @@
1−module OpenAPI.Checker.Report.Jet
1+module Data.OpenApi.Compare.Report.Jet
2− ( ReportJet (..)
2+ ( ReportJet (..),
3− , ReportJet'
3+ ReportJet',
4− , ConstructReportJet (..)
4+ ConstructReportJet (..),
5− , ReportJetResult
5+ ReportJetResult,
6 )
6 )
7where
7where
8
8
@@ -23,7 +23,7 @@ import Text.Pandoc.Builder
23-- The term "jet" in this context was introduced in the Urbit project:
23-- The term "jet" in this context was introduced in the Urbit project:
24-- https://urbit.org/docs/vere/jetting/
24-- https://urbit.org/docs/vere/jetting/
25--
25--
26−-- The pattern fits well for simplifying 'Behaviour' tree paths.
26+-- The pattern fits well for simplifying 'Behavior' tree paths.
27class ConstructReportJet x f where
27class ConstructReportJet x f where
28 constructReportJet :: x -> ReportJetResult f (Maybe Inlines)
28 constructReportJet :: x -> ReportJetResult f (Maybe Inlines)
29
29
src/Data/OpenApi/Compare/Run.hsnew file+31
@@ -0,0 +1,31 @@
1+module Data.OpenApi.Compare.Run
2+ ( runChecker,
3+ runReport,
4+ module Data.OpenApi.Compare.Report,
5+ )
6+where
7+
8+import Data.HList
9+import Data.OpenApi (OpenApi)
10+import Data.OpenApi.Compare.Paths
11+import Data.OpenApi.Compare.Report
12+import Data.OpenApi.Compare.Subtree
13+import Data.OpenApi.Compare.Validate.OpenApi ()
14+import Text.Pandoc.Builder
15+
16+runChecker :: (OpenApi, OpenApi) -> CheckerOutput
17+runChecker (client, server) =
18+ CheckerOutput
19+ { forwardChanges = run client server
20+ , backwardChanges = run server client
21+ }
22+ where
23+ toPC p c =
24+ ProdCons
25+ { producer = traced (step ClientSchema) p
26+ , consumer = traced (step ServerSchema) c
27+ }
28+ run p c = either id mempty . runCompatFormula . checkCompatibility Root HNil $ toPC p c
29+
30+runReport :: ReportConfig -> (OpenApi, OpenApi) -> (Blocks, ReportStatus)
31+runReport cfg = generateReport cfg . segregateIssues . runChecker
src/OpenAPI/Checker/Subtree.hs → src/Data/OpenApi/Compare/Subtree.hsrenamed+132−125
@@ -1,51 +1,52 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Subtree
3+module Data.OpenApi.Compare.Subtree
4− ( Steppable (..)
4+ ( Steppable (..),
5− , Step (..)
5+ Step (..),
6− , TraceRoot
6+ TraceRoot,
7− , Trace
7+ Trace,
8− , Traced
8+ Traced,
9− , Traced'
9+ Traced',
10− , pattern Traced
10+ pattern Traced,
11− , traced
11+ traced,
12− , retraced
12+ retraced,
13− , stepTraced
13+ stepTraced,
14− , Subtree (..)
14+ Subtree (..),
15− , checkCompatibility
15+ checkCompatibility,
16− , checkSubstructure
16+ checkSubstructure,
17− , CompatM (..)
17+ CompatM (..),
18− , CompatFormula'
18+ CompatFormula',
19− , SemanticCompatFormula
19+ SemanticCompatFormula,
20− , ProdCons (..)
20+ ProdCons (..),
21+ orientProdCons,
21− , swapProdCons
22+ swapProdCons,
22− , runCompatFormula
23+ runCompatFormula,
23− , issueAt
24+ issueAt,
24− , anItem
25+ anItem,
25− , anIssue
26+ anIssue,
26− , invertIssueOrientation
27+ invertIssueOrientation,
27− , invertIssueOrientationP
28+ invertIssueOrientationP,
28− , embedFormula
29+ embedFormula,
29− , anyOfAt
30+ anyOfAt,
30− , clarifyIssue
31+ clarifyIssue,
31− , structuralIssue
32+ structuralIssue,
32
33
33 -- * Structural helpers
34 -- * Structural helpers
34− , structuralMaybe
35+ structuralMaybe,
35− , structuralMaybeWith
36+ structuralMaybeWith,
36− , structuralEq
37+ structuralEq,
37− , iohmStructural
38+ iohmStructural,
38− , iohmStructuralWith
39+ iohmStructuralWith,
39− , structuralList
40+ structuralList,
40
41
41 -- * Reexports
42 -- * Reexports
42− , (>>>)
43+ (>>>),
43− , (<<<)
44+ (<<<),
44− , extract
45+ extract,
45− , ask
46+ ask,
46− , local
47+ local,
47− , step
48+ step,
48− , Typeable
49+ Typeable,
49 )
50 )
50where
51where
51
52
@@ -59,13 +60,13 @@ import qualified Data.HashMap.Strict.InsOrd as IOHM
59import Data.Hashable
60import Data.Hashable
60import Data.Kind
61import Data.Kind
61import Data.OpenApi
62import Data.OpenApi
63+import Data.OpenApi.Compare.Behavior
64+import Data.OpenApi.Compare.Formula
65+import Data.OpenApi.Compare.Memo
66+import Data.OpenApi.Compare.Paths
67+import qualified Data.OpenApi.Compare.PathsPrefixTree as P
62import qualified Data.Set as S
68import qualified Data.Set as S
63import Data.Typeable
69import Data.Typeable
64−import OpenAPI.Checker.Behavior
65−import OpenAPI.Checker.Formula
66−import OpenAPI.Checker.Memo
67−import OpenAPI.Checker.Paths
68−import qualified OpenAPI.Checker.PathsPrefixTree as P
69
70
70class
71class
71 (Typeable Step, Typeable a, Typeable b, Ord (Step a b), Show (Step a b)) =>
72 (Typeable Step, Typeable a, Typeable b, Ord (Step a b), Show (Step a b)) =>
@@ -110,10 +111,14 @@ data ProdCons a = ProdCons
110 }
111 }
111 deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)
112 deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)
112
113
114+orientProdCons :: Orientation -> ProdCons x -> ProdCons x
115+orientProdCons Forward x = x
116+orientProdCons Backward (ProdCons p c) = ProdCons c p
117+
113−swapProdCons
118+swapProdCons ::
114− :: SwapEnvRoles xs
119+ SwapEnvRoles xs =>
115− => (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a)
120+ (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a) ->
116− -> (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a)
121+ (HList xs -> ProdCons x -> CompatFormula' q AnIssue r a)
117swapProdCons f e (ProdCons p c) =
122swapProdCons f e (ProdCons p c) =
118 invertIssueOrientation $
123 invertIssueOrientation $
119 f (swapEnvRoles e) (ProdCons c p)
124 f (swapEnvRoles e) (ProdCons c p)
@@ -148,8 +153,8 @@ instance Applicative ProdCons where
148 ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)
153 ProdCons fp fc <*> ProdCons xp xc = ProdCons (fp xp) (fc xc)
149
154
150newtype CompatM a = CompatM
155newtype CompatM a = CompatM
151− { unCompatM
156+ { unCompatM ::
152− :: StateT (MemoState VarRef) Identity a
157+ StateT (MemoState VarRef) Identity a
153 }
158 }
154 deriving newtype
159 deriving newtype
155 ( Functor
160 ( Functor
@@ -178,64 +183,64 @@ class (Typeable t, Issuable (SubtreeLevel t)) => Subtree (t :: Type) where
178 type CheckEnv t :: [Type]
183 type CheckEnv t :: [Type]
179 type SubtreeLevel t :: BehaviorLevel
184 type SubtreeLevel t :: BehaviorLevel
180
185
181− checkStructuralCompatibility
186+ checkStructuralCompatibility ::
182− :: HList (CheckEnv t)
187+ HList (CheckEnv t) ->
183− -> ProdCons (Traced t)
188+ ProdCons (Traced t) ->
184− -> StructuralCompatFormula ()
189+ StructuralCompatFormula ()
185
190
186− checkSemanticCompatibility
191+ checkSemanticCompatibility ::
187− :: HList (CheckEnv t)
192+ HList (CheckEnv t) ->
188− -> Behavior (SubtreeLevel t)
193+ Behavior (SubtreeLevel t) ->
189− -> ProdCons (Traced t)
194+ ProdCons (Traced t) ->
190− -> SemanticCompatFormula ()
195+ SemanticCompatFormula ()
191
196
192{-# WARNING checkStructuralCompatibility "You should not be calling this directly. Use 'checkSubstructure'" #-}
197{-# WARNING checkStructuralCompatibility "You should not be calling this directly. Use 'checkSubstructure'" #-}
193
198
194{-# WARNING checkSemanticCompatibility "You should not be calling this directly. Use 'checkCompatibility'" #-}
199{-# WARNING checkSemanticCompatibility "You should not be calling this directly. Use 'checkCompatibility'" #-}
195
200
196−checkCompatibility
201+checkCompatibility ::
197− :: forall t xs.
202+ forall t xs.
198− (ReassembleHList xs (CheckEnv t), Subtree t)
203+ (ReassembleHList xs (CheckEnv t), Subtree t) =>
199− => Behavior (SubtreeLevel t)
204+ Behavior (SubtreeLevel t) ->
200− -> HList xs
205+ HList xs ->
201− -> ProdCons (Traced t)
206+ ProdCons (Traced t) ->
202− -> SemanticCompatFormula ()
207+ SemanticCompatFormula ()
203checkCompatibility bhv e = memo bhv SemanticMemoKey $ \pc ->
208checkCompatibility bhv e = memo bhv SemanticMemoKey $ \pc ->
204 case runCompatFormula $ checkSubstructure e pc of
209 case runCompatFormula $ checkSubstructure e pc of
205 Left _ -> checkSemanticCompatibility (reassemble e) bhv pc
210 Left _ -> checkSemanticCompatibility (reassemble e) bhv pc
206 Right () -> pure ()
211 Right () -> pure ()
207{-# INLINE checkCompatibility #-}
212{-# INLINE checkCompatibility #-}
208
213
209−checkSubstructure
214+checkSubstructure ::
210− :: (ReassembleHList xs (CheckEnv t), Subtree t)
215+ (ReassembleHList xs (CheckEnv t), Subtree t) =>
211− => HList xs
216+ HList xs ->
212− -> ProdCons (Traced t)
217+ ProdCons (Traced t) ->
213− -> StructuralCompatFormula ()
218+ StructuralCompatFormula ()
214checkSubstructure e = memo Root StructuralMemoKey $ checkStructuralCompatibility (reassemble e)
219checkSubstructure e = memo Root StructuralMemoKey $ checkStructuralCompatibility (reassemble e)
215{-# INLINE checkSubstructure #-}
220{-# INLINE checkSubstructure #-}
216
221
217−structuralMaybe
222+structuralMaybe ::
218− :: (Subtree a, ReassembleHList xs (CheckEnv a))
223+ (Subtree a, ReassembleHList xs (CheckEnv a)) =>
219− => HList xs
224+ HList xs ->
220− -> ProdCons (Maybe (Traced a))
225+ ProdCons (Maybe (Traced a)) ->
221− -> StructuralCompatFormula ()
226+ StructuralCompatFormula ()
222structuralMaybe e = structuralMaybeWith (checkSubstructure e)
227structuralMaybe e = structuralMaybeWith (checkSubstructure e)
223{-# INLINE structuralMaybe #-}
228{-# INLINE structuralMaybe #-}
224
229
225−structuralMaybeWith
230+structuralMaybeWith ::
226− :: (ProdCons a -> StructuralCompatFormula ())
231+ (ProdCons a -> StructuralCompatFormula ()) ->
227− -> ProdCons (Maybe a)
232+ ProdCons (Maybe a) ->
228− -> StructuralCompatFormula ()
233+ StructuralCompatFormula ()
229structuralMaybeWith f (ProdCons (Just a) (Just b)) = f $ ProdCons a b
234structuralMaybeWith f (ProdCons (Just a) (Just b)) = f $ ProdCons a b
230structuralMaybeWith _ (ProdCons Nothing Nothing) = pure ()
235structuralMaybeWith _ (ProdCons Nothing Nothing) = pure ()
231structuralMaybeWith _ _ = structuralIssue
236structuralMaybeWith _ _ = structuralIssue
232{-# INLINE structuralMaybeWith #-}
237{-# INLINE structuralMaybeWith #-}
233
238
234−structuralList
239+structuralList ::
235− :: (Subtree a, ReassembleHList xs (CheckEnv a))
240+ (Subtree a, ReassembleHList xs (CheckEnv a)) =>
236− => HList xs
241+ HList xs ->
237− -> ProdCons [Traced a]
242+ ProdCons [Traced a] ->
238− -> StructuralCompatFormula ()
243+ StructuralCompatFormula ()
239structuralList _ (ProdCons [] []) = pure ()
244structuralList _ (ProdCons [] []) = pure ()
240structuralList e (ProdCons (a : aa) (b : bb)) = do
245structuralList e (ProdCons (a : aa) (b : bb)) = do
241 checkSubstructure e $ ProdCons a b
246 checkSubstructure e $ ProdCons a b
@@ -248,11 +253,11 @@ structuralEq :: (Eq a, Comonad w) => ProdCons (w a) -> StructuralCompatFormula (
248structuralEq (ProdCons a b) = if extract a == extract b then pure () else structuralIssue
253structuralEq (ProdCons a b) = if extract a == extract b then pure () else structuralIssue
249{-# INLINE structuralEq #-}
254{-# INLINE structuralEq #-}
250
255
251−iohmStructural
256+iohmStructural ::
252− :: (ReassembleHList (k ': xs) (CheckEnv v), Ord k, Subtree v, Hashable k, Typeable k, Show k)
257+ (ReassembleHList (k ': xs) (CheckEnv v), Ord k, Subtree v, Hashable k, Typeable k, Show k) =>
253− => HList xs
258+ HList xs ->
254− -> ProdCons (Traced (IOHM.InsOrdHashMap k v))
259+ ProdCons (Traced (IOHM.InsOrdHashMap k v)) ->
255− -> StructuralCompatFormula ()
260+ StructuralCompatFormula ()
256iohmStructural e =
261iohmStructural e =
257 iohmStructuralWith (\k -> checkSubstructure (k `HCons` e))
262 iohmStructuralWith (\k -> checkSubstructure (k `HCons` e))
258{-# INLINE iohmStructural #-}
263{-# INLINE iohmStructural #-}
@@ -261,25 +266,26 @@ instance (Typeable k, Typeable v, Ord k, Show k) => Steppable (IOHM.InsOrdHashMa
261 data Step (IOHM.InsOrdHashMap k v) v = InsOrdHashMapKeyStep k
266 data Step (IOHM.InsOrdHashMap k v) v = InsOrdHashMapKeyStep k
262 deriving stock (Eq, Ord, Show)
267 deriving stock (Eq, Ord, Show)
263
268
264−iohmStructuralWith
269+iohmStructuralWith ::
265− :: (Ord k, Hashable k, Typeable k, Typeable v, Show k)
270+ (Ord k, Hashable k, Typeable k, Typeable v, Show k) =>
266− => (k -> ProdCons (Traced v) -> StructuralCompatFormula ())
271+ (k -> ProdCons (Traced v) -> StructuralCompatFormula ()) ->
267− -> ProdCons (Traced (IOHM.InsOrdHashMap k v))
272+ ProdCons (Traced (IOHM.InsOrdHashMap k v)) ->
268− -> StructuralCompatFormula ()
273+ StructuralCompatFormula ()
269iohmStructuralWith f pc = do
274iohmStructuralWith f pc = do
270 let ProdCons pEKeys cEKeys = S.fromList . IOHM.keys . extract <$> pc
275 let ProdCons pEKeys cEKeys = S.fromList . IOHM.keys . extract <$> pc
271 if pEKeys == cEKeys
276 if pEKeys == cEKeys
272 then
277 then
273 for_
278 for_
274 pEKeys
279 pEKeys
275− (\eKey ->
280+ ( \eKey ->
276− f eKey $ stepTraced (InsOrdHashMapKeyStep eKey) . fmap (IOHM.lookupDefault (error "impossible") eKey) <$> pc)
281+ f eKey $ stepTraced (InsOrdHashMapKeyStep eKey) . fmap (IOHM.lookupDefault (error "impossible") eKey) <$> pc
282+ )
277 else structuralIssue
283 else structuralIssue
278{-# INLINE iohmStructuralWith #-}
284{-# INLINE iohmStructuralWith #-}
279
285
280−runCompatFormula
286+runCompatFormula ::
281− :: CompatFormula' q f r a
287+ CompatFormula' q f r a ->
282− -> Either (P.PathsPrefixTree q f r) a
288+ Either (P.PathsPrefixTree q f r) a
283runCompatFormula (Compose f) =
289runCompatFormula (Compose f) =
284 calculate . runIdentity . runMemo 0 . unCompatM $ f
290 calculate . runIdentity . runMemo 0 . unCompatM $ f
285{-# INLINE runCompatFormula #-}
291{-# INLINE runCompatFormula #-}
@@ -309,27 +315,27 @@ invertIssueOrientationP = P.map (\(AnIssue ori i) -> AnIssue (toggleOrientation
309structuralIssue :: StructuralCompatFormula a
315structuralIssue :: StructuralCompatFormula a
310structuralIssue = Compose $ pure $ anError $ AnItem Root Proxy
316structuralIssue = Compose $ pure $ anError $ AnItem Root Proxy
311
317
312−anyOfAt
318+anyOfAt ::
313− :: Issuable l
319+ Issuable l =>
314− => Paths q r l
320+ Paths q r l ->
315− -> Issue l
321+ Issue l ->
316− -> [CompatFormula' q AnIssue r a]
322+ [CompatFormula' q AnIssue r a] ->
317− -> CompatFormula' q AnIssue r a
323+ CompatFormula' q AnIssue r a
318anyOfAt _ _ [x] = x
324anyOfAt _ _ [x] = x
319anyOfAt xs issue fs =
325anyOfAt xs issue fs =
320 Compose $ (`eitherOf` AnItem xs (anIssue issue)) <$> traverse getCompose fs
326 Compose $ (`eitherOf` AnItem xs (anIssue issue)) <$> traverse getCompose fs
321
327
322-- | If the given formula contains any issues, add another issue on top. Otherwise succeed.
328-- | If the given formula contains any issues, add another issue on top. Otherwise succeed.
323−clarifyIssue
329+clarifyIssue ::
324− :: AnItem q AnIssue r
330+ AnItem q AnIssue r ->
325− -> CompatFormula' q AnIssue r a
331+ CompatFormula' q AnIssue r a ->
326− -> CompatFormula' q AnIssue r a
332+ CompatFormula' q AnIssue r a
327clarifyIssue item f =
333clarifyIssue item f =
328 Compose ((`eitherOf` item) <$> pure <$> getCompose f) *> f
334 Compose ((`eitherOf` item) <$> pure <$> getCompose f) *> f
329
335
330−fixpointKnot
336+fixpointKnot ::
331− :: MonadState (MemoState VarRef) m
337+ MonadState (MemoState VarRef) m =>
332− => KnotTier (FormulaF q f r ()) VarRef m
338+ KnotTier (FormulaF q f r ()) VarRef m
333fixpointKnot =
339fixpointKnot =
334 KnotTier
340 KnotTier
335 { onKnotFound = modifyMemoNonce succ
341 { onKnotFound = modifyMemoNonce succ
@@ -337,19 +343,20 @@ fixpointKnot =
337 , tieKnot = \i x -> pure $ maxFixpoint i x
343 , tieKnot = \i x -> pure $ maxFixpoint i x
338 }
344 }
339
345
340−memo
346+memo ::
341− :: (Typeable (l :: k), Typeable q, Typeable f, Typeable k, Typeable a)
347+ (Typeable (l :: k), Typeable q, Typeable f, Typeable k, Typeable a) =>
342− => Paths q r l
348+ Paths q r l ->
343− -> MemoKey
349+ MemoKey ->
344− -> (ProdCons (Traced a) -> CompatFormula' q f r ())
350+ (ProdCons (Traced a) -> CompatFormula' q f r ()) ->
345− -> (ProdCons (Traced a) -> CompatFormula' q f r ())
351+ (ProdCons (Traced a) -> CompatFormula' q f r ())
346memo bhv k f pc = Compose $ do
352memo bhv k f pc = Compose $ do
347 formula' <-
353 formula' <-
348 memoWithKnot
354 memoWithKnot
349 fixpointKnot
355 fixpointKnot
350− (do
356+ ( do
351− formula <- getCompose $ f pc
357+ formula <- getCompose $ f pc
352− pure $ mapErrors (P.takeSubtree bhv) formula)
358+ pure $ mapErrors (P.takeSubtree bhv) formula
359+ )
353 (k, ask <$> pc)
360 (k, ask <$> pc)
354 pure $ mapErrors (P.embed bhv) formula'
361 pure $ mapErrors (P.embed bhv) formula'
355
362
src/OpenAPI/Checker/Validate/Header.hs → src/Data/OpenApi/Compare/Validate/Header.hsrenamed+9−7
@@ -1,6 +1,6 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.Header
3+module Data.OpenApi.Compare.Validate.Header
4 (
4 (
5 )
5 )
6where
6where
@@ -9,10 +9,10 @@ import Data.Foldable
9import Data.Functor
9import Data.Functor
10import Data.Maybe
10import Data.Maybe
11import Data.OpenApi
11import Data.OpenApi
12−import OpenAPI.Checker.Behavior
12+import Data.OpenApi.Compare.Behavior
13−import OpenAPI.Checker.References ()
13+import Data.OpenApi.Compare.References ()
14−import OpenAPI.Checker.Subtree
14+import Data.OpenApi.Compare.Subtree
15−import OpenAPI.Checker.Validate.Schema ()
15+import Data.OpenApi.Compare.Validate.Schema ()
16import Text.Pandoc.Builder
16import Text.Pandoc.Builder
17
17
18instance Subtree Header where
18instance Subtree Header where
@@ -50,7 +50,9 @@ instance Issuable 'HeaderLevel where
50 | NonEmptyHeaderRequired
50 | NonEmptyHeaderRequired
51 | HeaderSchemaRequired
51 | HeaderSchemaRequired
52 deriving stock (Eq, Ord, Show)
52 deriving stock (Eq, Ord, Show)
53− issueIsUnsupported _ = False
53+ issueKind = \case
54+ HeaderSchemaRequired -> ProbablyIssue -- catch-all schema?
55+ _ -> CertainIssue
54 describeIssue Forward RequiredHeaderMissing = para "Header has become required."
56 describeIssue Forward RequiredHeaderMissing = para "Header has become required."
55 describeIssue Backward RequiredHeaderMissing = para "Header is no longer required."
57 describeIssue Backward RequiredHeaderMissing = para "Header is no longer required."
56 describeIssue Forward NonEmptyHeaderRequired = para "The header does not allow empty values anymore."
58 describeIssue Forward NonEmptyHeaderRequired = para "The header does not allow empty values anymore."
@@ -61,4 +63,4 @@ instance Behavable 'HeaderLevel 'SchemaLevel where
61 data Behave 'HeaderLevel 'SchemaLevel
63 data Behave 'HeaderLevel 'SchemaLevel
62 = InSchema
64 = InSchema
63 deriving stock (Eq, Ord, Show)
65 deriving stock (Eq, Ord, Show)
64− describeBehaviour InSchema = "JSON Schema"
66+ describeBehavior InSchema = "JSON Schema"
src/OpenAPI/Checker/Validate/Link.hs → src/Data/OpenApi/Compare/Validate/Link.hsrenamed+6−6
@@ -1,10 +1,10 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.Link () where
3+module Data.OpenApi.Compare.Validate.Link () where
4
4
5import Data.OpenApi
5import Data.OpenApi
6−import OpenAPI.Checker.Behavior
6+import Data.OpenApi.Compare.Behavior
7−import OpenAPI.Checker.Subtree
7+import Data.OpenApi.Compare.Subtree
8import Text.Pandoc.Builder
8import Text.Pandoc.Builder
9
9
10instance Subtree Link where
10instance Subtree Link where
@@ -17,6 +17,6 @@ instance Issuable 'LinkLevel where
17 data Issue 'LinkLevel
17 data Issue 'LinkLevel
18 = LinksUnsupported
18 = LinksUnsupported
19 deriving stock (Eq, Ord, Show)
19 deriving stock (Eq, Ord, Show)
20− issueIsUnsupported = \case
20+ issueKind = \case
21− LinksUnsupported -> True
21+ LinksUnsupported -> Unsupported
22− describeIssue _ LinksUnsupported = para "OpenApi Diff does not currently support Link Objects."
22+ describeIssue _ LinksUnsupported = para "CompaREST does not currently support Link Objects."
src/OpenAPI/Checker/Validate/MediaTypeObject.hs → src/Data/OpenApi/Compare/Validate/MediaTypeObject.hsrenamed+17−17
@@ -1,8 +1,8 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.MediaTypeObject
3+module Data.OpenApi.Compare.Validate.MediaTypeObject
4− ( Issue (..)
4+ ( Issue (..),
5− , Behave (..)
5+ Behave (..),
6 )
6 )
7where
7where
8
8
@@ -12,15 +12,15 @@ import Data.HList
12import Data.HashMap.Strict.InsOrd as IOHM
12import Data.HashMap.Strict.InsOrd as IOHM
13import Data.Map.Strict as M
13import Data.Map.Strict as M
14import Data.OpenApi
14import Data.OpenApi
15+import Data.OpenApi.Compare.Behavior
16+import Data.OpenApi.Compare.Subtree
17+import Data.OpenApi.Compare.Validate.Header ()
18+import Data.OpenApi.Compare.Validate.Products
19+import Data.OpenApi.Compare.Validate.Schema ()
15import Data.String
20import Data.String
16import Data.Text (Text)
21import Data.Text (Text)
17import qualified Data.Text as T
22import qualified Data.Text as T
18import Network.HTTP.Media (MediaType, mainType, subType)
23import Network.HTTP.Media (MediaType, mainType, subType)
19−import OpenAPI.Checker.Behavior
20−import OpenAPI.Checker.Subtree
21−import OpenAPI.Checker.Validate.Header ()
22−import OpenAPI.Checker.Validate.Products
23−import OpenAPI.Checker.Validate.Schema ()
24import Text.Pandoc.Builder
24import Text.Pandoc.Builder
25
25
26tracedSchema :: Traced MediaTypeObject -> Maybe (Traced (Referenced Schema))
26tracedSchema :: Traced MediaTypeObject -> Maybe (Traced (Referenced Schema))
@@ -38,20 +38,20 @@ instance Issuable 'PayloadLevel where
38 | MediaEncodingMissing Text
38 | MediaEncodingMissing Text
39 | EncodingNotSupported
39 | EncodingNotSupported
40 deriving stock (Eq, Ord, Show)
40 deriving stock (Eq, Ord, Show)
41− issueIsUnsupported = \case
41+ issueKind = \case
42− EncodingNotSupported -> True
42+ EncodingNotSupported -> Unsupported
43− _ -> False
43+ _ -> CertainIssue
44
44
45 describeIssue _ MediaTypeSchemaRequired = para "Media type expected, but was not specified."
45 describeIssue _ MediaTypeSchemaRequired = para "Media type expected, but was not specified."
46 describeIssue Forward (MediaEncodingMissing enc) = para $ "Media encoding " <> str enc <> " has been removed."
46 describeIssue Forward (MediaEncodingMissing enc) = para $ "Media encoding " <> str enc <> " has been removed."
47 describeIssue Backward (MediaEncodingMissing enc) = para $ "Media encoding " <> str enc <> " added."
47 describeIssue Backward (MediaEncodingMissing enc) = para $ "Media encoding " <> str enc <> " added."
48− describeIssue _ EncodingNotSupported = para "OpenApi Diff does not currently support media encodings other than JSON."
48+ describeIssue _ EncodingNotSupported = para "CompaREST does not currently support media encodings other than JSON."
49
49
50instance Behavable 'PayloadLevel 'SchemaLevel where
50instance Behavable 'PayloadLevel 'SchemaLevel where
51 data Behave 'PayloadLevel 'SchemaLevel
51 data Behave 'PayloadLevel 'SchemaLevel
52 = PayloadSchema
52 = PayloadSchema
53 deriving stock (Eq, Ord, Show)
53 deriving stock (Eq, Ord, Show)
54− describeBehaviour PayloadSchema = "JSON Schema"
54+ describeBehavior PayloadSchema = "JSON Schema"
55
55
56instance Subtree MediaTypeObject where
56instance Subtree MediaTypeObject where
57 type SubtreeLevel MediaTypeObject = 'PayloadLevel
57 type SubtreeLevel MediaTypeObject = 'PayloadLevel
@@ -119,7 +119,7 @@ instance Subtree Encoding where
119
119
120 -- FIXME: Support only JSON body for now. Then Encoding is checked only for
120 -- FIXME: Support only JSON body for now. Then Encoding is checked only for
121 -- multipart/form-url-encoded
121 -- multipart/form-url-encoded
122− -- https://github.com/typeable/openapi-diff/issues/54
122+ -- #54
123 checkSemanticCompatibility _env beh _pc =
123 checkSemanticCompatibility _env beh _pc =
124 issueAt beh EncodingNotSupported
124 issueAt beh EncodingNotSupported
125
125
@@ -143,7 +143,7 @@ instance Behavable 'OperationLevel 'ResponseLevel where
143 data Behave 'OperationLevel 'ResponseLevel
143 data Behave 'OperationLevel 'ResponseLevel
144 = WithStatusCode HttpStatusCode
144 = WithStatusCode HttpStatusCode
145 deriving stock (Eq, Ord, Show)
145 deriving stock (Eq, Ord, Show)
146− describeBehaviour (WithStatusCode c) = "Response code " <> (fromString . show $ c)
146+ describeBehavior (WithStatusCode c) = "Response code " <> (fromString . show $ c)
147
147
148instance Issuable 'OperationLevel where
148instance Issuable 'OperationLevel where
149 data Issue 'OperationLevel
149 data Issue 'OperationLevel
@@ -152,8 +152,8 @@ instance Issuable 'OperationLevel where
152 | PathFragmentNotMatched Int
152 | PathFragmentNotMatched Int
153 | NoRequestBody
153 | NoRequestBody
154 deriving stock (Eq, Ord, Show)
154 deriving stock (Eq, Ord, Show)
155− issueIsUnsupported = \case
155+ issueKind = \case
156− _ -> False
156+ _ -> CertainIssue
157 describeIssue Forward (ConsumerDoesntHaveResponseCode c) =
157 describeIssue Forward (ConsumerDoesntHaveResponseCode c) =
158 para $ "Response code " <> (str . T.pack . show $ c) <> " has been removed."
158 para $ "Response code " <> (str . T.pack . show $ c) <> " has been removed."
159 describeIssue Backward (ConsumerDoesntHaveResponseCode c) =
159 describeIssue Backward (ConsumerDoesntHaveResponseCode c) =
src/OpenAPI/Checker/Validate/OAuth2Flows.hs → src/Data/OpenApi/Compare/Validate/OAuth2Flows.hsrenamed+26−21
@@ -1,9 +1,9 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.OAuth2Flows
3+module Data.OpenApi.Compare.Validate.OAuth2Flows
4− ( Step (..)
4+ ( Step (..),
5− , Issue (..)
5+ Issue (..),
6− , Behave (..)
6+ Behave (..),
7 )
7 )
8where
8where
9
9
@@ -12,13 +12,13 @@ import Data.Function
12import Data.Functor
12import Data.Functor
13import qualified Data.HashMap.Strict.InsOrd as IOHM
13import qualified Data.HashMap.Strict.InsOrd as IOHM
14import Data.OpenApi
14import Data.OpenApi
15+import Data.OpenApi.Compare.Behavior
16+import Data.OpenApi.Compare.Orphans ()
17+import Data.OpenApi.Compare.Subtree
15import Data.Proxy
18import Data.Proxy
16import Data.Set (Set)
19import Data.Set (Set)
17import qualified Data.Set as S
20import qualified Data.Set as S
18import Data.Text (Text)
21import Data.Text (Text)
19−import OpenAPI.Checker.Behavior
20−import OpenAPI.Checker.Orphans ()
21−import OpenAPI.Checker.Subtree
22import Text.Pandoc.Builder
22import Text.Pandoc.Builder
23
23
24instance Subtree OAuth2Flows where
24instance Subtree OAuth2Flows where
@@ -26,11 +26,11 @@ instance Subtree OAuth2Flows where
26 type SubtreeLevel OAuth2Flows = 'SecuritySchemeLevel
26 type SubtreeLevel OAuth2Flows = 'SecuritySchemeLevel
27 checkStructuralCompatibility _ = structuralEq
27 checkStructuralCompatibility _ = structuralEq
28 checkSemanticCompatibility env bhv pc = do
28 checkSemanticCompatibility env bhv pc = do
29− let supportFlow
29+ let supportFlow ::
30− :: (Subtree t, SubtreeLevel t ~ SubtreeLevel OAuth2Flows, CheckEnv OAuth2Flows ~ CheckEnv t)
30+ (Subtree t, SubtreeLevel t ~ SubtreeLevel OAuth2Flows, CheckEnv OAuth2Flows ~ CheckEnv t) =>
31− => Issue 'SecuritySchemeLevel
31+ Issue 'SecuritySchemeLevel ->
32− -> ProdCons (Maybe (Traced t))
32+ ProdCons (Maybe (Traced t)) ->
33− -> SemanticCompatFormula ()
33+ SemanticCompatFormula ()
34 supportFlow i x = case x of
34 supportFlow i x = case x of
35 -- producer will not attempt this flow
35 -- producer will not attempt this flow
36 (ProdCons Nothing _) -> pure ()
36 (ProdCons Nothing _) -> pure ()
@@ -38,11 +38,11 @@ instance Subtree OAuth2Flows where
38 (ProdCons (Just _) Nothing) -> issueAt bhv i
38 (ProdCons (Just _) Nothing) -> issueAt bhv i
39 (ProdCons (Just p) (Just c)) ->
39 (ProdCons (Just p) (Just c)) ->
40 checkCompatibility bhv env $ ProdCons p c
40 checkCompatibility bhv env $ ProdCons p c
41− getFlow
41+ getFlow ::
42− :: Typeable x
42+ Typeable x =>
43− => (OAuth2Flows -> Maybe (OAuth2Flow x))
43+ (OAuth2Flows -> Maybe (OAuth2Flow x)) ->
44− -> Traced OAuth2Flows
44+ Traced OAuth2Flows ->
45− -> Maybe (Traced (OAuth2Flow x))
45+ Maybe (Traced (OAuth2Flow x))
46 getFlow f (Traced t a) = Traced (t >>> step (OAuth2FlowsFlow Proxy)) <$> f a
46 getFlow f (Traced t a) = Traced (t >>> step (OAuth2FlowsFlow Proxy)) <$> f a
47 supportFlow ConsumerDoesNotSupportImplicitFlow $ getFlow _oAuth2FlowsImplicit <$> pc
47 supportFlow ConsumerDoesNotSupportImplicitFlow $ getFlow _oAuth2FlowsImplicit <$> pc
48 supportFlow ConsumerDoesNotSupportPasswordFlow $ getFlow _oAuth2FlowsPassword <$> pc
48 supportFlow ConsumerDoesNotSupportPasswordFlow $ getFlow _oAuth2FlowsPassword <$> pc
@@ -103,7 +103,9 @@ instance Issuable 'SecurityRequirementLevel where
103 = SecurityRequirementNotMet
103 = SecurityRequirementNotMet
104 | UndefinedSecurityScheme Text
104 | UndefinedSecurityScheme Text
105 deriving stock (Eq, Ord, Show)
105 deriving stock (Eq, Ord, Show)
106− issueIsUnsupported _ = False
106+ issueKind = \case
107+ SecurityRequirementNotMet -> CertainIssue
108+ UndefinedSecurityScheme _ -> SchemaInvalid
107 describeIssue Forward SecurityRequirementNotMet = para "Security scheme has been removed."
109 describeIssue Forward SecurityRequirementNotMet = para "Security scheme has been removed."
108 describeIssue Backward SecurityRequirementNotMet = para "Security scheme was added."
110 describeIssue Backward SecurityRequirementNotMet = para "Security scheme was added."
109 describeIssue _ (UndefinedSecurityScheme k) = para $ "Security scheme " <> code k <> " is not defined."
111 describeIssue _ (UndefinedSecurityScheme k) = para $ "Security scheme " <> code k <> " is not defined."
@@ -129,11 +131,14 @@ instance Issuable 'SecuritySchemeLevel where
129 | CanNotHaveScopes
131 | CanNotHaveScopes
130 | ScopeNotDefined Text
132 | ScopeNotDefined Text
131 deriving stock (Eq, Ord, Show)
133 deriving stock (Eq, Ord, Show)
132− issueIsUnsupported _ = False
134+ issueKind = \case
135+ CanNotHaveScopes -> SchemaInvalid
136+ ScopeNotDefined _ -> SchemaInvalid
137+ _ -> CertainIssue
133 describeIssue _ RefreshUrlsDontMatch = para "Refresh URL changed."
138 describeIssue _ RefreshUrlsDontMatch = para "Refresh URL changed."
134 describeIssue _ (HttpSchemeTypesDontMatch _ _) = para "HTTP scheme type changed."
139 describeIssue _ (HttpSchemeTypesDontMatch _ _) = para "HTTP scheme type changed."
135 describeIssue _ (ApiKeyParamsDontMatch _ _) = para "API Key parameters changed."
140 describeIssue _ (ApiKeyParamsDontMatch _ _) = para "API Key parameters changed."
136− describeIssue _ (OpenIdConnectUrlsDontMatch _ _) = para "OpenaId Connect URL changed."
141+ describeIssue _ (OpenIdConnectUrlsDontMatch _ _) = para "OpenID Connect URL changed."
137 describeIssue _ (CustomHttpSchemesDontMatch e a) =
142 describeIssue _ (CustomHttpSchemesDontMatch e a) =
138 para $ "Changed HTTP scheme from " <> code e <> " to " <> code a <> "."
143 para $ "Changed HTTP scheme from " <> code e <> " to " <> code a <> "."
139 describeIssue Forward ConsumerDoesNotSupportImplicitFlow = para "Implicit flow support has been removed."
144 describeIssue Forward ConsumerDoesNotSupportImplicitFlow = para "Implicit flow support has been removed."
@@ -162,4 +167,4 @@ instance Behavable 'SecurityRequirementLevel 'SecuritySchemeLevel where
162 data Behave 'SecurityRequirementLevel 'SecuritySchemeLevel
167 data Behave 'SecurityRequirementLevel 'SecuritySchemeLevel
163 = SecuritySchemeStep Text
168 = SecuritySchemeStep Text
164 deriving stock (Eq, Ord, Show)
169 deriving stock (Eq, Ord, Show)
165− describeBehaviour (SecuritySchemeStep s) = text s
170+ describeBehavior (SecuritySchemeStep s) = text s
src/OpenAPI/Checker/Validate/OpenApi.hs → src/Data/OpenApi/Compare/Validate/OpenApi.hsrenamed+20−18
@@ -2,19 +2,19 @@
2-- Does not compiles otherwise
2-- Does not compiles otherwise
3{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
3{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
4
4
5−module OpenAPI.Checker.Validate.OpenApi
5+module Data.OpenApi.Compare.Validate.OpenApi
6− ( Behave (..)
6+ ( Behave (..),
7− , Issue (..)
7+ Issue (..),
8 )
8 )
9where
9where
10
10
11import Data.HList
11import Data.HList
12import qualified Data.HashMap.Strict.InsOrd as IOHM
12import qualified Data.HashMap.Strict.InsOrd as IOHM
13import Data.OpenApi
13import Data.OpenApi
14−import OpenAPI.Checker.Behavior
14+import Data.OpenApi.Compare.Behavior
15−import OpenAPI.Checker.Paths
15+import Data.OpenApi.Compare.Paths
16−import OpenAPI.Checker.Subtree
16+import Data.OpenApi.Compare.Subtree
17−import OpenAPI.Checker.Validate.Operation
17+import Data.OpenApi.Compare.Validate.Operation
18
18
19tracedPaths :: Traced OpenApi -> Traced ProcessedPathItems
19tracedPaths :: Traced OpenApi -> Traced ProcessedPathItems
20tracedPaths oa =
20tracedPaths oa =
@@ -38,7 +38,8 @@ tracedSecuritySchemes :: Traced OpenApi -> Traced (Definitions SecurityScheme)
38tracedSecuritySchemes oa =
38tracedSecuritySchemes oa =
39 traced
39 traced
40 (ask oa >>> step ComponentsSecurityScheme)
40 (ask oa >>> step ComponentsSecurityScheme)
41− (_componentsSecuritySchemes . _openApiComponents . extract $ oa)
41+ (unSecurityDefinitions . _componentsSecuritySchemes . _openApiComponents . extract $ oa)
42+ where unSecurityDefinitions (SecurityDefinitions ds) = ds
42
43
43tracedResponses :: Traced OpenApi -> Traced (Definitions Response)
44tracedResponses :: Traced OpenApi -> Traced (Definitions Response)
44tracedResponses oa =
45tracedResponses oa =
@@ -79,16 +80,17 @@ instance Subtree OpenApi where
79 checkSemanticCompatibility _ beh prodCons = do
80 checkSemanticCompatibility _ beh prodCons = do
80 checkCompatibility @ProcessedPathItems
81 checkCompatibility @ProcessedPathItems
81 beh
82 beh
82− ((tracedRequestBodies <$> prodCons)
83+ ( (tracedRequestBodies <$> prodCons)
83− `HCons` (tracedParameters <$> prodCons)
84+ `HCons` (tracedParameters <$> prodCons)
84− `HCons` (tracedSecuritySchemes <$> prodCons)
85+ `HCons` (tracedSecuritySchemes <$> prodCons)
85− `HCons` (tracedResponses <$> prodCons)
86+ `HCons` (tracedResponses <$> prodCons)
86− `HCons` (tracedHeaders <$> prodCons)
87+ `HCons` (tracedHeaders <$> prodCons)
87− `HCons` (tracedSchemas <$> prodCons)
88+ `HCons` (tracedSchemas <$> prodCons)
88− `HCons` (_openApiServers . extract <$> prodCons)
89+ `HCons` (_openApiServers . extract <$> prodCons)
89− `HCons` (tracedLinks <$> prodCons)
90+ `HCons` (tracedLinks <$> prodCons)
90− `HCons` (tracedCallbacks <$> prodCons)
91+ `HCons` (tracedCallbacks <$> prodCons)
91− `HCons` HNil)
92+ `HCons` HNil
93+ )
92 (tracedPaths <$> prodCons)
94 (tracedPaths <$> prodCons)
93
95
94instance Steppable OpenApi ProcessedPathItems where
96instance Steppable OpenApi ProcessedPathItems where
src/OpenAPI/Checker/Validate/Operation.hs → src/Data/OpenApi/Compare/Validate/Operation.hsrenamed+57−50
@@ -2,19 +2,19 @@
2{-# OPTIONS_GHC -Wno-orphans #-}
2{-# OPTIONS_GHC -Wno-orphans #-}
3{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
3{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
4
4
5−module OpenAPI.Checker.Validate.Operation
5+module Data.OpenApi.Compare.Validate.Operation
6 ( -- * Operation
6 ( -- * Operation
7− MatchedOperation (..)
7+ MatchedOperation (..),
8− , OperationMethod (..)
8+ OperationMethod (..),
9− , pathItemMethod
9+ pathItemMethod,
10
10
11 -- * ProcessedPathItem
11 -- * ProcessedPathItem
12− , ProcessedPathItem (..)
12+ ProcessedPathItem (..),
13− , ProcessedPathItems (..)
13+ ProcessedPathItems (..),
14− , processPathItems
14+ processPathItems,
15− , Step (..)
15+ Step (..),
16− , Behave (..)
16+ Behave (..),
17− , Issue (..)
17+ Issue (..),
18 )
18 )
19where
19where
20
20
@@ -29,21 +29,21 @@ import qualified Data.List as L
29import Data.Map.Strict as M
29import Data.Map.Strict as M
30import Data.Maybe
30import Data.Maybe
31import Data.OpenApi
31import Data.OpenApi
32+import Data.OpenApi.Compare.Behavior
33+import Data.OpenApi.Compare.References
34+import Data.OpenApi.Compare.Subtree
35+import Data.OpenApi.Compare.Validate.MediaTypeObject
36+import Data.OpenApi.Compare.Validate.OAuth2Flows
37+import Data.OpenApi.Compare.Validate.PathFragment
38+import Data.OpenApi.Compare.Validate.Products
39+import Data.OpenApi.Compare.Validate.RequestBody
40+import Data.OpenApi.Compare.Validate.Responses
41+import Data.OpenApi.Compare.Validate.SecurityRequirement ()
42+import Data.OpenApi.Compare.Validate.Server ()
43+import Data.OpenApi.Compare.Validate.Sums
32import Data.String
44import Data.String
33import Data.Text (Text)
45import Data.Text (Text)
34import qualified Data.Text as T
46import qualified Data.Text as T
35−import OpenAPI.Checker.Behavior
36−import OpenAPI.Checker.References
37−import OpenAPI.Checker.Subtree
38−import OpenAPI.Checker.Validate.MediaTypeObject
39−import OpenAPI.Checker.Validate.OAuth2Flows
40−import OpenAPI.Checker.Validate.PathFragment
41−import OpenAPI.Checker.Validate.Products
42−import OpenAPI.Checker.Validate.RequestBody
43−import OpenAPI.Checker.Validate.Responses
44−import OpenAPI.Checker.Validate.SecurityRequirement ()
45−import OpenAPI.Checker.Validate.Server ()
46−import OpenAPI.Checker.Validate.Sums
47import Text.Pandoc.Builder
47import Text.Pandoc.Builder
48
48
49data MatchedOperation = MatchedOperation
49data MatchedOperation = MatchedOperation
@@ -86,11 +86,12 @@ tracedCallbacks (Traced t oper) =
86 | (k, v) <- IOHM.toList . _operationCallbacks . operation $ oper
86 | (k, v) <- IOHM.toList . _operationCallbacks . operation $ oper
87 ]
87 ]
88
88
89−-- FIXME: https://github.com/typeable/openapi-diff/issues/28
89+-- FIXME: #28
90−getServers
90+getServers ::
91− :: [Server] -- ^ Servers from env
91+ -- | Servers from env
92+ [Server] ->
92− -> MatchedOperation
93+ MatchedOperation ->
93− -> [Server]
94+ [Server]
94getServers env oper =
95getServers env oper =
95 case _operationServers . operation $ oper of
96 case _operationServers . operation $ oper of
96 [] -> env
97 [] -> env
@@ -101,21 +102,21 @@ instance Behavable 'OperationLevel 'PathFragmentLevel where
101 = InParam Text
102 = InParam Text
102 | InFragment (PathFragment Text)
103 | InFragment (PathFragment Text)
103 deriving stock (Eq, Ord, Show)
104 deriving stock (Eq, Ord, Show)
104− describeBehaviour (InParam p) = "Parameter " <> text p
105+ describeBehavior (InParam p) = "Parameter " <> text p
105− describeBehaviour (InFragment (StaticPath p)) = "Static fragment " <> code p
106+ describeBehavior (InFragment (StaticPath p)) = "Static fragment " <> code p
106− describeBehaviour (InFragment (DynamicPath p)) = "Dynamic fragment " <> code p
107+ describeBehavior (InFragment (DynamicPath p)) = "Dynamic fragment " <> code p
107
108
108instance Behavable 'OperationLevel 'RequestLevel where
109instance Behavable 'OperationLevel 'RequestLevel where
109 data Behave 'OperationLevel 'RequestLevel
110 data Behave 'OperationLevel 'RequestLevel
110 = InRequest
111 = InRequest
111 deriving stock (Eq, Ord, Show)
112 deriving stock (Eq, Ord, Show)
112− describeBehaviour InRequest = "Request"
113+ describeBehavior InRequest = "Request"
113
114
114instance Behavable 'OperationLevel 'SecurityRequirementLevel where
115instance Behavable 'OperationLevel 'SecurityRequirementLevel where
115 data Behave 'OperationLevel 'SecurityRequirementLevel
116 data Behave 'OperationLevel 'SecurityRequirementLevel
116 = SecurityRequirementStep Int
117 = SecurityRequirementStep Int
117 deriving stock (Eq, Ord, Show)
118 deriving stock (Eq, Ord, Show)
118− describeBehaviour (SecurityRequirementStep i) =
119+ describeBehavior (SecurityRequirementStep i) =
119 "Security requirement " <> (text . T.pack . show $ i)
120 "Security requirement " <> (text . T.pack . show $ i)
120
121
121instance Subtree MatchedOperation where
122instance Subtree MatchedOperation where
@@ -241,7 +242,7 @@ instance Subtree MatchedOperation where
241 checkProducts beh (const NoRequestBody) (const check) elements
242 checkProducts beh (const NoRequestBody) (const check) elements
242 checkResponses =
243 checkResponses =
243 swapProdCons (checkCompatibility beh) env $ tracedResponses <$> prodCons
244 swapProdCons (checkCompatibility beh) env $ tracedResponses <$> prodCons
244− -- FIXME: https://github.com/typeable/openapi-diff/issues/27
245+ -- FIXME: #27
245 checkCallbacks = do
246 checkCallbacks = do
246 let ProdCons pCallbacks cCallbacks = tracedCallbacks <$> prodCons
247 let ProdCons pCallbacks cCallbacks = tracedCallbacks <$> prodCons
247 for_ pCallbacks $ \(k, pCallback) -> do
248 for_ pCallbacks $ \(k, pCallback) -> do
@@ -250,7 +251,7 @@ instance Subtree MatchedOperation where
250 cCallbacks <&> \(_, cCallback) -> do
251 cCallbacks <&> \(_, cCallback) -> do
251 swapProdCons (checkCompatibility beh') env $ ProdCons pCallback cCallback
252 swapProdCons (checkCompatibility beh') env $ ProdCons pCallback cCallback
252 pure ()
253 pure ()
253− -- FIXME: https://github.com/typeable/openapi-diff/issues/28
254+ -- FIXME: #28
254 checkOperationSecurity = do
255 checkOperationSecurity = do
255 let ProdCons pSecs cSecs = tracedSecurity <$> prodCons
256 let ProdCons pSecs cSecs = tracedSecurity <$> prodCons
256 for_ pSecs $ \(i, pSec) -> do
257 for_ pSecs $ \(i, pSec) -> do
@@ -335,7 +336,12 @@ instance Issuable 'APILevel where
335 | AllPathsFailed FilePath
336 | AllPathsFailed FilePath
336 -- When several paths match given but all checks failed
337 -- When several paths match given but all checks failed
337 deriving stock (Eq, Ord, Show)
338 deriving stock (Eq, Ord, Show)
338− issueIsUnsupported _ = False
339+ issueKind = \case
340+ _ -> CertainIssue
341+ relatedIssues =
342+ (==) `withClass` \case
343+ NoPathsMatched fp -> Just fp
344+ AllPathsFailed fp -> Just fp
339 describeIssue Forward (NoPathsMatched p) = para $ "The path " <> (code . T.pack) p <> " has been removed."
345 describeIssue Forward (NoPathsMatched p) = para $ "The path " <> (code . T.pack) p <> " has been removed."
340 describeIssue Backward (NoPathsMatched p) = para $ "The path " <> (code . T.pack) p <> " has been added."
346 describeIssue Backward (NoPathsMatched p) = para $ "The path " <> (code . T.pack) p <> " has been added."
341 describeIssue Forward (AllPathsFailed p) = para $ "The path " <> (code . T.pack) p <> " has been removed."
347 describeIssue Forward (AllPathsFailed p) = para $ "The path " <> (code . T.pack) p <> " has been removed."
@@ -346,7 +352,7 @@ instance Behavable 'APILevel 'PathLevel where
346 = AtPath FilePath
352 = AtPath FilePath
347 deriving stock (Eq, Ord, Show)
353 deriving stock (Eq, Ord, Show)
348
354
349− describeBehaviour (AtPath p) = str (T.pack p)
355+ describeBehavior (AtPath p) = str (T.pack p)
350
356
351instance Subtree ProcessedPathItems where
357instance Subtree ProcessedPathItems where
352 type SubtreeLevel ProcessedPathItems = 'APILevel
358 type SubtreeLevel ProcessedPathItems = 'APILevel
@@ -430,17 +436,18 @@ tracedFragments mpi =
430 | (i, x) <- L.zip [0 ..] $ pathFragments $ extract mpi
436 | (i, x) <- L.zip [0 ..] $ pathFragments $ extract mpi
431 ]
437 ]
432
438
433−tracedMethod
439+tracedMethod ::
434− :: OperationMethod
440+ OperationMethod ->
435− -> Traced MatchedPathItem
441+ Traced MatchedPathItem ->
436− -> Maybe (Traced' MatchedOperation Operation)
442+ Maybe (Traced' MatchedOperation Operation)
437tracedMethod s mpi = env (ask mpi >>> step (OperationMethodStep s)) <$> (pathItemMethod s . pathItem . extract $ mpi)
443tracedMethod s mpi = env (ask mpi >>> step (OperationMethodStep s)) <$> (pathItemMethod s . pathItem . extract $ mpi)
438
444
439instance Issuable 'PathLevel where
445instance Issuable 'PathLevel where
440 data Issue 'PathLevel
446 data Issue 'PathLevel
441 = OperationMissing OperationMethod
447 = OperationMissing OperationMethod
442 deriving stock (Eq, Ord, Show)
448 deriving stock (Eq, Ord, Show)
443− issueIsUnsupported _ = False
449+ issueKind = \case
450+ OperationMissing _ -> CertainIssue
444 describeIssue Forward (OperationMissing op) = para $ "Method " <> strong (showMethod op) <> " has been removed."
451 describeIssue Forward (OperationMissing op) = para $ "Method " <> strong (showMethod op) <> " has been removed."
445 describeIssue Backward (OperationMissing op) = para $ "Method " <> strong (showMethod op) <> " has been added."
452 describeIssue Backward (OperationMissing op) = para $ "Method " <> strong (showMethod op) <> " has been added."
446
453
@@ -449,7 +456,7 @@ instance Behavable 'PathLevel 'OperationLevel where
449 = InOperation OperationMethod
456 = InOperation OperationMethod
450 deriving stock (Eq, Ord, Show)
457 deriving stock (Eq, Ord, Show)
451
458
452− describeBehaviour (InOperation method) = showMethod method
459+ describeBehavior (InOperation method) = showMethod method
453
460
454showMethod :: IsString s => OperationMethod -> s
461showMethod :: IsString s => OperationMethod -> s
455showMethod = \case
462showMethod = \case
@@ -480,10 +487,10 @@ instance Subtree MatchedPathItem where
480 checkSemanticCompatibility env beh prodCons = do
487 checkSemanticCompatibility env beh prodCons = do
481 let paramDefs = getH @(ProdCons (Traced (Definitions Param))) env
488 let paramDefs = getH @(ProdCons (Traced (Definitions Param))) env
482 pathTracedParams = getPathParams <$> paramDefs <*> prodCons
489 pathTracedParams = getPathParams <$> paramDefs <*> prodCons
483− getPathParams
490+ getPathParams ::
484− :: Traced (Definitions Param)
491+ Traced (Definitions Param) ->
485− -> Traced MatchedPathItem
492+ Traced MatchedPathItem ->
486− -> [Traced Param]
493+ [Traced Param]
487 getPathParams defs mpi = do
494 getPathParams defs mpi = do
488 paramRef <- tracedMatchedPathItemParameters mpi
495 paramRef <- tracedMatchedPathItemParameters mpi
489 pure $ dereference defs paramRef
496 pure $ dereference defs paramRef
@@ -557,9 +564,9 @@ instance Issuable 'CallbackLevel where
557 data Issue 'CallbackLevel
564 data Issue 'CallbackLevel
558 = CallbacksUnsupported
565 = CallbacksUnsupported
559 deriving stock (Eq, Ord, Show)
566 deriving stock (Eq, Ord, Show)
560− issueIsUnsupported = \case
567+ issueKind = \case
561− CallbacksUnsupported -> True
568+ CallbacksUnsupported -> Unsupported
562− describeIssue _ CallbacksUnsupported = para "OpenApi Diff does not currently support callbacks."
569+ describeIssue _ CallbacksUnsupported = para "CompaREST does not currently support callbacks."
563
570
564tracedCallbackPathItems :: Traced Callback -> Traced ProcessedPathItems
571tracedCallbackPathItems :: Traced Callback -> Traced ProcessedPathItems
565tracedCallbackPathItems (Traced t (Callback x)) =
572tracedCallbackPathItems (Traced t (Callback x)) =
@@ -573,4 +580,4 @@ instance Behavable 'OperationLevel 'CallbackLevel where
573 data Behave 'OperationLevel 'CallbackLevel = OperationCallback Text
580 data Behave 'OperationLevel 'CallbackLevel = OperationCallback Text
574 deriving stock (Eq, Ord, Show)
581 deriving stock (Eq, Ord, Show)
575
582
576− describeBehaviour (OperationCallback key) = "Operation " <> code key
583+ describeBehavior (OperationCallback key) = "Operation " <> code key
src/OpenAPI/Checker/Validate/Param.hs → src/Data/OpenApi/Compare/Validate/Param.hsrenamed+20−15
@@ -1,9 +1,9 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
2{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
3
3
4−module OpenAPI.Checker.Validate.Param
4+module Data.OpenApi.Compare.Validate.Param
5− ( Behave (..)
5+ ( Behave (..),
6− , Issue (..)
6+ Issue (..),
7 )
7 )
8where
8where
9
9
@@ -11,11 +11,11 @@ import Control.Monad
11import Data.Functor
11import Data.Functor
12import Data.Maybe
12import Data.Maybe
13import Data.OpenApi
13import Data.OpenApi
14+import Data.OpenApi.Compare.Behavior
15+import Data.OpenApi.Compare.Orphans ()
16+import Data.OpenApi.Compare.Subtree
17+import Data.OpenApi.Compare.Validate.Schema ()
14import Data.Text as T
18import Data.Text as T
15−import OpenAPI.Checker.Behavior
16−import OpenAPI.Checker.Orphans ()
17−import OpenAPI.Checker.Subtree
18−import OpenAPI.Checker.Validate.Schema ()
19import Text.Pandoc.Builder
19import Text.Pandoc.Builder
20
20
21-- | The type is normalized encoding style of the parameter. If two encoding
21-- | The type is normalized encoding style of the parameter. If two encoding
@@ -66,9 +66,11 @@ instance Issuable 'PathFragmentLevel where
66 ParamStyleMismatch
66 ParamStyleMismatch
67 | -- | One of schemas not presented
67 | -- | One of schemas not presented
68 ParamSchemaMismatch
68 ParamSchemaMismatch
69− | PathFragmentsDontMatch Text Text
69+ | PathFragmentsDontMatch (ProdCons Text)
70 deriving stock (Eq, Ord, Show)
70 deriving stock (Eq, Ord, Show)
71− issueIsUnsupported _ = False
71+ issueKind = \case
72+ ParamSchemaMismatch -> ProbablyIssue -- the schema could be catch-all (?)
73+ _ -> CertainIssue
72 describeIssue _ ParamNameMismatch = para "The path fragments don't match."
74 describeIssue _ ParamNameMismatch = para "The path fragments don't match."
73 describeIssue Forward ParamEmptinessIncompatible = para "The parameter can no longer be empty."
75 describeIssue Forward ParamEmptinessIncompatible = para "The parameter can no longer be empty."
74 describeIssue Backward ParamEmptinessIncompatible = para "The parameter can now be empty."
76 describeIssue Backward ParamEmptinessIncompatible = para "The parameter can now be empty."
@@ -77,14 +79,15 @@ instance Issuable 'PathFragmentLevel where
77 describeIssue _ ParamPlaceIncompatible = para "Parameters in incompatible locations."
79 describeIssue _ ParamPlaceIncompatible = para "Parameters in incompatible locations."
78 describeIssue _ ParamStyleMismatch = para "Different parameter styles (encodings)."
80 describeIssue _ ParamStyleMismatch = para "Different parameter styles (encodings)."
79 describeIssue _ ParamSchemaMismatch = para "Expected a schema, but didn't find one."
81 describeIssue _ ParamSchemaMismatch = para "Expected a schema, but didn't find one."
82+ describeIssue ori (PathFragmentsDontMatch (orientProdCons ori -> ProdCons e a)) =
80− describeIssue _ (PathFragmentsDontMatch e a) = para $ "Parameter changed from " <> code e <> " to " <> code a <> "."
83+ para $ "Parameter changed from " <> code e <> " to " <> code a <> "."
81
84
82instance Behavable 'PathFragmentLevel 'SchemaLevel where
85instance Behavable 'PathFragmentLevel 'SchemaLevel where
83 data Behave 'PathFragmentLevel 'SchemaLevel
86 data Behave 'PathFragmentLevel 'SchemaLevel
84 = InParamSchema
87 = InParamSchema
85 deriving stock (Eq, Ord, Show)
88 deriving stock (Eq, Ord, Show)
86
89
87− describeBehaviour InParamSchema = "JSON Schema"
90+ describeBehavior InParamSchema = "JSON Schema"
88
91
89instance Subtree Param where
92instance Subtree Param where
90 type SubtreeLevel Param = 'PathFragmentLevel
93 type SubtreeLevel Param = 'PathFragmentLevel
@@ -103,15 +106,17 @@ instance Subtree Param where
103 when (_paramName (extract p) /= _paramName (extract c)) $
106 when (_paramName (extract p) /= _paramName (extract c)) $
104 issueAt beh ParamNameMismatch
107 issueAt beh ParamNameMismatch
105 when
108 when
106− ((fromMaybe False . _paramRequired . extract $ c)
109+ ( (fromMaybe False . _paramRequired . extract $ c)
107− && not (fromMaybe False . _paramRequired . extract $ p))
110+ && not (fromMaybe False . _paramRequired . extract $ p)
111+ )
108 $ issueAt beh ParamRequired
112 $ issueAt beh ParamRequired
109 case (_paramIn . extract $ p, _paramIn . extract $ c) of
113 case (_paramIn . extract $ p, _paramIn . extract $ c) of
110 (ParamQuery, ParamQuery) -> do
114 (ParamQuery, ParamQuery) -> do
111 -- Emptiness is only for query params
115 -- Emptiness is only for query params
112 when
116 when
113− ((fromMaybe False . _paramAllowEmptyValue . extract $ p)
117+ ( (fromMaybe False . _paramAllowEmptyValue . extract $ p)
114− && not (fromMaybe False . _paramAllowEmptyValue . extract $ c))
118+ && not (fromMaybe False . _paramAllowEmptyValue . extract $ c)
119+ )
115 $ issueAt beh ParamEmptinessIncompatible
120 $ issueAt beh ParamEmptinessIncompatible
116 (a, b) | a == b -> pure ()
121 (a, b) | a == b -> pure ()
117 _ -> issueAt beh ParamPlaceIncompatible
122 _ -> issueAt beh ParamPlaceIncompatible
src/OpenAPI/Checker/Validate/PathFragment.hs → src/Data/OpenApi/Compare/Validate/PathFragment.hsrenamed+9−10
@@ -1,21 +1,20 @@
1−module OpenAPI.Checker.Validate.PathFragment
1+module Data.OpenApi.Compare.Validate.PathFragment
2− ( parsePath
2+ ( parsePath,
3− , PathFragment (..)
3+ PathFragment (..),
4− , PathFragmentParam
4+ PathFragmentParam,
5 )
5 )
6where
6where
7
7
8import qualified Data.Aeson as A
8import qualified Data.Aeson as A
9import Data.OpenApi
9import Data.OpenApi
10+import Data.OpenApi.Compare.Behavior
11+import Data.OpenApi.Compare.Subtree
12+import Data.OpenApi.Compare.Validate.Param
10import Data.Text (Text)
13import Data.Text (Text)
11import qualified Data.Text as T
14import qualified Data.Text as T
12−import Data.Typeable
13−import OpenAPI.Checker.Behavior
14−import OpenAPI.Checker.Subtree
15−import OpenAPI.Checker.Validate.Param
16
15
17-- TODO: templates can be only part of the PathFragment. Currently only supports templates as full PathFragment.
16-- TODO: templates can be only part of the PathFragment. Currently only supports templates as full PathFragment.
18−-- https://github.com/typeable/openapi-diff/issues/23
17+-- #23
19parsePath :: FilePath -> [PathFragment Text]
18parsePath :: FilePath -> [PathFragment Text]
20parsePath = fmap partition . T.splitOn "/" . T.pack
19parsePath = fmap partition . T.splitOn "/" . T.pack
21 where
20 where
@@ -74,6 +73,6 @@ instance Subtree PathFragmentParam where
74 checkSemanticCompatibility _ beh (ProdCons (extract -> StaticPath x) (extract -> StaticPath y)) =
73 checkSemanticCompatibility _ beh (ProdCons (extract -> StaticPath x) (extract -> StaticPath y)) =
75 if x == y
74 if x == y
76 then pure ()
75 then pure ()
77− else issueAt beh (PathFragmentsDontMatch x y)
76+ else issueAt beh (PathFragmentsDontMatch (ProdCons x y))
78 checkSemanticCompatibility env beh prodCons = do
77 checkSemanticCompatibility env beh prodCons = do
79 checkCompatibility beh env (tracedPathFragmentParam <$> prodCons)
78 checkCompatibility beh env (tracedPathFragmentParam <$> prodCons)
src/Data/OpenApi/Compare/Validate/Products.hsnew file+48
@@ -0,0 +1,48 @@
1+-- | Checks product-like entities. The key is some identificator for the product
2+--element. Each element may be required or optional.
3+--
4+--One example of product is request parameters. There are optional and required
5+--parameters. The client and server have possibly different set of
6+--parameters. What we must check is if server requires some request parameter,
7+--then this parameter must be presented by client and their schemas must match.
8+--
9+--So when we checking products we are checking from the server's (consumer)
10+--perspective, ensuring that all parameters are provided by the client (producer)
11+--and their schemas match.
12+--
13+--This module abstracts this logic for arbitrary elements
14+module Data.OpenApi.Compare.Validate.Products
15+ ( checkProducts,
16+ ProductLike (..),
17+ )
18+where
19+
20+import Data.Foldable
21+import Data.Map.Strict (Map)
22+import qualified Data.Map.Strict as M
23+import Data.OpenApi.Compare.Behavior
24+import Data.OpenApi.Compare.Paths
25+import Data.OpenApi.Compare.Subtree
26+
27+-- | Some entity which is product-like
28+data ProductLike a = ProductLike
29+ { productValue :: a
30+ , required :: Bool
31+ }
32+
33+checkProducts ::
34+ (Ord k, Issuable l) =>
35+ Paths q r l ->
36+ -- | No required element found
37+ (k -> Issue l) ->
38+ (k -> ProdCons t -> CompatFormula' q AnIssue r ()) ->
39+ ProdCons (Map k (ProductLike t)) ->
40+ CompatFormula' q AnIssue r ()
41+checkProducts xs noElt check (ProdCons p c) = for_ (M.toList c) $ \(key, consElt) ->
42+ case M.lookup key p of
43+ Nothing -> case required consElt of
44+ True -> issueAt xs $ noElt key
45+ False -> pure ()
46+ Just prodElt -> do
47+ let elts = ProdCons prodElt consElt
48+ check key (productValue <$> elts)
src/OpenAPI/Checker/Validate/RequestBody.hs → src/Data/OpenApi/Compare/Validate/RequestBody.hsrenamed+10−9
@@ -1,8 +1,8 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.RequestBody
3+module Data.OpenApi.Compare.Validate.RequestBody
4− ( Issue (..)
4+ ( Issue (..),
5− , Behave (..)
5+ Behave (..),
6 )
6 )
7where
7where
8
8
@@ -11,12 +11,12 @@ import Data.HashMap.Strict.InsOrd as IOHM
11import Data.Map.Strict as M
11import Data.Map.Strict as M
12import Data.Maybe
12import Data.Maybe
13import Data.OpenApi
13import Data.OpenApi
14+import Data.OpenApi.Compare.Behavior
15+import Data.OpenApi.Compare.Subtree
16+import Data.OpenApi.Compare.Validate.MediaTypeObject
17+import Data.OpenApi.Compare.Validate.Sums
14import qualified Data.Text as T
18import qualified Data.Text as T
15import Network.HTTP.Media (MediaType)
19import Network.HTTP.Media (MediaType)
16−import OpenAPI.Checker.Behavior
17−import OpenAPI.Checker.Subtree
18−import OpenAPI.Checker.Validate.MediaTypeObject
19−import OpenAPI.Checker.Validate.Sums
20import Text.Pandoc.Builder
20import Text.Pandoc.Builder
21
21
22-- TODO: Use RequestMediaTypeObjectMapping
22-- TODO: Use RequestMediaTypeObjectMapping
@@ -30,7 +30,8 @@ instance Issuable 'RequestLevel where
30 = RequestBodyRequired
30 = RequestBodyRequired
31 | RequestMediaTypeNotFound MediaType
31 | RequestMediaTypeNotFound MediaType
32 deriving stock (Eq, Ord, Show)
32 deriving stock (Eq, Ord, Show)
33− issueIsUnsupported _ = False
33+ issueKind = \case
34+ _ -> CertainIssue
34 describeIssue Forward RequestBodyRequired =
35 describeIssue Forward RequestBodyRequired =
35 para "Request body has become required."
36 para "Request body has become required."
36 describeIssue Backward RequestBodyRequired =
37 describeIssue Backward RequestBodyRequired =
@@ -44,7 +45,7 @@ instance Behavable 'RequestLevel 'PayloadLevel where
44 data Behave 'RequestLevel 'PayloadLevel
45 data Behave 'RequestLevel 'PayloadLevel
45 = InPayload
46 = InPayload
46 deriving stock (Eq, Ord, Show)
47 deriving stock (Eq, Ord, Show)
47− describeBehaviour InPayload = "Payload"
48+ describeBehavior InPayload = "Payload"
48
49
49instance Subtree RequestBody where
50instance Subtree RequestBody where
50 type SubtreeLevel RequestBody = 'RequestLevel
51 type SubtreeLevel RequestBody = 'RequestLevel
src/OpenAPI/Checker/Validate/Responses.hs → src/Data/OpenApi/Compare/Validate/Responses.hsrenamed+15−14
@@ -1,8 +1,8 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
2{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
3
3
4−module OpenAPI.Checker.Validate.Responses
4+module Data.OpenApi.Compare.Validate.Responses
5− ( Behave (..)
5+ ( Behave (..),
6 )
6 )
7where
7where
8
8
@@ -11,17 +11,17 @@ import Data.HashMap.Strict.InsOrd as IOHM
11import Data.Map.Strict as M
11import Data.Map.Strict as M
12import Data.Maybe
12import Data.Maybe
13import Data.OpenApi
13import Data.OpenApi
14+import Data.OpenApi.Compare.Behavior
15+import Data.OpenApi.Compare.References
16+import Data.OpenApi.Compare.Subtree
17+import Data.OpenApi.Compare.Validate.Header ()
18+import Data.OpenApi.Compare.Validate.Link ()
19+import Data.OpenApi.Compare.Validate.MediaTypeObject
20+import Data.OpenApi.Compare.Validate.Products
21+import Data.OpenApi.Compare.Validate.Schema ()
22+import Data.OpenApi.Compare.Validate.Sums
14import qualified Data.Text as T
23import qualified Data.Text as T
15import Network.HTTP.Media (MediaType)
24import Network.HTTP.Media (MediaType)
16−import OpenAPI.Checker.Behavior
17−import OpenAPI.Checker.References
18−import OpenAPI.Checker.Subtree
19−import OpenAPI.Checker.Validate.Header ()
20−import OpenAPI.Checker.Validate.Link ()
21−import OpenAPI.Checker.Validate.MediaTypeObject
22−import OpenAPI.Checker.Validate.Products
23−import OpenAPI.Checker.Validate.Schema ()
24−import OpenAPI.Checker.Validate.Sums
25import Text.Pandoc.Builder
25import Text.Pandoc.Builder
26
26
27tracedResponses :: Traced Responses -> IOHM.InsOrdHashMap HttpStatusCode (Traced (Referenced Response))
27tracedResponses :: Traced Responses -> IOHM.InsOrdHashMap HttpStatusCode (Traced (Referenced Response))
@@ -71,7 +71,8 @@ instance Issuable 'ResponseLevel where
71 = ConsumerDoesntHaveMediaType MediaType
71 = ConsumerDoesntHaveMediaType MediaType
72 | ProducerDoesntHaveResponseHeader HeaderName
72 | ProducerDoesntHaveResponseHeader HeaderName
73 deriving stock (Eq, Ord, Show)
73 deriving stock (Eq, Ord, Show)
74− issueIsUnsupported _ = False
74+ issueKind = \case
75+ _ -> CertainIssue
75 describeIssue Forward (ConsumerDoesntHaveMediaType t) =
76 describeIssue Forward (ConsumerDoesntHaveMediaType t) =
76 para $ "Media type was removed: " <> (code . T.pack . show $ t) <> "."
77 para $ "Media type was removed: " <> (code . T.pack . show $ t) <> "."
77 describeIssue Backward (ConsumerDoesntHaveMediaType t) =
78 describeIssue Backward (ConsumerDoesntHaveMediaType t) =
@@ -86,14 +87,14 @@ instance Behavable 'ResponseLevel 'PayloadLevel where
86 = ResponsePayload
87 = ResponsePayload
87 deriving stock (Eq, Ord, Show)
88 deriving stock (Eq, Ord, Show)
88
89
89− describeBehaviour ResponsePayload = "Payload"
90+ describeBehavior ResponsePayload = "Payload"
90
91
91instance Behavable 'ResponseLevel 'HeaderLevel where
92instance Behavable 'ResponseLevel 'HeaderLevel where
92 data Behave 'ResponseLevel 'HeaderLevel
93 data Behave 'ResponseLevel 'HeaderLevel
93 = InHeader HeaderName
94 = InHeader HeaderName
94 deriving stock (Eq, Ord, Show)
95 deriving stock (Eq, Ord, Show)
95
96
96− describeBehaviour (InHeader name) = "Header " <> code name
97+ describeBehavior (InHeader name) = "Header " <> code name
97
98
98instance Subtree Response where
99instance Subtree Response where
99 type SubtreeLevel Response = 'ResponseLevel
100 type SubtreeLevel Response = 'ResponseLevel
src/Data/OpenApi/Compare/Validate/Schema.hsnew file+398
@@ -0,0 +1,398 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Data.OpenApi.Compare.Validate.Schema
4+ (
5+ )
6+where
7+
8+import Control.Monad (when,unless)
9+import qualified Data.Aeson as A
10+import Data.Coerce
11+import Data.Foldable (for_, toList)
12+import Data.Functor
13+import Data.HList
14+import Data.List (genericIndex, genericLength, group)
15+import qualified Data.List.NonEmpty as NE
16+import qualified Data.Map as M
17+import Data.Maybe
18+import Data.OpenApi
19+import Data.OpenApi.Compare.Behavior
20+import Data.OpenApi.Compare.Paths
21+import qualified Data.OpenApi.Compare.PathsPrefixTree as P
22+import Data.OpenApi.Compare.Subtree
23+import Data.OpenApi.Compare.Validate.Schema.DNF
24+import Data.OpenApi.Compare.Validate.Schema.Issues
25+import Data.OpenApi.Compare.Validate.Schema.JsonFormula
26+import Data.OpenApi.Compare.Validate.Schema.Partition
27+import Data.OpenApi.Compare.Validate.Schema.Process
28+import Data.OpenApi.Compare.Validate.Schema.Traced
29+import Data.OpenApi.Compare.Validate.Schema.TypedJson
30+import Data.Ord
31+import Data.Ratio
32+import Data.Semigroup
33+import qualified Data.Set as S
34+import Data.Text (Text)
35+
36+checkFormulas ::
37+ (ReassembleHList xs (CheckEnv (Referenced Schema))) =>
38+ HList xs ->
39+ Behavior 'SchemaLevel ->
40+ ProdCons (Trace Schema) ->
41+ ProdCons (Traced (Definitions Schema)) ->
42+ ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel) ->
43+ SemanticCompatFormula ()
44+checkFormulas env beh trs defs (ProdCons (fp, ep) (fc, ec)) =
45+ case P.toList ep ++ P.toList ec of
46+ issues@(_ : _) -> for_ issues $ embedFormula beh . anItem
47+ [] -> do
48+ -- We have the following isomorphisms:
49+ -- (A ⊂ X ∩ Y) = (A ⊂ X) /\ (A ⊂ Y)
50+ -- (A ⊂ ⊤) = 1
51+ -- (X ∪ Y ⊂ B) = (X ⊂ B) /\ (Y ⊂ B)
52+ -- (∅ ⊂ B) = 1
53+ -- The remaining cases are, notably, not isomorphisms:
54+ -- 1) (A ⊂ X ∪ Y) <= (A ⊂ X) \/ (A ⊂ Y)
55+ -- 2) (A ⊂ ∅) <= 0
56+ -- 3) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
57+ -- 4) (⊤ ⊂ B) <= 0
58+ -- Therefore we have the implications with (∃ and ∀ being the N-ary
59+ -- versions of \/ and /\ respectively):
60+ -- (⋃_i ⋂_j A[i,j]) ⊂ (⋃_k ⋂_l B[k,l])
61+ -- <= ∃k ∀l ∀i, (⋂_j A[i,j]) ⊂ B[k,l]
62+ -- = ∀i ∃k ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
63+ -- with the caveat that the the set over which k ranges is nonempty.
64+ -- (because 2) is not an isomorphism), and that this is a sufficient
65+ -- but not necessary condition (because 1) is not an isomorphism).
66+ -- Our disjunction loses information, so it makes sense to nest it as
67+ -- deeply as possible, hence we choose the latter representation.
68+ --
69+ -- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
70+ -- function, with the understanding that ∃j, A[j] ⊂ B is a sufficient,
71+ -- but not necessary condition (because of 3) and 4)).
72+ --
73+ -- If k ranges over an empty set, we have the isomorphism:
74+ -- (⋃_i ⋂_j A[i,j]) ⊂ ∅ = ∀i, (⋂_j A[i,j]) ⊂ ∅
75+ -- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
76+ -- shortcut of ∃j, A[j] ⊂ ∅ hardly helps.
77+ --
78+ -- Disjunctions tend to erase informative error messages, so we may want
79+ -- to avoid them. This can be formally done as follows: if we can
80+ -- partition the universal set into a disjoint union of some parts:
81+ -- ⊤ = ⊔_α P[α]
82+ -- such that the conjuncts in our disjunctive normal form are subordinate
83+ -- to the partition:
84+ -- ∀i ∃α, (⋂_j A[i,j]) ⊂ P[α]
85+ -- ∀k ∃α, (⋂_l B[k,l]) ⊂ P[α]
86+ -- then we can partition the sets over which i and k range into partitions
87+ -- I[α] and K[α], and then in each "bucket" verify the inclusion in the
88+ -- aforementioned way:
89+ -- ∀α, (⋃_i∈I[α] ⋂_j A[i,j]) ⊂ (⋃_k∈K[α] ⋂_l B[k,l])
90+ -- = ∀α ∀i∈I[α] ∃k∈K[α] ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
91+ -- We already somewhat do this by partitioning JSON into types, but we can
92+ -- additionally partition e.g. "enum" fields or existence of particular
93+ -- properties. This works especially well if we manage to ensure K[α] are
94+ -- 1-element sets.
95+ --
96+ -- Since the set:
97+ -- (⋃_i∈I[α] ⋂_j A[i,j]) = (⋃_i ⋂_j A[i,j]) ∩ P[α]
98+ -- does not actually appear in the source schema, we need to construct it
99+ -- ourselves and come up with a name for it.
100+ let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
101+ -- Specifically handle the case when a schema's type has been
102+ -- restricted from "all" to specific types: if all types were allowed
103+ -- in the producer and not all types are allowed in the consumer, it's
104+ -- usually easier to say what's left than what's removed
105+ when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
106+ forType_ $ \tyName ty -> do
107+ let beh' = beh >>> step (OfType tyName)
108+ case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
109+ (DNF pss, BottomDNF) -> unless typesRestricted $ do
110+ -- don't repeat the TypesRestricted issue
111+ for_ pss $ \(Disjunct ps) -> checkContradiction beh' Nothing ps
112+ (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
113+ for_ cs $ checkImplication env beh' trs ps -- avoid disjunction if there's only one conjunct
114+ (TopDNF, DNF css) ->
115+ -- producer is "open" (allows any value), but consumer has restrictions.
116+ -- In this case we want to show which restrictions were added. (instead
117+ -- of showing an empty list restrictions that couldn't be satisfied.)
118+ for_ css $ \(Disjunct cs) -> for_ cs $ checkImplication env beh' trs S.empty
119+ (pss', css') -> for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
120+ (mPart, ProdCons pf cf) -> do
121+ let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
122+ case (getJsonFormula pf, getJsonFormula cf) of
123+ (DNF pss, BottomDNF) -> for_ pss $ \(Disjunct ps) -> checkContradiction beh' mPart ps
124+ (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
125+ for_ cs $ checkImplication env beh'' trs ps
126+ -- unlucky:
127+ (DNF pss, DNF css) -> for_ pss $ \(Disjunct ps) -> do
128+ anyOfAt
129+ beh'
130+ (issueFromDisjunct Nothing ps)
131+ [for_ cs $ checkImplication env beh' trs ps | Disjunct cs <- S.toList css]
132+ pure ()
133+ where
134+ anyBottomTypes f = getAny $
135+ foldType $ \_ ty -> case getJsonFormula $ ty f of
136+ BottomDNF -> Any True
137+ _ -> mempty
138+ nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
139+ BottomDNF -> mempty
140+ _ -> [tyName]
141+ issueFromDisjunct :: Typeable t => Maybe Partition -> S.Set (Condition t) -> Issue 'TypedSchemaLevel
142+ issueFromDisjunct _ ps
143+ | Just e <- findExactly ps
144+ , all (satisfiesTyped e) ps =
145+ EnumDoesntSatisfy $ untypeValue e -- what does this look like when partitioned?
146+ issueFromDisjunct mPart ps = NoMatchingCondition mPart $ SomeCondition <$> S.toList ps
147+
148+checkContradiction ::
149+ Behavior 'TypedSchemaLevel ->
150+ Maybe Partition ->
151+ S.Set (Condition t) ->
152+ SemanticCompatFormula ()
153+checkContradiction beh mPart _ = issueAt beh $ maybe TypeBecomesEmpty PartitionBecomesEmpty mPart -- TODO #70
154+
155+checkImplication ::
156+ (ReassembleHList xs (CheckEnv (Referenced Schema))) =>
157+ HList xs ->
158+ Behavior 'TypedSchemaLevel ->
159+ ProdCons (Trace Schema) -> -- the traces of the root schemas used in this comparison
160+ S.Set (Condition t) ->
161+ Condition t ->
162+ SemanticCompatFormula ()
163+checkImplication env beh trs prods cons = case findExactly prods of
164+ Just e
165+ | all (satisfiesTyped e) prods ->
166+ if satisfiesTyped e cons
167+ then pure ()
168+ else issueAt beh (EnumDoesntSatisfy $ untypeValue e)
169+ | otherwise -> pure () -- vacuously true
170+ Nothing -> case cons of
171+ -- the above code didn't catch it, so there's no Exactly condition on the lhs
172+ Exactly e -> issueAt beh (NoMatchingEnum $ untypeValue e)
173+ Maximum m -> foldCheck min m NoMatchingMaximum MatchingMaximumWeak $ \case
174+ Maximum m' -> Just m'
175+ _ -> Nothing
176+ Minimum m -> foldCheck max m (NoMatchingMinimum . coerce) (MatchingMinimumWeak . coerce) $ \case
177+ Minimum m' -> Just m'
178+ _ -> Nothing
179+ MultipleOf m -> foldCheck lcmScientific m NoMatchingMultipleOf MatchingMultipleOfWeak $ \case
180+ MultipleOf m' -> Just m'
181+ _ -> Nothing
182+ NumberFormat f -> case flip any prods $ \case
183+ NumberFormat f' -> f == f'
184+ _ -> False of
185+ True -> pure ()
186+ False -> issueAt beh (NoMatchingFormat f)
187+ MaxLength m -> foldCheck min m NoMatchingMaxLength MatchingMaxLengthWeak $ \case
188+ MaxLength m' -> Just m'
189+ _ -> Nothing
190+ MinLength m -> foldCheck max m NoMatchingMinLength MatchingMinLengthWeak $ \case
191+ MinLength m' -> Just m'
192+ _ -> Nothing
193+ Pattern p -> case flip any prods $ \case
194+ Pattern p' -> p == p'
195+ _ -> False of
196+ True -> pure ()
197+ False -> issueAt beh (NoMatchingPattern p) -- TODO: regex comparison #32
198+ StringFormat f -> case flip any prods $ \case
199+ StringFormat f' -> f == f'
200+ _ -> False of
201+ True -> pure ()
202+ False -> issueAt beh (NoMatchingFormat f)
203+ Items _ cons' -> case foldSome (<>) prods $ \case
204+ Items _ rs -> Just (Just (rs NE.:| []), mempty)
205+ TupleItems (map snd -> fs) -> Just (mempty, Just (fs NE.:| []))
206+ _ -> Nothing of
207+ Just (mItems, Just pfs)
208+ | not $ allSame (length <$> pfs) -> pure () -- vacuously
209+ | let plen = genericLength (NE.head pfs) ->
210+ clarifyIssue (AnItem beh (anIssue TupleToArray)) $
211+ for_ [0 .. plen - 1] $ \i -> do
212+ let prod' = tracedConjunct $ case mItems of
213+ Just prods' -> ((`genericIndex` i) <$> pfs) <> prods'
214+ Nothing -> (`genericIndex` i) <$> pfs
215+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons prod' cons'
216+ Just (Just prods', Nothing) -> do
217+ let prod' = tracedConjunct prods'
218+ checkCompatibility (beh >>> step InItems) env $ ProdCons prod' cons'
219+ _ -> clarifyIssue (AnItem beh (anIssue NoMatchingItems)) $ do
220+ checkCompatibility (beh >>> step InItems) env $ ProdCons prodTopSchema cons'
221+ TupleItems (map snd -> fs) -> case foldSome (<>) prods $ \case
222+ TupleItems (map snd -> fs') -> Just (Just $ fs' NE.:| [], Just . Max $ genericLength fs', Just . Min $ genericLength fs', mempty)
223+ MinItems m' -> Just (mempty, Just . Max $ m', mempty, mempty)
224+ MaxItems m' -> Just (mempty, mempty, Just . Min $ m', mempty)
225+ Items _ rs -> Just (mempty, mempty, mempty, Just (rs NE.:| []))
226+ _ -> Nothing of
227+ -- if the length constraints in the producer are contradictory:
228+ Just (_, Just (Max lowest), Just (Min highest), _) | lowest > highest -> pure ()
229+ -- We have an explicit tuple items clause...
230+ Just (Just pfs, Just (Max plen), _, _)
231+ | plen /= genericLength fs -> -- ...of wrong length
232+ issueAt beh (TupleItemsLengthChanged ProdCons {producer = plen, consumer = genericLength fs})
233+ | otherwise ->
234+ for_ [0 .. plen - 1] $ \i -> do
235+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons (tracedConjunct $ (`genericIndex` i) <$> pfs) (fs `genericIndex` i)
236+ -- We have a fixed length array in the producer...
237+ Just (Nothing, Just (Max plen), Just (Min plen'), mProd)
238+ | plen == plen' ->
239+ clarifyIssue (AnItem beh (anIssue ArrayToTuple)) $ case mProd of
240+ _
241+ | plen /= genericLength fs -> -- ...of wrong length
242+ issueAt beh (TupleItemsLengthChanged ProdCons {producer = plen, consumer = genericLength fs})
243+ Just rs -> for_ [0 .. plen - 1] $ \i -> do
244+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons (tracedConjunct rs) (fs `genericIndex` i)
245+ -- ...and no "items" schema
246+ Nothing -> clarifyIssue (AnItem beh (anIssue NoMatchingTupleItems)) $ do
247+ for_ [0 .. plen - 1] $ \i -> do
248+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons prodTopSchema (fs `genericIndex` i)
249+ _ -> issueAt beh NoMatchingTupleItems
250+ MaxItems m -> foldCheck min m NoMatchingMaxItems MatchingMaxItemsWeak $ \case
251+ MaxItems m' -> Just m'
252+ TupleItems fs -> Just $ toInteger $ length fs
253+ _ -> Nothing
254+ MinItems m -> foldCheck max m NoMatchingMinItems MatchingMinItemsWeak $ \case
255+ MinItems m' -> Just m'
256+ TupleItems fs -> Just $ toInteger $ length fs
257+ _ -> Nothing
258+ UniqueItems -> case flip any prods $ \case
259+ UniqueItems -> True
260+ MaxItems 1 -> True
261+ TupleItems fs | length fs == 1 -> True
262+ _ -> False of
263+ True -> pure ()
264+ False -> issueAt beh NoMatchingUniqueItems
265+ Properties props _ madd -> case foldSome (<>) prods $ \case
266+ Properties props' _ madd' -> Just $ (props', madd') NE.:| []
267+ _ -> Nothing of
268+ Just pm ->
269+ anyOfAt beh NoMatchingProperties $ -- TODO: could first "concat" the lists
270+ NE.toList pm <&> \(props', madd') -> do
271+ for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
272+ let beh' = beh >>> step (InProperty k)
273+ go sch sch' = checkCompatibility beh' env (ProdCons sch sch')
274+ case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
275+ -- producer does not require field, but consumer does (can fail)
276+ (False, True) -> issueAt beh' PropertyNowRequired
277+ _ -> pure ()
278+ case (M.lookup k props', madd', M.lookup k props, madd) of
279+ -- (producer, additional producer, consumer, additional consumer)
280+ (Nothing, Nothing, _, _) -> pure () -- vacuously: the producer asserts that this field cannot exist,
281+ -- and the consumer either doesn't require it, or it does and we've already raised an error about it.
282+ (_, _, Nothing, Nothing) -> issueAt beh' UnexpectedProperty
283+ (Just p', _, Just p, _) -> go (propRefSchema p') (propRefSchema p)
284+ (Nothing, Just add', Just p, _) ->
285+ clarifyIssue (AnItem beh' (anIssue AdditionalToProperty)) $
286+ go add' (propRefSchema p)
287+ (Just p', _, Nothing, Just add) ->
288+ clarifyIssue (AnItem beh' (anIssue PropertyToAdditional)) $
289+ go (propRefSchema p') add
290+ (Nothing, Just _, Nothing, Just _) -> pure ()
291+ pure ()
292+ case (madd', madd) of
293+ (Nothing, _) -> pure () -- vacuously
294+ (_, Nothing) -> issueAt beh NoAdditionalProperties
295+ (Just add', Just add) -> checkCompatibility (beh >>> step InAdditionalProperty) env (ProdCons add' add)
296+ pure ()
297+ Nothing -> issueAt beh NoMatchingProperties
298+ MaxProperties m -> foldCheck min m NoMatchingMaxProperties MatchingMaxPropertiesWeak $ \case
299+ MaxProperties m' -> Just m'
300+ _ -> Nothing
301+ MinProperties m -> foldCheck max m NoMatchingMinProperties MatchingMinPropertiesWeak $ \case
302+ MinProperties m' -> Just m'
303+ _ -> Nothing
304+ where
305+ lcmScientific (toRational -> a) (toRational -> b) =
306+ fromRational $ lcm (numerator a) (numerator b) % gcd (denominator a) (denominator b)
307+
308+ foldCheck ::
309+ Eq a =>
310+ (a -> a -> a) ->
311+ a ->
312+ (a -> Issue 'TypedSchemaLevel) ->
313+ (ProdCons a -> Issue 'TypedSchemaLevel) ->
314+ (forall t. Condition t -> Maybe a) ->
315+ SemanticCompatFormula ()
316+ foldCheck f m missing weak extr = case foldSome f prods extr of
317+ Just m'
318+ | f m' m == m' -> pure ()
319+ | otherwise -> issueAt beh (weak ProdCons {producer = m', consumer = m})
320+ Nothing -> issueAt beh (missing m)
321+
322+ prodTopSchema = traced (producer trs >>> step ImplicitTopSchema) $ Inline mempty
323+
324+allSame :: (Foldable f, Eq a) => f a -> Bool
325+allSame xs = case group (toList xs) of
326+ [] -> True
327+ [_] -> True
328+ _ -> False
329+
330+foldSome :: (b -> b -> b) -> S.Set a -> (a -> Maybe b) -> Maybe b
331+foldSome combine xs extr =
332+ fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr . S.toList $ xs
333+
334+findExactly :: S.Set (Condition t) -> Maybe (TypedValue t)
335+findExactly xs = foldSome const xs $ \case
336+ Exactly x -> Just x
337+ _ -> Nothing
338+
339+instance Subtree Schema where
340+ type SubtreeLevel Schema = 'SchemaLevel
341+ type CheckEnv Schema = '[ProdCons (Traced (Definitions Schema))]
342+ checkStructuralCompatibility env pc = do
343+ structuralEq $ fmap _schemaRequired <$> pc
344+ structuralEq $ fmap _schemaNullable <$> pc
345+ structuralMaybeWith (structuralList env) $ tracedAllOf <$> pc
346+ structuralMaybeWith (structuralList env) $ tracedOneOf <$> pc
347+ structuralMaybe env $ sequence . stepTraced NotStep . fmap _schemaNot <$> pc
348+ structuralMaybeWith (structuralList env) $ tracedAnyOf <$> pc
349+ iohmStructural env $ stepTraced PropertiesStep . fmap _schemaProperties <$> pc
350+ structuralMaybeWith structuralAdditionalProperties $ tracedAdditionalProperties <$> pc
351+ structuralMaybeWith structuralDiscriminator $ tracedDiscriminator <$> pc
352+ structuralEq $ fmap _schemaReadOnly <$> pc
353+ structuralEq $ fmap _schemaWriteOnly <$> pc
354+ structuralEq $ fmap _schemaXml <$> pc
355+ structuralEq $ fmap _schemaMaxProperties <$> pc
356+ structuralEq $ fmap _schemaMinProperties <$> pc
357+ structuralEq $ fmap _schemaDefault <$> pc
358+ structuralEq $ fmap _schemaType <$> pc
359+ structuralEq $ fmap _schemaFormat <$> pc
360+ structuralMaybeWith structuralItems $ tracedItems <$> pc
361+ structuralEq $ fmap _schemaMaximum <$> pc
362+ structuralEq $ fmap _schemaExclusiveMaximum <$> pc
363+ structuralEq $ fmap _schemaMinimum <$> pc
364+ structuralEq $ fmap _schemaExclusiveMinimum <$> pc
365+ structuralEq $ fmap _schemaMaxLength <$> pc
366+ structuralEq $ fmap _schemaMinLength <$> pc
367+ structuralEq $ fmap _schemaPattern <$> pc
368+ structuralEq $ fmap _schemaMaxItems <$> pc
369+ structuralEq $ fmap _schemaMinItems <$> pc
370+ structuralEq $ fmap _schemaUniqueItems <$> pc
371+ structuralEq $ fmap _schemaEnum <$> pc
372+ structuralEq $ fmap _schemaMultipleOf <$> pc
373+ pure ()
374+ where
375+ structuralAdditionalProperties
376+ (ProdCons (Left x) (Left y)) = unless (x == y) structuralIssue
377+ structuralAdditionalProperties
378+ (ProdCons (Right x) (Right y)) =
379+ checkSubstructure env $ ProdCons x y
380+ structuralAdditionalProperties _ = structuralIssue
381+ structuralDiscriminator pc' = do
382+ structuralEq $ fmap _discriminatorPropertyName <$> pc'
383+ iohmStructural env $
384+ stepTraced DiscriminatorMapping . fmap (fmap parseDiscriminatorValue . _discriminatorMapping) <$> pc'
385+ pure ()
386+ structuralItems (ProdCons (Left a) (Left b)) =
387+ checkSubstructure env $ ProdCons a b
388+ structuralItems (ProdCons (Right a) (Right b)) =
389+ structuralList env $ ProdCons a b
390+ structuralItems _ = structuralIssue
391+ checkSemanticCompatibility env beh schs = do
392+ let defs = getH env
393+ checkFormulas env beh (ask <$> schs) defs $ schemaToFormula <$> defs <*> schs
394+
395+parseDiscriminatorValue :: Text -> Referenced Schema
396+parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of
397+ A.Success x -> x
398+ A.Error _ -> Ref $ Reference v
src/Data/OpenApi/Compare/Validate/Schema/DNF.hsnew file+101
@@ -0,0 +1,101 @@
1+module Data.OpenApi.Compare.Validate.Schema.DNF
2+ ( DNF (..),
3+ Disjunct (..),
4+ pattern SingleDisjunct,
5+ pattern TopDNF,
6+ pattern BottomDNF,
7+ pattern LiteralDNF,
8+ foldDNF,
9+ forDNF,
10+ )
11+where
12+
13+import Algebra.Lattice
14+import Control.Applicative
15+import Data.Foldable
16+import qualified Data.Set as S
17+
18+-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
19+-- the formula is a disjunction of a set of clauses, each of which is a
20+-- conjunction of a set of some elementary formulas.
21+-- Invariant: no two disjuncts imply eachother
22+newtype DNF a = DNF (S.Set (Disjunct a))
23+ deriving stock (Eq, Ord, Show)
24+
25+-- A disjunct is a thing that is to be disjuncted. Itself it is a conjunction of some literals.
26+newtype Disjunct a = Disjunct (S.Set a)
27+ deriving stock (Eq, Ord, Show)
28+
29+disjImplies :: Ord a => Disjunct a -> Disjunct a -> Bool
30+disjImplies (Disjunct xs) (Disjunct ys) = ys `S.isSubsetOf` xs
31+
32+disjConjunction :: Ord a => Disjunct a -> Disjunct a -> Disjunct a
33+disjConjunction (Disjunct xs) (Disjunct ys) = Disjunct $ xs `S.union` ys
34+
35+disjAdd :: Ord a => DNF a -> Disjunct a -> DNF a
36+disjAdd (DNF yss) xs
37+ | any (xs `disjImplies`) yss = DNF yss
38+ | otherwise = DNF $ S.insert xs $ S.filter (not . (`disjImplies` xs)) yss
39+
40+instance Ord a => Lattice (DNF a) where
41+ xss \/ DNF yss = S.foldl' disjAdd xss yss
42+ DNF xss /\ DNF yss =
43+ foldl' disjAdd bottom $
44+ liftA2 disjConjunction (S.toList xss) (S.toList yss)
45+
46+pattern BottomDNF :: DNF a
47+pattern BottomDNF <-
48+ DNF (S.null -> True)
49+ where
50+ BottomDNF = DNF S.empty
51+
52+isSingleton :: S.Set a -> Maybe a
53+isSingleton s
54+ | S.size s == 1 = S.lookupMin s
55+ | otherwise = Nothing
56+
57+pattern SingleDisjunct :: Ord a => Disjunct a -> DNF a
58+pattern SingleDisjunct xs <-
59+ DNF (isSingleton -> Just xs)
60+ where
61+ SingleDisjunct xs = DNF $ S.singleton xs
62+
63+pattern TopDNF :: DNF a
64+pattern TopDNF <-
65+ DNF (isSingleton -> Just (Disjunct (S.null -> True)))
66+ where
67+ TopDNF = DNF $ S.singleton $ Disjunct S.empty
68+
69+pattern LiteralDNF :: Ord a => a -> DNF a
70+pattern LiteralDNF x <-
71+ SingleDisjunct (Disjunct (isSingleton -> Just x))
72+ where
73+ LiteralDNF x = SingleDisjunct $ Disjunct $ S.singleton x
74+
75+instance Ord a => BoundedJoinSemiLattice (DNF a) where
76+ bottom = BottomDNF
77+
78+instance Ord a => BoundedMeetSemiLattice (DNF a) where
79+ top = TopDNF
80+
81+foldDisjunct :: BoundedMeetSemiLattice l => (a -> l) -> Disjunct a -> l
82+foldDisjunct f (Disjunct xs) = S.foldl' (\y l -> y /\ f l) top xs
83+
84+foldDNF :: BoundedLattice l => (a -> l) -> DNF a -> l
85+foldDNF f (DNF xss) = S.foldl' (\y xs -> y \/ foldDisjunct f xs) bottom xss
86+
87+newtype LiftA f a = LiftA {getLiftA :: f a}
88+ deriving newtype (Functor, Applicative)
89+
90+instance (Lattice a, Applicative f) => Lattice (LiftA f a) where
91+ (/\) = liftA2 (/\)
92+ (\/) = liftA2 (\/)
93+
94+instance (BoundedJoinSemiLattice a, Applicative f) => BoundedJoinSemiLattice (LiftA f a) where
95+ bottom = pure bottom
96+
97+instance (BoundedMeetSemiLattice a, Applicative f) => BoundedMeetSemiLattice (LiftA f a) where
98+ top = pure top
99+
100+forDNF :: (BoundedLattice l, Applicative f) => (a -> f l) -> DNF a -> f l
101+forDNF f = getLiftA . foldDNF (LiftA . f)
src/Data/OpenApi/Compare/Validate/Schema/Issues.hsnew file+321
@@ -0,0 +1,321 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Data.OpenApi.Compare.Validate.Schema.Issues
4+ ( Issue (..),
5+ Behave (..),
6+ )
7+where
8+
9+import qualified Data.Aeson as A
10+import Data.OpenApi
11+import Data.OpenApi.Compare.Behavior
12+import Data.OpenApi.Compare.Subtree
13+import Data.OpenApi.Compare.Validate.Schema.JsonFormula
14+import Data.OpenApi.Compare.Validate.Schema.Partition
15+import Data.OpenApi.Compare.Validate.Schema.TypedJson
16+import Data.Scientific
17+import Data.Text (Text)
18+import qualified Data.Text as T
19+import Text.Pandoc.Builder hiding (Format, Null)
20+
21+instance Issuable 'TypedSchemaLevel where
22+ data Issue 'TypedSchemaLevel
23+ = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
24+ EnumDoesntSatisfy A.Value
25+ | -- | consumer only expects a specific value which the producer does not produce.
26+ NoMatchingEnum A.Value
27+ | -- | consumer declares a maximum numeric value ($1), producer doesn't
28+ NoMatchingMaximum (Bound Scientific)
29+ | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
30+ MatchingMaximumWeak (ProdCons (Bound Scientific))
31+ | -- | consumer declares a minimum numeric value, producer doesn't
32+ NoMatchingMinimum (Bound Scientific)
33+ | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
34+ MatchingMinimumWeak (ProdCons (Bound Scientific))
35+ | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
36+ NoMatchingMultipleOf Scientific
37+ | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
38+ MatchingMultipleOfWeak (ProdCons Scientific)
39+ | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
40+ NoMatchingFormat Format
41+ | -- | consumer declares a maximum length of the string ($1), producer doesn't.
42+ NoMatchingMaxLength Integer
43+ | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
44+ MatchingMaxLengthWeak (ProdCons Integer)
45+ | -- | consumer declares a minimum length of the string ($1), producer doesn't.
46+ NoMatchingMinLength Integer
47+ | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
48+ MatchingMinLengthWeak (ProdCons Integer)
49+ | -- | consumer declares the string value must match a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
50+ NoMatchingPattern Pattern
51+ | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
52+ NoMatchingItems
53+ | -- | producer and consumer declare that an array must be a tuple of a fixed length, but the lengths don't match
54+ TupleItemsLengthChanged (ProdCons Integer)
55+ | -- | consumer declares that the array is a tuple, but the producer doesn't, the length constraints match, but there were issues with the components
56+ ArrayToTuple
57+ | -- | producer declares that the array is a tuple, but the consumer doesn't, and there were issues with the components
58+ TupleToArray
59+ | -- | consumer declares that the array is a tuple, but the producer doesn't, and there aren't sufficient length constraints
60+ NoMatchingTupleItems
61+ | -- | consumer declares a maximum length of the array ($1), producer doesn't.
62+ NoMatchingMaxItems Integer
63+ | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
64+ MatchingMaxItemsWeak (ProdCons Integer)
65+ | -- | consumer declares a minimum length of the array ($1), producer doesn't.
66+ NoMatchingMinItems Integer
67+ | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
68+ MatchingMinItemsWeak (ProdCons Integer)
69+ | -- | consumer declares that items must be unique, producer doesn't
70+ NoMatchingUniqueItems
71+ | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
72+ NoMatchingProperties
73+ | -- | producer allows additional properties, consumer doesn't
74+ NoAdditionalProperties
75+ | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
76+ NoMatchingMaxProperties Integer
77+ | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
78+ MatchingMaxPropertiesWeak (ProdCons Integer)
79+ | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
80+ NoMatchingMinProperties Integer
81+ | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
82+ MatchingMinPropertiesWeak (ProdCons Integer)
83+ | -- | producer declares that the value must satisfy a disjunction of some conditions, but consumer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
84+ NoMatchingCondition (Maybe Partition) [SomeCondition]
85+ | -- | consumer indicates that no values of this type are allowed, but we weren't able to conclude that in the producer (currently only immediate contradictions are checked, c.f. #70)
86+ TypeBecomesEmpty
87+ | -- | consumer indicates that no values in a particular partition are allowed, but we weren't able to conclude this in the producer
88+ PartitionBecomesEmpty Partition
89+ deriving stock (Eq, Ord, Show)
90+ issueKind = \case
91+ NoMatchingEnum _ -> ProbablyIssue
92+ MatchingMaximumWeak _ -> ProbablyIssue -- interplay with MultipleOf could make this not an issue
93+ MatchingMinimumWeak _ -> ProbablyIssue -- ditto
94+ MatchingMultipleOfWeak _ -> ProbablyIssue -- ditto
95+ NoMatchingFormat _ -> Unsupported
96+ NoMatchingPattern _ -> Unsupported
97+ ArrayToTuple -> Comment
98+ TupleToArray -> Comment
99+ NoMatchingProperties -> ProbablyIssue -- TODO: #109
100+ TypeBecomesEmpty -> ProbablyIssue -- TODO: #70
101+ PartitionBecomesEmpty _ -> ProbablyIssue -- ditto
102+ _ -> CertainIssue
103+ relatedIssues =
104+ (==) `withClass` \case
105+ EnumDoesntSatisfy v -> Just v
106+ NoMatchingEnum v -> Just v
107+ _ -> Nothing
108+ `withClass` \case
109+ NoMatchingMaximum _ -> Just ()
110+ MatchingMaximumWeak _ -> Just ()
111+ _ -> Nothing
112+ `withClass` \case
113+ NoMatchingMinimum _ -> Just ()
114+ MatchingMinimumWeak _ -> Just ()
115+ _ -> Nothing
116+ `withClass` \case
117+ NoMatchingMultipleOf _ -> Just ()
118+ MatchingMultipleOfWeak _ -> Just ()
119+ _ -> Nothing
120+ `withClass` \case
121+ NoMatchingMaxLength _ -> Just ()
122+ MatchingMaxLengthWeak _ -> Just ()
123+ _ -> Nothing
124+ `withClass` \case
125+ NoMatchingMinLength _ -> Just ()
126+ MatchingMinLengthWeak _ -> Just ()
127+ _ -> Nothing
128+ `withClass` \case
129+ NoMatchingItems -> Just ()
130+ ArrayToTuple -> Just ()
131+ TupleToArray -> Just ()
132+ NoMatchingTupleItems -> Just ()
133+ TupleItemsLengthChanged _ -> Just ()
134+ _ -> Nothing
135+ `withClass` \case
136+ NoMatchingMaxItems _ -> Just ()
137+ MatchingMaxItemsWeak _ -> Just ()
138+ _ -> Nothing
139+ `withClass` \case
140+ NoMatchingMinItems _ -> Just ()
141+ MatchingMinItemsWeak _ -> Just ()
142+ _ -> Nothing
143+ `withClass` \case
144+ NoMatchingMaxProperties _ -> Just ()
145+ MatchingMaxPropertiesWeak _ -> Just ()
146+ _ -> Nothing
147+ `withClass` \case
148+ NoMatchingMinProperties _ -> Just ()
149+ MatchingMinPropertiesWeak _ -> Just ()
150+ _ -> Nothing
151+ describeIssue Forward (EnumDoesntSatisfy v) = para "The following enum value was removed:" <> showJSONValue v
152+ describeIssue Backward (EnumDoesntSatisfy v) = para "The following enum value was added:" <> showJSONValue v
153+ describeIssue Forward (NoMatchingEnum v) = para "The following enum value has been added:" <> showJSONValue v
154+ describeIssue Backward (NoMatchingEnum v) = para "The following enum value has been removed:" <> showJSONValue v
155+ describeIssue Forward (NoMatchingMaximum b) = para $ "Upper bound has been added:" <> showBound b <> "."
156+ describeIssue Backward (NoMatchingMaximum b) = para $ "Upper bound has been removed:" <> showBound b <> "."
157+ describeIssue ori (MatchingMaximumWeak (orientProdCons ori -> ProdCons p c)) =
158+ para $ "Upper bound changed from " <> showBound p <> " to " <> showBound c <> "."
159+ describeIssue Forward (NoMatchingMinimum b) = para $ "Lower bound has been added: " <> showBound b <> "."
160+ describeIssue Backward (NoMatchingMinimum b) = para $ "Lower bound has been removed: " <> showBound b <> "."
161+ describeIssue ori (MatchingMinimumWeak (orientProdCons ori -> ProdCons p c)) =
162+ para $ "Lower bound changed from " <> showBound p <> " to " <> showBound c <> "."
163+ describeIssue Forward (NoMatchingMultipleOf n) = para $ "Value is now a multiple of " <> show' n <> "."
164+ describeIssue Backward (NoMatchingMultipleOf n) = para $ "Value is no longer a multiple of " <> show' n <> "."
165+ describeIssue ori (MatchingMultipleOfWeak (orientProdCons ori -> ProdCons p c)) =
166+ para $ "Value changed from being a multiple of " <> show' p <> " to being a multiple of " <> show' c <> "."
167+ describeIssue Forward (NoMatchingFormat f) = para $ "Format added: " <> code f <> "."
168+ describeIssue Backward (NoMatchingFormat f) = para $ "Format removed: " <> code f <> "."
169+ describeIssue Forward (NoMatchingMaxLength n) = para $ "Maximum length added: " <> show' n <> "."
170+ describeIssue Backward (NoMatchingMaxLength n) = para $ "Maximum length removed: " <> show' n <> "."
171+ describeIssue ori (MatchingMaxLengthWeak (orientProdCons ori -> ProdCons p c)) =
172+ para $ "Maximum length of the string changed from " <> show' p <> " to " <> show' c <> "."
173+ describeIssue Forward (NoMatchingMinLength n) = para $ "Minimum length of the string added: " <> show' n <> "."
174+ describeIssue Backward (NoMatchingMinLength n) = para $ "Minimum length of the string removed: " <> show' n <> "."
175+ describeIssue ori (MatchingMinLengthWeak (orientProdCons ori -> ProdCons p c)) =
176+ para $ "Minimum length of the string changed from " <> show' p <> " to " <> show' c <> "."
177+ describeIssue Forward (NoMatchingPattern p) = para "Pattern (regular expression) added: " <> codeBlock p
178+ describeIssue Backward (NoMatchingPattern p) = para "Pattern (regular expression) removed: " <> codeBlock p
179+ describeIssue Forward NoMatchingItems = para "Array item schema has been added."
180+ describeIssue Backward NoMatchingItems = para "Array item schema has been removed."
181+ describeIssue ori (TupleItemsLengthChanged (orientProdCons ori -> ProdCons p c)) =
182+ para $ "Tuple length changed from " <> show' p <> " to " <> show' c <> "."
183+ describeIssue Forward ArrayToTuple = para "The array is now explicitly defined as a tuple."
184+ describeIssue Backward ArrayToTuple = para "The array is no longer explicitly defined as a tuple."
185+ describeIssue Forward TupleToArray = para "The array is no longer explicitly defined as a tuple."
186+ describeIssue Backward TupleToArray = para "The array is now explicitly defined as a tuple."
187+ describeIssue Forward NoMatchingTupleItems = para "The array is now explicitly defined as a tuple."
188+ describeIssue Backward NoMatchingTupleItems = para "The array is no longer explicitly defined as a tuple."
189+ describeIssue Forward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been added " <> show' n <> "."
190+ describeIssue Backward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been removed " <> show' n <> "."
191+ describeIssue ori (MatchingMaxItemsWeak (orientProdCons ori -> ProdCons p c)) =
192+ para $ "Maximum length of the array changed from " <> show' p <> " to " <> show' c <> "."
193+ describeIssue Forward (NoMatchingMinItems n) = para $ "Minimum length of the array added: " <> show' n <> "."
194+ describeIssue Backward (NoMatchingMinItems n) = para $ "Minimum length of the array removed: " <> show' n <> "."
195+ describeIssue ori (MatchingMinItemsWeak (orientProdCons ori -> ProdCons p c)) =
196+ para $ "Minimum length of the array changed from " <> show' p <> " to " <> show' c <> "."
197+ describeIssue Forward NoMatchingUniqueItems = para "Items are now required to be unique."
198+ describeIssue Backward NoMatchingUniqueItems = para "Items are no longer required to be unique."
199+ describeIssue Forward NoMatchingProperties = para "Property added."
200+ describeIssue Backward NoMatchingProperties = para "Property removed."
201+ describeIssue Forward NoAdditionalProperties = para "Additional properties have been removed."
202+ describeIssue Backward NoAdditionalProperties = para "Additional properties have been added."
203+ describeIssue Forward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been added: " <> show' n <> "."
204+ describeIssue Backward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been removed: " <> show' n <> "."
205+ describeIssue ori (MatchingMaxPropertiesWeak (orientProdCons ori -> ProdCons p c)) =
206+ para $ "Maximum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
207+ describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
208+ describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
209+ describeIssue ori (MatchingMinPropertiesWeak (orientProdCons ori -> ProdCons p c)) =
210+ para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
211+ describeIssue _ (NoMatchingCondition mPart conds) =
212+ para
213+ ( case mPart of
214+ Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this):"
215+ Just locPart ->
216+ "In cases where " <> showPartition locPart
217+ <> " – could not verify that the following conditions hold (please file a bug if you see this):"
218+ )
219+ <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
220+ describeIssue Forward TypeBecomesEmpty = para "The type has been removed."
221+ describeIssue Backward TypeBecomesEmpty = para "The type has been added."
222+ describeIssue Forward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been removed."
223+ describeIssue Backward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been added."
224+
225+show' :: Show x => x -> Inlines
226+show' = str . T.pack . show
227+
228+instance Issuable 'SchemaLevel where
229+ data Issue 'SchemaLevel
230+ = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
231+ NotSupported Text
232+ | -- | We couldn't prove that the branches of a oneOf are disjoint, and we will treat it as an anyOf, meaning we don't check whether the overlaps are excluded in a compatible way
233+ OneOfNotDisjoint
234+ | -- | The schema is actually invalid
235+ InvalidSchema Text
236+ | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
237+ UnguardedRecursion
238+ | -- | Producer doesn't place any restrictions on the types, but the consumer does. List what types remain available in the consumer.
239+ TypesRestricted [JsonType]
240+ | -- | in the producer this field used to be handled as part of "additionalProperties", and the consumer this is a specific "properties" entry. Only thrown when this change actually causes other issues
241+ AdditionalToProperty
242+ | -- | in the consumer this field used to be handled as part of "additionalProperties", and the producer this is a specific "properties" entry. Only thrown when this change actually causes other issues
243+ PropertyToAdditional
244+ | -- | consumer requires a property that is not required/allowed in the producer
245+ PropertyNowRequired
246+ | -- | producer allows a property that is not allowed in the consumer
247+ UnexpectedProperty
248+ deriving stock (Eq, Ord, Show)
249+ issueKind = \case
250+ NotSupported _ -> Unsupported
251+ OneOfNotDisjoint -> Unsupported
252+ InvalidSchema _ -> SchemaInvalid
253+ UnguardedRecursion -> Unsupported
254+ AdditionalToProperty -> Comment
255+ PropertyToAdditional -> Comment
256+ TypesRestricted _ -> ProbablyIssue -- TODO: #70
257+ _ -> CertainIssue
258+ relatedIssues =
259+ (==) `withClass` \case
260+ AdditionalToProperty -> Just ()
261+ PropertyToAdditional -> Just ()
262+ _ -> Nothing
263+ `withClass` \case
264+ PropertyNowRequired -> Just ()
265+ UnexpectedProperty -> Just ()
266+ _ -> Nothing
267+ describeIssue _ (NotSupported i) =
268+ para (emph "Encountered a feature that CompaREST does not support: " <> text i <> ".")
269+ describeIssue _ OneOfNotDisjoint =
270+ para $
271+ "Could not deduce that " <> code "oneOf"
272+ <> " cases don't overlap. Treating the "
273+ <> code "oneOf"
274+ <> " as an "
275+ <> code "anyOf"
276+ <> ". Reported errors might not be accurate."
277+ describeIssue _ (InvalidSchema i) =
278+ para (emph "The schema is invalid: " <> text i <> ".")
279+ describeIssue _ UnguardedRecursion =
280+ para "Encountered recursion that is too complex for CompaREST to untangle."
281+ describeIssue Forward (TypesRestricted tys) = case tys of
282+ [] -> para "No longer has any valid values." -- weird
283+ _ -> para "Values are now limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
284+ describeIssue Backward (TypesRestricted tys) = case tys of
285+ [] -> para "Any value of any type is now allowed." -- weird
286+ _ -> para "Values are no longer limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
287+ describeIssue Forward AdditionalToProperty = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
288+ describeIssue Backward AdditionalToProperty = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
289+ describeIssue Forward PropertyToAdditional = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
290+ describeIssue Backward PropertyToAdditional = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
291+ describeIssue Forward PropertyNowRequired = para "The property has become required."
292+ describeIssue Backward PropertyNowRequired = para "The property may not be present."
293+ describeIssue Forward UnexpectedProperty = para "The property has been removed."
294+ describeIssue Backward UnexpectedProperty = para "The property has been added."
295+
296+instance Behavable 'SchemaLevel 'TypedSchemaLevel where
297+ data Behave 'SchemaLevel 'TypedSchemaLevel
298+ = OfType JsonType
299+ deriving stock (Eq, Ord, Show)
300+
301+ describeBehavior (OfType t) = describeJSONType t
302+
303+instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
304+ data Behave 'TypedSchemaLevel 'TypedSchemaLevel
305+ = InPartition Partition
306+ deriving stock (Eq, Ord, Show)
307+
308+ describeBehavior (InPartition partition) = "In cases where " <> showPartition partition
309+
310+instance Behavable 'TypedSchemaLevel 'SchemaLevel where
311+ data Behave 'TypedSchemaLevel 'SchemaLevel
312+ = InItems
313+ | InItem Integer
314+ | InProperty Text
315+ | InAdditionalProperty
316+ deriving stock (Eq, Ord, Show)
317+
318+ describeBehavior InItems = "Items"
319+ describeBehavior (InItem i) = "Item " <> show' i
320+ describeBehavior (InProperty p) = "Property " <> code p
321+ describeBehavior InAdditionalProperty = "Additional properties"
src/Data/OpenApi/Compare/Validate/Schema/JsonFormula.hsnew file+237
@@ -0,0 +1,237 @@
1+module Data.OpenApi.Compare.Validate.Schema.JsonFormula
2+ ( Bound (..),
3+ showBound,
4+ Property (..),
5+ Condition (..),
6+ showCondition,
7+ satisfiesTyped,
8+ checkStringFormat,
9+ checkNumberFormat,
10+ SomeCondition (..),
11+ JsonFormula (..),
12+ satisfiesFormula,
13+ satisfies,
14+ showJSONValue,
15+ showJSONValueInline,
16+ )
17+where
18+
19+import Algebra.Lattice
20+import qualified Data.Aeson as A
21+import qualified Data.Aeson.Key as Key
22+import qualified Data.Aeson.KeyMap as KeyMap
23+import qualified Data.ByteString.Lazy as BSL
24+import qualified Data.Foldable as F
25+import Data.Functor
26+import Data.Int
27+import Data.Kind
28+import qualified Data.Map as M
29+import Data.Maybe
30+import Data.OpenApi
31+import Data.OpenApi.Compare.Orphans ()
32+import Data.OpenApi.Compare.Subtree
33+import Data.OpenApi.Compare.Validate.Schema.DNF
34+import Data.OpenApi.Compare.Validate.Schema.TypedJson
35+import Data.Ord
36+import Data.Ratio
37+import Data.Scientific
38+import qualified Data.Set as S
39+import Data.Text (Text)
40+import qualified Data.Text as T
41+import qualified Data.Text.Encoding as T
42+import Data.Typeable
43+import Text.Pandoc.Builder hiding (Format)
44+import Text.Regex.Pcre2
45+
46+data Bound a = Exclusive !a | Inclusive !a
47+ deriving stock (Eq, Show, Functor)
48+
49+-- | The order is lexicographical on @a * Bool@.
50+instance Ord a => Ord (Bound a) where
51+ Exclusive a `compare` Exclusive b = compare a b
52+ Exclusive a `compare` Inclusive b = if a <= b then LT else GT
53+ Inclusive a `compare` Exclusive b = if a < b then LT else GT
54+ Inclusive a `compare` Inclusive b = compare a b
55+
56+showBound :: Show a => Bound a -> Inlines
57+showBound (Inclusive x) = show' x <> " inclusive"
58+showBound (Exclusive x) = show' x <> " exclusive"
59+
60+data Property = Property
61+ { propRequired :: Bool
62+ , propFormula :: ForeachType JsonFormula
63+ , propRefSchema :: Traced (Referenced Schema)
64+ }
65+ deriving stock (Eq, Ord, Show)
66+
67+-- | A primitive structural condition for the "top level" of a JSON value (of a specific type)
68+data Condition :: JsonType -> Type where
69+ Exactly :: TypedValue t -> Condition t
70+ Maximum :: !(Bound Scientific) -> Condition 'Number
71+ Minimum ::
72+ !(Down (Bound (Down Scientific))) ->
73+ -- | this has the right Ord
74+ Condition 'Number
75+ MultipleOf :: !Scientific -> Condition 'Number
76+ NumberFormat :: !Format -> Condition 'Number
77+ MaxLength :: !Integer -> Condition 'String
78+ MinLength :: !Integer -> Condition 'String
79+ Pattern :: !Pattern -> Condition 'String
80+ StringFormat :: !Format -> Condition 'String
81+ Items ::
82+ !(ForeachType JsonFormula) ->
83+ !(Traced (Referenced Schema)) ->
84+ Condition 'Array
85+ TupleItems ::
86+ ![(ForeachType JsonFormula, Traced (Referenced Schema))] ->
87+ Condition 'Array
88+ MaxItems :: !Integer -> Condition 'Array
89+ MinItems :: !Integer -> Condition 'Array
90+ UniqueItems :: Condition 'Array
91+ Properties ::
92+ !(M.Map Text Property) ->
93+ -- | formula for additional properties
94+ !(ForeachType JsonFormula) ->
95+ -- | schema for additional properties, Nothing means bottom
96+ !(Maybe (Traced (Referenced Schema))) ->
97+ Condition 'Object
98+ MaxProperties :: !Integer -> Condition 'Object
99+ MinProperties :: !Integer -> Condition 'Object
100+
101+deriving stock instance Eq (Condition t)
102+
103+deriving stock instance Ord (Condition t)
104+
105+deriving stock instance Show (Condition t)
106+
107+showCondition :: Condition a -> Blocks
108+showCondition = \case
109+ (Exactly v) -> para "The value should be:" <> showJSONValue (untypeValue v)
110+ (Maximum b) -> para $ "The value should be less than " <> showBound b <> "."
111+ (Minimum (Down b)) -> para $ "The value should be more than " <> showBound (getDown <$> b) <> "."
112+ (MultipleOf n) -> para $ "The value should be a multiple of " <> show' n <> "."
113+ (NumberFormat p) -> para $ "The number should have the following format:" <> code p <> "."
114+ (Pattern p) -> para "The value should satisfy the following pattern (regular expression):" <> codeBlock p
115+ (StringFormat p) -> para $ "The string should have the following format:" <> code p <> "."
116+ (MaxLength p) -> para $ "The length of the string should be less than or equal to " <> show' p <> "."
117+ (MinLength p) -> para $ "The length of the string should be more than or equal to " <> show' p <> "."
118+ (Items i _) -> para "The items of the array should satisfy:" <> showForEachJsonFormula i
119+ (TupleItems is) -> para ("There should be " <> show' (length is) <> " items in the array:") <> bulletList (showForEachJsonFormula . fst <$> is)
120+ (MaxItems n) -> para $ "The length of the array should be less than or equal to " <> show' n <> "."
121+ (MinItems n) -> para $ "The length of the array should be more than or equal to " <> show' n <> "."
122+ UniqueItems -> para "The elements in the array should be unique."
123+ (Properties props additional _) ->
124+ bulletList $
125+ ( M.toList props
126+ <&> ( \(k, p) ->
127+ para (code k)
128+ <> para (strong $ if propRequired p then "Required" else "Optional")
129+ <> showForEachJsonFormula (propFormula p)
130+ )
131+ )
132+ <> [ para (emph "Additional properties")
133+ <> showForEachJsonFormula additional
134+ ]
135+ (MaxProperties n) -> para $ "The maximum number of fields should be " <> show' n <> "."
136+ (MinProperties n) -> para $ "The minimum number of fields should be " <> show' n <> "."
137+ where
138+ showForEachJsonFormula :: ForeachType JsonFormula -> Blocks
139+ showForEachJsonFormula i =
140+ bulletList $
141+ foldType
142+ ( \t f -> case getJsonFormula $ f i of
143+ BottomDNF -> mempty
144+ (DNF conds) ->
145+ [ para (describeJSONType t)
146+ <> bulletList
147+ ( S.toList conds <&> \case
148+ Disjunct (S.toList -> []) -> para "Empty"
149+ Disjunct (S.toList -> cond) -> bulletList (showCondition <$> cond)
150+ )
151+ ]
152+ )
153+
154+showJSONValue :: A.Value -> Blocks
155+showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
156+
157+showJSONValueInline :: A.Value -> Inlines
158+showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
159+
160+show' :: Show x => x -> Inlines
161+show' = str . T.pack . show
162+
163+satisfiesTyped :: TypedValue t -> Condition t -> Bool
164+satisfiesTyped e (Exactly e') = e == e'
165+satisfiesTyped (TNumber n) (Maximum (Exclusive m)) = n < m
166+satisfiesTyped (TNumber n) (Maximum (Inclusive m)) = n <= m
167+satisfiesTyped (TNumber n) (Minimum (Down (Exclusive (Down m)))) = n > m
168+satisfiesTyped (TNumber n) (Minimum (Down (Inclusive (Down m)))) = n >= m
169+satisfiesTyped (TNumber n) (MultipleOf m) = denominator (toRational n / toRational m) == 1 -- TODO: could be better #36
170+satisfiesTyped (TNumber n) (NumberFormat f) = checkNumberFormat f n
171+satisfiesTyped (TString s) (MaxLength m) = fromIntegral (T.length s) <= m
172+satisfiesTyped (TString s) (MinLength m) = fromIntegral (T.length s) >= m
173+satisfiesTyped (TString s) (Pattern p) = isJust $ match p s -- TODO: regex stuff #32
174+satisfiesTyped (TString s) (StringFormat f) = checkStringFormat f s
175+satisfiesTyped (TArray a) (Items f _) = all (`satisfies` f) a
176+satisfiesTyped (TArray a) (TupleItems fs) = length fs == F.length a && and (zipWith satisfies (F.toList a) (fst <$> fs))
177+satisfiesTyped (TArray a) (MaxItems m) = fromIntegral (F.length a) <= m
178+satisfiesTyped (TArray a) (MinItems m) = fromIntegral (F.length a) >= m
179+satisfiesTyped (TArray a) UniqueItems = S.size (S.fromList $ F.toList a) == F.length a -- TODO: could be better #36
180+satisfiesTyped (TObject o) (Properties props additional _) =
181+ all (`KeyMap.member` o) (map Key.fromText $ M.keys (M.filter propRequired props))
182+ && all (\(k, v) -> satisfies v $ maybe additional propFormula $ M.lookup (Key.toText k) props) (KeyMap.toList o)
183+satisfiesTyped (TObject o) (MaxProperties m) = fromIntegral (KeyMap.size o) <= m
184+satisfiesTyped (TObject o) (MinProperties m) = fromIntegral (KeyMap.size o) >= m
185+
186+checkNumberFormat :: Format -> Scientific -> Bool
187+checkNumberFormat "int32" (toRational -> n) =
188+ denominator n == 1
189+ && n >= toRational (minBound :: Int32)
190+ && n <= toRational (maxBound :: Int32)
191+checkNumberFormat "int64" (toRational -> n) =
192+ denominator n == 1
193+ && n >= toRational (minBound :: Int64)
194+ && n <= toRational (maxBound :: Int64)
195+checkNumberFormat "float" _n = True
196+checkNumberFormat "double" _n = True
197+checkNumberFormat f _n = error $ "Invalid number format: " <> T.unpack f
198+
199+checkStringFormat :: Format -> Text -> Bool
200+checkStringFormat "byte" _s = True -- TODO: regex stuff #32
201+checkStringFormat "binary" _s = True
202+checkStringFormat "date" _s = True
203+checkStringFormat "date-time" _s = True
204+checkStringFormat "password" _s = True
205+checkStringFormat "uuid" _s = True
206+checkStringFormat f _s = error $ "Invalid string format: " <> T.unpack f
207+
208+data SomeCondition where
209+ SomeCondition :: Typeable t => Condition t -> SomeCondition
210+
211+instance Eq SomeCondition where
212+ SomeCondition x == SomeCondition y = case cast x of
213+ Just x' -> x' == y
214+ Nothing -> False
215+
216+instance Ord SomeCondition where
217+ compare (SomeCondition x) (SomeCondition y) = case cast x of
218+ Just x' -> compare x' y
219+ Nothing -> compare (typeRep x) (typeRep y)
220+
221+deriving stock instance Show SomeCondition
222+
223+newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
224+ deriving stock (Eq, Ord, Show)
225+ deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
226+
227+satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
228+satisfiesFormula val = foldDNF (satisfiesTyped val) . getJsonFormula
229+
230+satisfies :: A.Value -> ForeachType JsonFormula -> Bool
231+satisfies val p = case val of
232+ A.Null -> satisfiesFormula TNull $ forNull p
233+ A.Bool b -> satisfiesFormula (TBool b) $ forBoolean p
234+ A.Number n -> satisfiesFormula (TNumber n) $ forNumber p
235+ A.String s -> satisfiesFormula (TString s) $ forString p
236+ A.Array a -> satisfiesFormula (TArray a) $ forArray p
237+ A.Object o -> satisfiesFormula (TObject o) $ forObject p
src/Data/OpenApi/Compare/Validate/Schema/Partition.hsnew file+320
@@ -0,0 +1,320 @@
1+module Data.OpenApi.Compare.Validate.Schema.Partition
2+ ( partitionSchema,
3+ partitionRefSchema,
4+ selectPartition,
5+ runPartitionM,
6+ tryPartition,
7+ showPartition,
8+ intersectSchema,
9+ intersectRefSchema,
10+ IntersectionResult (..),
11+ runIntersectionM,
12+ Partition,
13+ )
14+where
15+
16+import Algebra.Lattice
17+import Algebra.Lattice.Lifted
18+import Control.Applicative
19+import Control.Monad (when)
20+import Control.Monad.Reader hiding (ask)
21+import qualified Control.Monad.Reader as R
22+import Control.Monad.State
23+import qualified Control.Monad.Trans.Reader as R (liftCatch)
24+import qualified Control.Monad.Trans.Writer as W (liftCatch)
25+import Control.Monad.Writer
26+import qualified Data.Aeson as A
27+import Data.Foldable
28+import Data.Functor.Identity
29+import qualified Data.HashMap.Strict.InsOrd as IOHM
30+import Data.List (sortBy)
31+import qualified Data.List as L
32+import qualified Data.List.NonEmpty as NE
33+import qualified Data.Map as M
34+import Data.Maybe
35+import Data.Monoid
36+import Data.OpenApi
37+import Data.OpenApi.Compare.Memo
38+import Data.OpenApi.Compare.References
39+import Data.OpenApi.Compare.Subtree
40+import Data.OpenApi.Compare.Validate.Schema.DNF
41+import Data.OpenApi.Compare.Validate.Schema.JsonFormula
42+import Data.OpenApi.Compare.Validate.Schema.Traced
43+import Data.OpenApi.Compare.Validate.Schema.TypedJson
44+import Data.Ord
45+import qualified Data.Set as S
46+import Data.Text (Text)
47+import qualified Data.Text as T
48+import Data.Traversable (forM)
49+import Text.Pandoc.Builder hiding (Format)
50+
51+data PartitionData
52+ = DByEnumValue (DNF (S.Set A.Value))
53+ | DByProperties (DNF (S.Set Text, S.Set Text)) -- optional, required
54+ deriving stock (Eq, Ord, Show)
55+
56+conjPart :: PartitionData -> PartitionData -> Maybe PartitionData
57+conjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss /\ yss
58+conjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss /\ yss
59+conjPart _ _ = Nothing
60+
61+disjPart :: PartitionData -> PartitionData -> Maybe PartitionData
62+disjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss \/ yss
63+disjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss \/ yss
64+disjPart _ _ = Nothing
65+
66+newtype Partitions = Partitions (M.Map PartitionLocation (S.Set PartitionData))
67+ deriving stock (Eq, Ord, Show)
68+
69+instance Lattice Partitions where
70+ Partitions xss /\ Partitions yss = Partitions $ M.unionWith conj xss yss
71+ where
72+ conj xs ys = S.fromList . catMaybes $ liftA2 conjPart (S.toList xs) (S.toList ys)
73+ Partitions xss \/ Partitions yss = Partitions $ M.intersectionWith disj xss yss
74+ where
75+ disj xs ys = S.fromList . catMaybes $ liftA2 disjPart (S.toList xs) (S.toList ys)
76+
77+instance BoundedMeetSemiLattice Partitions where
78+ top = Partitions M.empty
79+
80+-- The lattice has no bottom, but we use 'Lifted' to adjoin a free bottom element
81+
82+type PartitionM = ReaderT (Traced (Definitions Schema)) (State (MemoState ()))
83+
84+ignoreKnot :: KnotTier (Lifted Partitions) () PartitionM
85+ignoreKnot =
86+ KnotTier
87+ { onKnotFound = pure ()
88+ , onKnotUsed = \_ -> pure bottom
89+ , tieKnot = \_ -> pure
90+ }
91+
92+singletonPart :: PartitionData -> Lifted Partitions
93+singletonPart = Lift . Partitions . M.singleton PHere . S.singleton
94+
95+partitionSchema :: Traced Schema -> PartitionM (Lifted Partitions)
96+partitionSchema sch = do
97+ allClauses <- case tracedAllOf sch of
98+ Nothing -> pure []
99+ Just xs -> mapM partitionRefSchema xs
100+
101+ anyClause <- case tracedAnyOf sch of
102+ Nothing -> pure top
103+ Just xs -> joins <$> mapM partitionRefSchema xs
104+
105+ oneClause <- case tracedOneOf sch of
106+ Nothing -> pure top
107+ Just xs -> joins <$> mapM partitionRefSchema xs
108+
109+ byEnumClause <- case _schemaEnum $ extract sch of
110+ Nothing -> pure top
111+ Just xs ->
112+ pure . singletonPart $
113+ DByEnumValue $ LiteralDNF (S.fromList xs)
114+
115+ -- We can only partition by presence of a property if additional properties
116+ -- are disallowed, and the property is not optional
117+ let reqd = S.fromList $ _schemaRequired $ extract sch
118+ byPropertiesClause <- case _schemaAdditionalProperties $ extract sch of
119+ Just (AdditionalPropertiesAllowed False) -> do
120+ let props = S.fromList . IOHM.keys . _schemaProperties $ extract sch
121+ pure . singletonPart $
122+ DByProperties $ LiteralDNF (props S.\\ reqd, props `S.intersection` reqd)
123+ _ -> pure top
124+
125+ -- We can partition on something nested in a property only if the property is
126+ -- required
127+ let reqdProps = IOHM.filterWithKey (\k _ -> k `S.member` reqd) $ tracedProperties sch
128+ inPropertiesClauses <- forM (IOHM.toList reqdProps) $ \(k, rs) -> do
129+ f <- partitionRefSchema rs
130+ pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
131+
132+ pure $ meets $ allClauses <> [anyClause, oneClause, byEnumClause, byPropertiesClause] <> inPropertiesClauses
133+
134+partitionRefSchema :: Traced (Referenced Schema) -> PartitionM (Lifted Partitions)
135+partitionRefSchema x = do
136+ defs <- R.ask
137+ memoWithKnot ignoreKnot (partitionSchema $ dereference defs x) (ask x)
138+
139+partitionCondition :: Condition t -> PartitionM (Lifted Partitions)
140+partitionCondition = \case
141+ Exactly x ->
142+ pure . singletonPart $
143+ DByEnumValue $ LiteralDNF (S.singleton $ untypeValue x)
144+ Properties props _ madd -> do
145+ let byProps = case madd of
146+ Just _ -> top
147+ Nothing ->
148+ singletonPart $
149+ DByProperties $
150+ LiteralDNF
151+ ( M.keysSet $ M.filter (not . propRequired) props
152+ , M.keysSet $ M.filter propRequired props
153+ )
154+ inProps <- forM (M.toList $ M.filter propRequired props) $ \(k, prop) -> do
155+ f <- partitionRefSchema $ propRefSchema prop
156+ pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
157+ pure $ byProps /\ meets inProps
158+ _ -> pure top
159+
160+runPartitionM :: Traced (Definitions Schema) -> PartitionM a -> a
161+runPartitionM defs = runIdentity . runMemo () . (`runReaderT` defs)
162+
163+partitionJsonFormulas ::
164+ ProdCons (Traced (Definitions Schema)) ->
165+ ProdCons (JsonFormula t) ->
166+ Lifted Partitions
167+partitionJsonFormulas defs pc = producer pcPart \/ consumer pcPart
168+ where
169+ pcPart = partitionFormula <$> defs <*> pc
170+ partitionFormula def (JsonFormula xss) = runPartitionM def $ forDNF partitionCondition xss
171+
172+selectPartition :: Lifted Partitions -> Maybe (PartitionLocation, S.Set PartitionChoice)
173+selectPartition Bottom = Nothing
174+selectPartition (Lift (Partitions m)) =
175+ go [(loc, part) | (loc, parts) <- sortBy (comparing $ locLength . fst) $ M.toList m, part <- S.toList parts]
176+ where
177+ locLength :: PartitionLocation -> Int
178+ locLength = walk 0
179+ where
180+ walk !n PHere = n
181+ walk !n (PInProperty _ l) = walk (n + 1) l
182+ go [] = Nothing
183+ -- Skip partitioning by property for now
184+ go ((_, DByProperties _) : ps) = go ps
185+ -- Don't partition by enum value at the root (this reports removed enum values as contradictions in their respective partitions)
186+ go ((PHere, DByEnumValue _) : ps) = go ps
187+ go ((loc, DByEnumValue (DNF xss)) : ps)
188+ -- Check that no disjunction branches are unresticted
189+ | Just enums <- traverse (\(Disjunct xs) -> fmap (foldr1 S.intersection) . NE.nonEmpty . S.toList $ xs) . S.toList $ xss =
190+ -- TODO: improve
191+ Just (loc, S.map (CByEnumValue . S.singleton) $ S.unions enums)
192+ | otherwise = go ps
193+
194+-- This essentially has 3 cases:
195+-- Nothing -- we have produced a bottom schema
196+-- Just (False, _) -- there's been no change to the schema
197+-- Just (True, x) -- x is a new schema
198+type IntersectionM = ReaderT (Traced (Definitions Schema)) (WriterT Any Maybe)
199+
200+mBottom :: IntersectionM a
201+mBottom = lift . lift $ Nothing
202+
203+catchBottom :: IntersectionM a -> IntersectionM a -> IntersectionM a
204+catchBottom act handler = R.liftCatch (W.liftCatch (\a h -> a <|> h ())) act (\_ -> handler)
205+
206+mChange :: IntersectionM ()
207+mChange = tell $ Any True
208+
209+data IntersectionResult a = Disjoint | Same a | New a
210+ deriving stock (Eq, Ord, Show)
211+
212+runIntersectionM :: Traced (Definitions Schema) -> IntersectionM a -> IntersectionResult a
213+runIntersectionM defs act = case runWriterT $ runReaderT act defs of
214+ Nothing -> Disjoint
215+ Just (x, Any False) -> Same x
216+ Just (x, Any True) -> New x
217+
218+intersectSchema ::
219+ PartitionLocation ->
220+ PartitionChoice ->
221+ Traced Schema ->
222+ IntersectionM Schema
223+intersectSchema loc part sch = do
224+ allOf' <- forM (tracedAllOf sch) $ \rss ->
225+ -- Assuming i ranges over a nonempty set (checked in processSchema)
226+ -- (⋂_i A[i]) ∩ X = ⋂_i (A[i] ∩ X)
227+ -- If any intersections are empty, the result is empty. If any intersections are a change, the result is a change.
228+ traverse (intersectRefSchema loc part) rss
229+ anyOf' <- forM (tracedAnyOf sch) $ \rss -> do
230+ -- (⋃_i A[i]) ∩ X = ⋃_i (A[i] ∩ X)
231+ -- Collect only the nonempty A[i] ∩ X, unless there are none, in which case the result is empty.
232+ -- If any schema is empty, we remove it from the list which constitutes a change.
233+ mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
234+ case catMaybes mSchemas of
235+ [] -> mBottom
236+ schs -> pure schs
237+ oneOf' <- forM (tracedOneOf sch) $ \rss -> do
238+ -- Same as anyOf'. By intersecting we're only making them more disjoint if anything.
239+ mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
240+ case catMaybes mSchemas of
241+ [] -> mBottom
242+ schs -> pure schs
243+ let sch' = (extract sch) {_schemaAllOf = allOf', _schemaAnyOf = anyOf', _schemaOneOf = oneOf'}
244+ -- Now the local changes:
245+ case loc of
246+ PInProperty k loc' -> case IOHM.lookup k $ tracedProperties sch of
247+ Nothing -> error $ "Partitioning via absent property: " <> T.unpack k
248+ Just prop -> do
249+ prop' <- intersectRefSchema loc' part prop
250+ pure $ sch' {_schemaProperties = IOHM.adjust (const prop') k $ _schemaProperties sch'}
251+ PHere -> case part of
252+ CByEnumValue vals -> do
253+ enum' <- case _schemaEnum sch' of
254+ Nothing -> do
255+ mChange
256+ pure $ S.toList vals
257+ Just xs -> do
258+ when (any (`S.notMember` vals) xs) mChange
259+ case filter (`S.member` vals) xs of
260+ [] -> mBottom
261+ xs' -> pure xs'
262+ pure $ sch' {_schemaEnum = Just enum'}
263+ CByProperties {} -> error "CByProperties not implemented"
264+
265+intersectRefSchema ::
266+ PartitionLocation ->
267+ PartitionChoice ->
268+ Traced (Referenced Schema) ->
269+ IntersectionM (Referenced Schema)
270+intersectRefSchema loc part rs = do
271+ defs <- R.ask
272+ Inline <$> intersectSchema loc part (dereference defs rs)
273+
274+intersectCondition :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> Condition t -> DNF (Condition t)
275+intersectCondition _defs PHere (CByEnumValue values) cond@(Exactly x) =
276+ if untypeValue x `S.member` values then LiteralDNF cond else bottom
277+intersectCondition defs (PInProperty k loc) part cond@(Properties props add madd) = case M.lookup k props of
278+ Nothing -> LiteralDNF cond -- shouldn't happen
279+ Just prop -> case runIntersectionM defs $ intersectRefSchema loc part $ propRefSchema prop of
280+ New rs' ->
281+ let trs' = traced (ask (propRefSchema prop) >>> step (Partitioned (loc, part))) rs'
282+ in LiteralDNF $ Properties (M.insert k prop {propRefSchema = trs'} props) add madd
283+ Same _ -> LiteralDNF cond
284+ Disjoint -> bottom
285+intersectCondition _defs _loc _part cond = LiteralDNF cond
286+
287+intersectFormula :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> JsonFormula t -> JsonFormula t
288+intersectFormula defs loc part = JsonFormula . foldDNF (intersectCondition defs loc part) . getJsonFormula
289+
290+tryPartition :: ProdCons (Traced (Definitions Schema)) -> ProdCons (JsonFormula t) -> [(Maybe Partition, ProdCons (JsonFormula t))]
291+tryPartition defs pc = case selectPartition $ partitionJsonFormulas defs pc of
292+ Nothing -> [(Nothing, pc)]
293+ Just (loc, parts) -> [(Just (loc, part), intersectFormula <$> defs <*> pure loc <*> pure part <*> pc) | part <- S.toList parts]
294+
295+showPartition :: Partition -> Inlines
296+showPartition = \case
297+ (partition, CByEnumValue (S.toList -> [v])) ->
298+ renderPartitionLocation partition <> " is " <> showJSONValueInline v
299+ (partition, CByEnumValue (S.toList -> vs)) ->
300+ renderPartitionLocation partition <> " has values: "
301+ <> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
302+ (partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
303+ renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
304+ (partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
305+ renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
306+ (partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
307+ renderPartitionLocation partition
308+ <> " contains the properties "
309+ <> listCodes incl
310+ <> " and does not contain the properties "
311+ <> listCodes excl
312+ where
313+ listCodes :: [Text] -> Inlines
314+ listCodes = fold . L.intersperse ", " . fmap code
315+ renderPartitionLocation :: PartitionLocation -> Inlines
316+ renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
317+ where
318+ renderPartitionLocation' :: PartitionLocation -> Text
319+ renderPartitionLocation' PHere = mempty
320+ renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
src/Data/OpenApi/Compare/Validate/Schema/Process.hsnew file+375
@@ -0,0 +1,375 @@
1+module Data.OpenApi.Compare.Validate.Schema.Process
2+ ( schemaToFormula,
3+ )
4+where
5+
6+import Algebra.Lattice
7+import Control.Monad.Reader hiding (ask)
8+import qualified Control.Monad.Reader as R
9+import Control.Monad.State
10+import Control.Monad.Writer
11+import qualified Data.Aeson as A
12+import Data.Functor.Identity
13+import qualified Data.HashMap.Strict.InsOrd as IOHM
14+import qualified Data.Map as M
15+import Data.Maybe
16+import Data.Monoid
17+import Data.OpenApi hiding (get)
18+import Data.OpenApi.Compare.Behavior
19+import Data.OpenApi.Compare.Memo
20+import Data.OpenApi.Compare.Paths
21+import qualified Data.OpenApi.Compare.PathsPrefixTree as P
22+import Data.OpenApi.Compare.References
23+import Data.OpenApi.Compare.Subtree
24+import Data.OpenApi.Compare.Validate.Schema.DNF
25+import Data.OpenApi.Compare.Validate.Schema.Issues
26+import Data.OpenApi.Compare.Validate.Schema.JsonFormula
27+import Data.OpenApi.Compare.Validate.Schema.Partition
28+import Data.OpenApi.Compare.Validate.Schema.Traced
29+import Data.OpenApi.Compare.Validate.Schema.TypedJson
30+import Data.Ord
31+import qualified Data.Set as S
32+import Data.Traversable (forM)
33+
34+-- | A fake writer monad that doesn't actually record anything and allows lazy recursion.
35+newtype Silent w a = Silent {runSilent :: a}
36+ deriving stock (Functor)
37+ deriving (Applicative, Monad) via Identity
38+
39+instance Monoid w => MonadWriter w (Silent w) where
40+ tell _ = Silent ()
41+ listen (Silent x) = Silent (x, mempty)
42+ pass (Silent (x, _)) = Silent x
43+
44+type ProcessM = StateT (MemoState ()) (ReaderT (Traced (Definitions Schema)) (Writer (P.PathsPrefixTree Behave AnIssue 'SchemaLevel)))
45+
46+type SilentM = StateT (MemoState ()) (ReaderT (Traced (Definitions Schema)) (Silent (P.PathsPrefixTree Behave AnIssue 'SchemaLevel)))
47+
48+-- Either ProcessM or SilentM
49+type MonadProcess m =
50+ ( MonadReader (Traced (Definitions Schema)) m
51+ , MonadWriter (P.PathsPrefixTree Behave AnIssue 'SchemaLevel) m
52+ , MonadState (MemoState ()) m
53+ )
54+
55+warn :: MonadProcess m => Issue 'SchemaLevel -> m ()
56+warn issue = tell $ P.singleton $ AnItem Root $ anIssue issue
57+
58+-- Perform a computation lazily, ignoring the warnings and discarding memoization/loop detection information.
59+lazily :: MonadProcess m => SilentM a -> m a
60+lazily m = do
61+ defs <- R.ask
62+ pure $ runSilent $ runReaderT (runMemo () m) defs
63+
64+warnKnot :: MonadProcess m => KnotTier (ForeachType JsonFormula) () m
65+warnKnot =
66+ KnotTier
67+ { onKnotFound = warn UnguardedRecursion
68+ , onKnotUsed = \_ -> pure bottom
69+ , tieKnot = \_ -> pure
70+ }
71+
72+processRefSchema ::
73+ MonadProcess m =>
74+ Traced (Referenced Schema) ->
75+ m (ForeachType JsonFormula)
76+processRefSchema x = do
77+ defs <- R.ask
78+ memoWithKnot warnKnot (processSchema $ dereference defs x) (ask x)
79+
80+-- | Turn a schema into a tuple of 'JsonFormula's that describes the condition
81+-- for every possible type of a JSON value. The conditions are independent, and
82+-- are thus checked independently.
83+processSchema ::
84+ MonadProcess m =>
85+ Traced Schema ->
86+ m (ForeachType JsonFormula)
87+processSchema sch@(extract -> Schema {..}) = do
88+ let singletonFormula :: Condition t -> JsonFormula t
89+ singletonFormula = JsonFormula . LiteralDNF
90+
91+ allClauses <- case tracedAllOf sch of
92+ Nothing -> pure []
93+ Just [] -> [] <$ warn (InvalidSchema "no items in allOf")
94+ Just xs -> mapM processRefSchema xs
95+
96+ anyClause <- case tracedAnyOf sch of
97+ Nothing -> pure top
98+ Just [] -> bottom <$ warn (InvalidSchema "no items in anyOf")
99+ Just xs -> joins <$> mapM processRefSchema xs
100+
101+ oneClause <- case tracedOneOf sch of
102+ Nothing -> pure top
103+ Just [] -> bottom <$ warn (InvalidSchema "no items in oneOf")
104+ Just xs -> do
105+ checkOneOfDisjoint xs >>= \case
106+ True -> pure ()
107+ False -> warn OneOfNotDisjoint
108+ joins <$> mapM processRefSchema xs
109+
110+ case _schemaNot of
111+ Nothing -> pure ()
112+ Just _ -> warn (NotSupported "not clause is unsupported")
113+
114+ let typeClause = case _schemaType of
115+ Nothing -> top
116+ Just OpenApiNull ->
117+ bottom
118+ { forNull = top
119+ }
120+ Just OpenApiBoolean ->
121+ bottom
122+ { forBoolean = top
123+ }
124+ Just OpenApiNumber ->
125+ bottom
126+ { forNumber = top
127+ }
128+ Just OpenApiInteger ->
129+ bottom
130+ { forNumber = singletonFormula $ MultipleOf 1
131+ }
132+ Just OpenApiString ->
133+ bottom
134+ { forString = top
135+ }
136+ Just OpenApiArray ->
137+ bottom
138+ { forArray = top
139+ }
140+ Just OpenApiObject ->
141+ bottom
142+ { forObject = top
143+ }
144+
145+ let valueEnum A.Null =
146+ bottom
147+ { forNull = singletonFormula $ Exactly TNull
148+ }
149+ valueEnum (A.Bool b) =
150+ bottom
151+ { forBoolean = singletonFormula $ Exactly $ TBool b
152+ }
153+ valueEnum (A.Number n) =
154+ bottom
155+ { forNumber = singletonFormula $ Exactly $ TNumber n
156+ }
157+ valueEnum (A.String s) =
158+ bottom
159+ { forString = singletonFormula $ Exactly $ TString s
160+ }
161+ valueEnum (A.Array a) =
162+ bottom
163+ { forArray = singletonFormula $ Exactly $ TArray a
164+ }
165+ valueEnum (A.Object o) =
166+ bottom
167+ { forObject = singletonFormula $ Exactly $ TObject o
168+ }
169+ enumClause <- case _schemaEnum of
170+ Nothing -> pure top
171+ Just [] -> bottom <$ warn (InvalidSchema "no items in enum")
172+ Just xs -> pure $ joins (valueEnum <$> xs)
173+
174+ let maximumClause = case _schemaMaximum of
175+ Nothing -> top
176+ Just n ->
177+ top
178+ { forNumber = singletonFormula $
179+ Maximum $
180+ case _schemaExclusiveMaximum of
181+ Just True -> Exclusive n
182+ _ -> Inclusive n
183+ }
184+
185+ minimumClause = case _schemaMinimum of
186+ Nothing -> top
187+ Just n ->
188+ top
189+ { forNumber = singletonFormula $
190+ Minimum $
191+ Down $
192+ case _schemaExclusiveMinimum of
193+ Just True -> Exclusive $ Down n
194+ _ -> Inclusive $ Down n
195+ }
196+
197+ multipleOfClause = case _schemaMultipleOf of
198+ Nothing -> top
199+ Just n ->
200+ top
201+ { forNumber = singletonFormula $ MultipleOf n
202+ }
203+
204+ formatClause <- case _schemaFormat of
205+ Nothing -> pure top
206+ Just f
207+ | f `elem` ["int32", "int64", "float", "double"] ->
208+ pure
209+ top
210+ { forNumber = singletonFormula $ NumberFormat f
211+ }
212+ Just f
213+ | f `elem` ["byte", "binary", "date", "date-time", "password", "uuid"] ->
214+ pure
215+ top
216+ { forString = singletonFormula $ StringFormat f
217+ }
218+ Just f -> top <$ warn (NotSupported $ "Unknown format: " <> f)
219+
220+ let maxLengthClause = case _schemaMaxLength of
221+ Nothing -> top
222+ Just n ->
223+ top
224+ { forString = singletonFormula $ MaxLength n
225+ }
226+
227+ minLengthClause = case _schemaMinLength of
228+ Nothing -> top
229+ Just n ->
230+ top
231+ { forString = singletonFormula $ MinLength n
232+ }
233+
234+ patternClause = case _schemaPattern of
235+ Nothing -> top
236+ Just p ->
237+ top
238+ { forString = singletonFormula $ Pattern p
239+ }
240+
241+ itemsClause <- case tracedItems sch of
242+ Nothing -> pure top
243+ Just (Left rs) -> do
244+ f <- lazily $ processRefSchema rs
245+ pure top {forArray = singletonFormula $ Items f rs}
246+ Just (Right rss) -> do
247+ fsrs <- forM rss $ \rs -> do
248+ f <- lazily $ processRefSchema rs
249+ pure (f, rs)
250+ pure top {forArray = singletonFormula $ TupleItems fsrs}
251+
252+ let maxItemsClause = case _schemaMaxItems of
253+ Nothing -> top
254+ Just n ->
255+ top
256+ { forArray = singletonFormula $ MaxItems n
257+ }
258+
259+ minItemsClause = case _schemaMinItems of
260+ Nothing -> top
261+ Just n ->
262+ top
263+ { forArray = singletonFormula $ MinItems n
264+ }
265+
266+ uniqueItemsClause = case _schemaUniqueItems of
267+ Just True ->
268+ top
269+ { forArray = singletonFormula UniqueItems
270+ }
271+ _ -> top
272+
273+ (addProps, addPropSchema) <- case tracedAdditionalProperties sch of
274+ Just (Right rs) -> (,Just rs) <$> lazily (processRefSchema rs)
275+ Just (Left False) -> pure (bottom, Nothing)
276+ _ -> pure (top, Just $ traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty)
277+ propList <- forM (S.toList . S.fromList $ IOHM.keys _schemaProperties <> _schemaRequired) $ \k -> do
278+ (f, psch) <- case IOHM.lookup k $ tracedProperties sch of
279+ Just rs -> (,rs) <$> lazily (processRefSchema rs)
280+ Nothing ->
281+ let fakeSchema = traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty
282+ in -- The mempty here is incorrect, but if addPropSchema was Nothing, then
283+ -- addProps is bottom, and k is in _schemaRequired. We handle this situation
284+ -- below and short-circuit the entire Properties condition to bottom
285+ pure (addProps, fromMaybe fakeSchema addPropSchema)
286+ pure (k, Property (k `elem` _schemaRequired) f psch)
287+ let allBottom f = getAll $
288+ foldType $ \_ ty -> case getJsonFormula $ ty f of
289+ BottomDNF -> All True
290+ _ -> All False
291+ allTop f = getAll $
292+ foldType $ \_ ty -> case getJsonFormula $ ty f of
293+ TopDNF -> All True
294+ _ -> All False
295+ -- remove optional fields whose schemata match that of additional props
296+ propMap = M.filter (\p -> propRequired p || propFormula p /= addProps) $ M.fromList propList
297+ propertiesClause
298+ | any (\p -> propRequired p && allBottom (propFormula p)) propMap =
299+ bottom -- if any required field has unsatisfiable schema
300+ | M.null propMap
301+ , allTop addProps =
302+ top -- if all fields are optional and have trivial schemata
303+ | otherwise =
304+ top
305+ { forObject = singletonFormula $ Properties propMap addProps addPropSchema
306+ }
307+
308+ maxPropertiesClause = case _schemaMaxProperties of
309+ Nothing -> top
310+ Just n ->
311+ top
312+ { forObject = singletonFormula $ MaxProperties n
313+ }
314+
315+ minPropertiesClause = case _schemaMinProperties of
316+ Nothing -> top
317+ Just n ->
318+ top
319+ { forObject = singletonFormula $ MinProperties n
320+ }
321+
322+ nullableClause
323+ | Just True <- _schemaNullable =
324+ bottom
325+ { forNull = singletonFormula $ Exactly TNull
326+ }
327+ | otherwise = bottom
328+
329+ pure $
330+ nullableClause
331+ \/ meets
332+ ( allClauses
333+ <> [ anyClause
334+ , oneClause
335+ , typeClause
336+ , enumClause
337+ , maximumClause
338+ , minimumClause
339+ , multipleOfClause
340+ , formatClause
341+ , maxLengthClause
342+ , minLengthClause
343+ , patternClause
344+ , itemsClause
345+ , maxItemsClause
346+ , minItemsClause
347+ , uniqueItemsClause
348+ , propertiesClause
349+ , maxPropertiesClause
350+ , minPropertiesClause
351+ ]
352+ )
353+
354+{- TODO: ReadOnly/WriteOnly #68 -}
355+
356+checkOneOfDisjoint :: MonadProcess m => [Traced (Referenced Schema)] -> m Bool
357+checkOneOfDisjoint schs = do
358+ defs <- R.ask
359+ pure $ case selectPartition $ joins $ runPartitionM defs $ traverse partitionRefSchema schs of
360+ Nothing -> False
361+ Just (loc, parts) ->
362+ let intersects part sch = case runIntersectionM defs $ intersectRefSchema loc part sch of
363+ Disjoint -> False
364+ _ -> True
365+ in all (\part -> 1 >= length (filter (intersects part) schs)) parts
366+ where
367+
368+runProcessM :: Traced (Definitions Schema) -> ProcessM a -> (a, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
369+runProcessM defs = runWriter . (`runReaderT` defs) . runMemo ()
370+
371+schemaToFormula ::
372+ Traced (Definitions Schema) ->
373+ Traced Schema ->
374+ (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
375+schemaToFormula defs rs = runProcessM defs $ processSchema rs
src/Data/OpenApi/Compare/Validate/Schema/Traced.hsnew file+115
@@ -0,0 +1,115 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Data.OpenApi.Compare.Validate.Schema.Traced
4+ ( Step (..),
5+ tracedAllOf,
6+ tracedAnyOf,
7+ tracedOneOf,
8+ tracedItems,
9+ tracedAdditionalProperties,
10+ tracedDiscriminator,
11+ tracedProperties,
12+ tracedConjunct,
13+ PartitionLocation (..),
14+ PartitionChoice (..),
15+ Partition,
16+ )
17+where
18+
19+import qualified Data.Aeson as A
20+import Data.Functor
21+import qualified Data.HashMap.Strict.InsOrd as IOHM
22+import qualified Data.List.NonEmpty as NE
23+import Data.OpenApi
24+import Data.OpenApi.Compare.Subtree
25+import qualified Data.Set as S
26+import Data.Text (Text)
27+
28+data PartitionChoice
29+ = CByEnumValue (S.Set A.Value)
30+ | CByProperties (S.Set Text) (S.Set Text) -- included, excluded
31+ deriving stock (Eq, Ord, Show)
32+
33+data PartitionLocation
34+ = PHere
35+ | PInProperty Text PartitionLocation
36+ deriving stock (Eq, Ord, Show)
37+
38+type Partition = (PartitionLocation, PartitionChoice)
39+
40+instance Steppable Schema (Referenced Schema) where
41+ data Step Schema (Referenced Schema)
42+ = AllOfStep Int
43+ | OneOfStep Int
44+ | AnyOfStep Int
45+ | ItemsObjectStep
46+ | ItemsArrayStep Int
47+ | AdditionalPropertiesStep
48+ | NotStep
49+ | ImplicitTopSchema
50+ deriving stock (Eq, Ord, Show)
51+
52+instance Steppable (Referenced Schema) (Referenced Schema) where
53+ data Step (Referenced Schema) (Referenced Schema)
54+ = -- | Invariant (for better memoization only): the "tail" of the trace is
55+ -- the "least" of the traces of the conjuncted schemata
56+ ConjunctedWith (NE.NonEmpty (Trace (Referenced Schema)))
57+ | Partitioned Partition
58+ deriving stock (Eq, Ord, Show)
59+
60+instance Steppable Schema (Definitions (Referenced Schema)) where
61+ data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
62+ deriving stock (Eq, Ord, Show)
63+
64+instance Steppable Schema Discriminator where
65+ data Step Schema Discriminator = DiscriminatorStep
66+ deriving stock (Eq, Ord, Show)
67+
68+instance Steppable Discriminator (Definitions (Referenced Schema)) where
69+ data Step Discriminator (Definitions (Referenced Schema)) = DiscriminatorMapping
70+ deriving stock (Eq, Ord, Show)
71+
72+tracedAllOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
73+tracedAllOf sch =
74+ _schemaAllOf (extract sch) <&> \xs ->
75+ [traced (ask sch >>> step (AllOfStep i)) x | (i, x) <- zip [0 ..] xs]
76+
77+tracedAnyOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
78+tracedAnyOf sch =
79+ _schemaAnyOf (extract sch) <&> \xs ->
80+ [traced (ask sch >>> step (AnyOfStep i)) x | (i, x) <- zip [0 ..] xs]
81+
82+tracedOneOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
83+tracedOneOf sch =
84+ _schemaOneOf (extract sch) <&> \xs ->
85+ [traced (ask sch >>> step (OneOfStep i)) x | (i, x) <- zip [0 ..] xs]
86+
87+tracedItems :: Traced Schema -> Maybe (Either (Traced (Referenced Schema)) [Traced (Referenced Schema)])
88+tracedItems sch =
89+ _schemaItems (extract sch) <&> \case
90+ OpenApiItemsObject x -> Left $ traced (ask sch >>> step ItemsObjectStep) x
91+ OpenApiItemsArray xs ->
92+ Right
93+ [traced (ask sch >>> step (ItemsArrayStep i)) x | (i, x) <- zip [0 ..] xs]
94+
95+tracedAdditionalProperties :: Traced Schema -> Maybe (Either Bool (Traced (Referenced Schema)))
96+tracedAdditionalProperties sch =
97+ _schemaAdditionalProperties (extract sch) <&> \case
98+ AdditionalPropertiesAllowed b -> Left b
99+ AdditionalPropertiesSchema x -> Right $ traced (ask sch >>> step AdditionalPropertiesStep) x
100+
101+tracedDiscriminator :: Traced Schema -> Maybe (Traced Discriminator)
102+tracedDiscriminator = sequence . stepTraced DiscriminatorStep . fmap _schemaDiscriminator
103+
104+tracedProperties :: Traced Schema -> IOHM.InsOrdHashMap Text (Traced (Referenced Schema))
105+tracedProperties sch =
106+ IOHM.mapWithKey
107+ (\k -> traced (ask sch >>> step PropertiesStep >>> step (InsOrdHashMapKeyStep k)))
108+ (_schemaProperties $ extract sch)
109+
110+tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
111+tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
112+ (rs NE.:| []) -> rs
113+ (rs1 NE.:| rs2 : rss) ->
114+ traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
115+ Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
src/Data/OpenApi/Compare/Validate/Schema/TypedJson.hsnew file+120
@@ -0,0 +1,120 @@
1+module Data.OpenApi.Compare.Validate.Schema.TypedJson
2+ ( JsonType (..),
3+ describeJSONType,
4+ TypedValue (..),
5+ untypeValue,
6+ ForeachType (..),
7+ foldType,
8+ forType_,
9+ )
10+where
11+
12+import Algebra.Lattice
13+import qualified Data.Aeson as A
14+import Data.Kind
15+import Data.Monoid
16+import Data.Scientific
17+import Data.String
18+import Data.Text (Text)
19+import Data.Typeable
20+
21+-- | Type of a JSON value
22+data JsonType
23+ = Null
24+ | Boolean
25+ | Number
26+ | String
27+ | Array
28+ | Object
29+ deriving stock (Eq, Ord, Show)
30+
31+describeJSONType :: IsString s => JsonType -> s
32+describeJSONType = \case
33+ Null -> "Null"
34+ Boolean -> "Boolean"
35+ Number -> "Number"
36+ String -> "String"
37+ Array -> "Array"
38+ Object -> "Object"
39+
40+-- | A 'A.Value' whose type we know
41+data TypedValue :: JsonType -> Type where
42+ TNull :: TypedValue 'Null
43+ TBool :: !Bool -> TypedValue 'Boolean
44+ TNumber :: !Scientific -> TypedValue 'Number
45+ TString :: !Text -> TypedValue 'String
46+ TArray :: !A.Array -> TypedValue 'Array
47+ TObject :: !A.Object -> TypedValue 'Object
48+
49+deriving stock instance Eq (TypedValue t)
50+
51+deriving stock instance Ord (TypedValue t)
52+
53+deriving stock instance Show (TypedValue t)
54+
55+untypeValue :: TypedValue t -> A.Value
56+untypeValue TNull = A.Null
57+untypeValue (TBool b) = A.Bool b
58+untypeValue (TNumber n) = A.Number n
59+untypeValue (TString s) = A.String s
60+untypeValue (TArray a) = A.Array a
61+untypeValue (TObject o) = A.Object o
62+
63+data ForeachType (f :: JsonType -> Type) = ForeachType
64+ { forNull :: f 'Null
65+ , forBoolean :: f 'Boolean
66+ , forNumber :: f 'Number
67+ , forString :: f 'String
68+ , forArray :: f 'Array
69+ , forObject :: f 'Object
70+ }
71+
72+deriving stock instance (forall x. Typeable x => Eq (f x)) => Eq (ForeachType f)
73+
74+deriving stock instance (forall x. Typeable x => Ord (f x)) => Ord (ForeachType f)
75+
76+deriving stock instance (forall x. Typeable x => Show (f x)) => Show (ForeachType f)
77+
78+foldType :: Monoid m => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m) -> m
79+foldType k =
80+ k Null forNull
81+ <> k Boolean forBoolean
82+ <> k Number forNumber
83+ <> k String forString
84+ <> k Array forArray
85+ <> k Object forObject
86+
87+forType_ :: Applicative m => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m ()) -> m ()
88+forType_ k = getAp $ foldType (\ty proj -> Ap $ k ty proj)
89+
90+broadcastType :: (forall x. Typeable x => f x) -> ForeachType f
91+broadcastType k =
92+ ForeachType
93+ { forNull = k
94+ , forBoolean = k
95+ , forNumber = k
96+ , forString = k
97+ , forArray = k
98+ , forObject = k
99+ }
100+
101+zipType :: (forall x. Typeable x => f x -> g x -> h x) -> ForeachType f -> ForeachType g -> ForeachType h
102+zipType k f1 f2 =
103+ ForeachType
104+ { forNull = k (forNull f1) (forNull f2)
105+ , forBoolean = k (forBoolean f1) (forBoolean f2)
106+ , forNumber = k (forNumber f1) (forNumber f2)
107+ , forString = k (forString f1) (forString f2)
108+ , forArray = k (forArray f1) (forArray f2)
109+ , forObject = k (forObject f1) (forObject f2)
110+ }
111+
112+instance (forall x. Lattice (f x)) => Lattice (ForeachType f) where
113+ (\/) = zipType (\/)
114+ (/\) = zipType (/\)
115+
116+instance (forall x. BoundedJoinSemiLattice (f x)) => BoundedJoinSemiLattice (ForeachType f) where
117+ bottom = broadcastType bottom
118+
119+instance (forall x. BoundedMeetSemiLattice (f x)) => BoundedMeetSemiLattice (ForeachType f) where
120+ top = broadcastType top
src/OpenAPI/Checker/Validate/SecurityRequirement.hs → src/Data/OpenApi/Compare/Validate/SecurityRequirement.hsrenamed+14−13
@@ -1,8 +1,8 @@
1{-# LANGUAGE PartialTypeSignatures #-}
1{-# LANGUAGE PartialTypeSignatures #-}
2{-# OPTIONS_GHC -Wno-orphans #-}
2{-# OPTIONS_GHC -Wno-orphans #-}
3
3
4−module OpenAPI.Checker.Validate.SecurityRequirement
4+module Data.OpenApi.Compare.Validate.SecurityRequirement
5− ( Issue (..)
5+ ( Issue (..),
6 )
6 )
7where
7where
8
8
@@ -17,14 +17,14 @@ import Data.HList
17import qualified Data.HashMap.Strict.InsOrd as IOHM
17import qualified Data.HashMap.Strict.InsOrd as IOHM
18import qualified Data.List.NonEmpty as NE
18import qualified Data.List.NonEmpty as NE
19import Data.OpenApi
19import Data.OpenApi
20+import Data.OpenApi.Compare.Behavior
21+import Data.OpenApi.Compare.Subtree
22+import Data.OpenApi.Compare.Validate.OAuth2Flows
23+import Data.OpenApi.Compare.Validate.SecurityScheme ()
20import Data.Set (Set)
24import Data.Set (Set)
21import qualified Data.Set as S
25import qualified Data.Set as S
22import Data.Text (Text)
26import Data.Text (Text)
23import Data.Traversable
27import Data.Traversable
24−import OpenAPI.Checker.Behavior
25−import OpenAPI.Checker.Subtree
26−import OpenAPI.Checker.Validate.OAuth2Flows
27−import OpenAPI.Checker.Validate.SecurityScheme ()
28
28
29instance Subtree SecurityRequirement where
29instance Subtree SecurityRequirement where
30 type SubtreeLevel SecurityRequirement = 'SecurityRequirementLevel
30 type SubtreeLevel SecurityRequirement = 'SecurityRequirementLevel
@@ -41,18 +41,19 @@ instance Subtree SecurityRequirement where
41 for (IOHM.toList $ getSecurityRequirement sec) $ \(key, scopes) ->
41 for (IOHM.toList $ getSecurityRequirement sec) $ \(key, scopes) ->
42 (,scopes) <$> lookupScheme key defs
42 (,scopes) <$> lookupScheme key defs
43 structuralMaybeWith
43 structuralMaybeWith
44− (\pc' -> do
44+ ( \pc' -> do
45− let ProdCons pScopes cScopes = fmap snd <$> pc'
45+ let ProdCons pScopes cScopes = fmap snd <$> pc'
46− unless (pScopes == cScopes) structuralIssue
46+ unless (pScopes == cScopes) structuralIssue
47− structuralList env $ fmap fst <$> pc'
47+ structuralList env $ fmap fst <$> pc'
48− pure ())
48+ pure ()
49+ )
49 normalized
50 normalized
50 pure ()
51 pure ()
51 checkSemanticCompatibility env bhv' pc = do
52 checkSemanticCompatibility env bhv' pc = do
52 let schemes = getH @(ProdCons (Traced (Definitions SecurityScheme))) env
53 let schemes = getH @(ProdCons (Traced (Definitions SecurityScheme))) env
53 ( ProdCons pErrs cErrs
54 ( ProdCons pErrs cErrs
54− , (ProdCons pSchemes cSchemes)
55+ , (ProdCons pSchemes cSchemes) ::
55− :: ProdCons [(Behavior 'SecuritySchemeLevel, Traced SecurityScheme, [Text])]
56+ ProdCons [(Behavior 'SecuritySchemeLevel, Traced SecurityScheme, [Text])]
56 ) =
57 ) =
57 NE.unzip $
58 NE.unzip $
58 partitionEithers <$> do
59 partitionEithers <$> do
src/OpenAPI/Checker/Validate/SecurityScheme.hs → src/Data/OpenApi/Compare/Validate/SecurityScheme.hsrenamed+5−5
@@ -1,16 +1,16 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.SecurityScheme
3+module Data.OpenApi.Compare.Validate.SecurityScheme
4 (
4 (
5 )
5 )
6where
6where
7
7
8import Control.Monad
8import Control.Monad
9import Data.OpenApi
9import Data.OpenApi
10−import OpenAPI.Checker.Behavior
10+import Data.OpenApi.Compare.Behavior
11−import OpenAPI.Checker.Orphans ()
11+import Data.OpenApi.Compare.Orphans ()
12−import OpenAPI.Checker.Subtree
12+import Data.OpenApi.Compare.Subtree
13−import OpenAPI.Checker.Validate.OAuth2Flows
13+import Data.OpenApi.Compare.Validate.OAuth2Flows
14
14
15instance Subtree SecurityScheme where
15instance Subtree SecurityScheme where
16 type CheckEnv SecurityScheme = '[]
16 type CheckEnv SecurityScheme = '[]
src/OpenAPI/Checker/Validate/Server.hs → src/Data/OpenApi/Compare/Validate/Server.hsrenamed+21−18
@@ -1,7 +1,7 @@
1{-# OPTIONS_GHC -Wno-orphans #-}
1{-# OPTIONS_GHC -Wno-orphans #-}
2
2
3−module OpenAPI.Checker.Validate.Server
3+module Data.OpenApi.Compare.Validate.Server
4− ( Issue (..)
4+ ( Issue (..),
5 )
5 )
6where
6where
7
7
@@ -19,21 +19,21 @@ import qualified Data.HashSet.InsOrd as IOHM
19import qualified Data.HashSet.InsOrd as IOHS
19import qualified Data.HashSet.InsOrd as IOHS
20import Data.Maybe
20import Data.Maybe
21import Data.OpenApi
21import Data.OpenApi
22+import Data.OpenApi.Compare.Behavior
23+import Data.OpenApi.Compare.Common
24+import Data.OpenApi.Compare.Paths
25+import Data.OpenApi.Compare.Subtree
26+import Data.OpenApi.Compare.Validate.MediaTypeObject
22import qualified Data.Set as S
27import qualified Data.Set as S
23import Data.Text (Text)
28import Data.Text (Text)
24import qualified Data.Text as T
29import qualified Data.Text as T
25import Data.Traversable
30import Data.Traversable
26−import OpenAPI.Checker.Behavior
27−import OpenAPI.Checker.Common
28−import OpenAPI.Checker.Paths
29−import OpenAPI.Checker.Subtree
30−import OpenAPI.Checker.Validate.MediaTypeObject
31import Text.Pandoc.Builder
31import Text.Pandoc.Builder
32import Prelude as P
32import Prelude as P
33
33
34−tracedParsedServerUrlParts
34+tracedParsedServerUrlParts ::
35− :: Server
35+ Server ->
36− -> Either (Issue 'ServerLevel) ProcessedServer
36+ Either (Issue 'ServerLevel) ProcessedServer
37tracedParsedServerUrlParts s =
37tracedParsedServerUrlParts s =
38 let parsedUrl = parseServerUrl $ _serverUrl s
38 let parsedUrl = parseServerUrl $ _serverUrl s
39 lookupVar var = case IOHM.lookup var (_serverVariables s) of
39 lookupVar var = case IOHM.lookup var (_serverVariables s) of
@@ -46,7 +46,7 @@ instance Behavable 'OperationLevel 'ServerLevel where
46 = InServer Text
46 = InServer Text
47 deriving stock (Eq, Ord, Show)
47 deriving stock (Eq, Ord, Show)
48
48
49− describeBehaviour (InServer n) = "Server " <> code n
49+ describeBehavior (InServer n) = "Server " <> code n
50
50
51instance Subtree [Server] where
51instance Subtree [Server] where
52 type SubtreeLevel [Server] = 'OperationLevel
52 type SubtreeLevel [Server] = 'OperationLevel
@@ -63,11 +63,12 @@ instance Subtree [Server] where
63 pcServer
63 pcServer
64 <&> partitionEithers
64 <&> partitionEithers
65 . fmap
65 . fmap
66− (\(Traced t s) ->
66+ ( \(Traced t s) ->
67− let bhv = beh >>> step (InServer $ _serverUrl s)
67+ let bhv = beh >>> step (InServer $ _serverUrl s)
68− in case tracedParsedServerUrlParts s of
68+ in case tracedParsedServerUrlParts s of
69− Left e -> Left $ issueAt bhv e
69+ Left e -> Left $ issueAt bhv e
70− Right u -> Right (bhv, Traced (t >>> step (ServerStep $ _serverUrl s)) u))
70+ Right u -> Right (bhv, Traced (t >>> step (ServerStep $ _serverUrl s)) u)
71+ )
71 . sequence
72 . sequence
72 sequenceA_ pErrs
73 sequenceA_ pErrs
73 sequenceA_ cErrs
74 sequenceA_ cErrs
@@ -101,7 +102,7 @@ data ServerUrlPart var
101 deriving stock (Eq, Show, Functor, Foldable, Traversable)
102 deriving stock (Eq, Show, Functor, Foldable, Traversable)
102
103
103-- | This is super rough. Things like @{a|b}c@ will not match @ac@.
104-- | This is super rough. Things like @{a|b}c@ will not match @ac@.
104−-- FIXME: https://github.com/typeable/openapi-diff/issues/46
105+-- FIXME: #46
105--
106--
106-- NOTE: syntax is defined vaguely in the spec.
107-- NOTE: syntax is defined vaguely in the spec.
107parseServerUrl :: Text -> [ServerUrlPart Text]
108parseServerUrl :: Text -> [ServerUrlPart Text]
@@ -133,7 +134,9 @@ instance Issuable 'ServerLevel where
133 | ServerVariableNotDefined Text
134 | ServerVariableNotDefined Text
134 | ServerNotMatched
135 | ServerNotMatched
135 deriving stock (Eq, Ord, Show)
136 deriving stock (Eq, Ord, Show)
136− issueIsUnsupported _ = False
137+ issueKind = \case
138+ ServerVariableNotDefined _ -> SchemaInvalid
139+ _ -> CertainIssue
137 describeIssue Forward (EnumValueNotConsumed _ v) =
140 describeIssue Forward (EnumValueNotConsumed _ v) =
138 para $ "Enum value " <> code v <> " has been removed."
141 para $ "Enum value " <> code v <> " has been removed."
139 describeIssue Backward (EnumValueNotConsumed _ v) =
142 describeIssue Backward (EnumValueNotConsumed _ v) =
src/Data/OpenApi/Compare/Validate/Sums.hsnew file+25
@@ -0,0 +1,25 @@
1+module Data.OpenApi.Compare.Validate.Sums
2+ ( checkSums,
3+ )
4+where
5+
6+import Data.Foldable
7+import Data.Map.Strict (Map)
8+import qualified Data.Map.Strict as M
9+import Data.OpenApi.Compare.Behavior
10+import Data.OpenApi.Compare.Paths
11+import Data.OpenApi.Compare.Subtree
12+
13+checkSums ::
14+ (Ord k, Issuable l) =>
15+ Paths q r l ->
16+ (k -> Issue l) ->
17+ (k -> ProdCons t -> CompatFormula' q AnIssue r ()) ->
18+ ProdCons (Map k t) ->
19+ CompatFormula' q AnIssue r ()
20+checkSums xs noElt check (ProdCons p c) = for_ (M.toList p) $ \(key, prodElt) ->
21+ case M.lookup key c of
22+ Nothing -> issueAt xs $ noElt key
23+ Just consElt ->
24+ let sumElts = ProdCons prodElt consElt
25+ in check key sumElts
src/Data/OpenUnion/Extra.hs+5−5
@@ -1,7 +1,7 @@
1module Data.OpenUnion.Extra
1module Data.OpenUnion.Extra
2− ( (@@>)
2+ ( (@@>),
3− , TryLiftUnion (..)
3+ TryLiftUnion (..),
4− , pattern SingletonUnion
4+ pattern SingletonUnion,
5 )
5 )
6where
6where
7
7
@@ -18,8 +18,8 @@ instance TryLiftUnion '[] where
18 tryLiftUnion _ = empty
18 tryLiftUnion _ = empty
19
19
20instance
20instance
21− (Typeable y, SubList ys (y : ys), TryLiftUnion ys)
21+ (Typeable y, SubList ys (y : ys), TryLiftUnion ys) =>
22− => TryLiftUnion (y ': ys)
22+ TryLiftUnion (y ': ys)
23 where
23 where
24 tryLiftUnion (x :: x) = case eqT @x @y of
24 tryLiftUnion (x :: x) = case eqT @x @y of
25 Nothing -> reUnion <$> tryLiftUnion @ys x
25 Nothing -> reUnion <$> tryLiftUnion @ys x
src/OpenAPI/Checker/Memo.hsdeleted−126
@@ -1,126 +0,0 @@
1−{-# LANGUAGE AllowAmbiguousTypes #-}
2−-- | Utilities for effectfully memoizing other, more effectful functions.
3−module OpenAPI.Checker.Memo
4− ( MonadMemo
5− , MemoState
6− , runMemo
7− , modifyMemoNonce
8− , KnotTier(..)
9− , unknot
10− , memoWithKnot
11− , memoTaggedWithKnot
12− ) where
13−
14−import Control.Monad.State
15−import Data.Dynamic
16−import qualified Data.Map as M
17−import Data.Tagged
18−import Data.Void
19−import qualified Data.TypeRepMap as T
20−import Type.Reflection
21−
22−data Progress a = Finished a | Started | TyingKnot Dynamic
23−
24−data MemoMap a where
25− MemoMap :: !(M.Map k (Progress v)) -> MemoMap (k, v)
26−
27−data MemoState s = MemoState s (T.TypeRepMap MemoMap)
28−
29−-- | An effectful memoization monad.
30−type MonadMemo s m = MonadState (MemoState s) m
31−
32−memoStateLookup
33− :: forall k v s. (Typeable k, Typeable v, Ord k)
34− => k -> MemoState s -> Maybe (Progress v)
35−memoStateLookup k (MemoState _ t) = case T.lookup @(k, v) t of
36− Just (MemoMap m) -> M.lookup k m
37− Nothing -> Nothing
38−
39−memoStateInsert
40− :: forall k v s. (Typeable k, Typeable v, Ord k)
41− => k -> Progress v -> MemoState s -> MemoState s
42−memoStateInsert k x (MemoState s t) = MemoState s $ T.insert (MemoMap m'') t
43− where
44− m'' = M.insert k x m'
45− m' = case T.lookup @(k, v) t of
46− Just (MemoMap m) -> m
47− Nothing -> M.empty
48−
49−modifyMemoNonce :: MonadMemo s m => (s -> s) -> m s
50−modifyMemoNonce f = do
51− MemoState s t <- get
52− put $ MemoState (f s) t
53− pure s
54−
55−-- | Run a memoized computation.
56−runMemo :: Monad m => s -> StateT (MemoState s) m a -> m a
57−runMemo s = (`evalStateT` MemoState s T.empty)
58−
59−-- | A description of how to effectfully tie knots in type @v@, using the @m@
60−-- monad, and by sharing some @d@ data among the recursive instances.
61−data KnotTier v d m = KnotTier
62− { onKnotFound :: m d -- ^ Create some data that will be connected to this knot
63− , onKnotUsed :: d -> m v -- ^ This is what the knot will look like as a value
64− -- to the inner computations
65− , tieKnot :: d -> v -> m v -- ^ Once we're done and we're outside, tie the
66− -- knot using the datum
67− }
68−
69−unknot :: KnotTier v Void m
70−unknot = KnotTier
71− { onKnotFound = error "Recursion detected"
72− , onKnotUsed = absurd
73− , tieKnot = absurd
74− }
75−
76−-- | Run a potentially recursive computation. The provided key will be used to
77−-- refer to the result of this computation. If during the computation, another
78−-- attempt to run the computation with the same key is made, we run a
79−-- tying-the-knot procedure.
80−--
81−-- If another attempt to run the computation with the same key is made
82−-- *after we're done*, we will return the memoized value.
83−memoWithKnot
84− :: forall k v d m s.
85− (Typeable k, Typeable v, Typeable d, Ord k, MonadMemo s m)
86− => KnotTier v d m
87− -> m v -- ^ the computation to memoize
88− -> k -- ^ key for memoization
89− -> m v
90−memoWithKnot tier f k = memoStateLookup @k @v k <$> get >>= \case
91− Just (Finished v) -> pure v
92− Just Started -> do
93− d <- onKnotFound tier
94− modify $ memoStateInsert @k @v k (TyingKnot $ toDyn d)
95− onKnotUsed tier d
96− Just (TyingKnot dyn) -> case fromDynamic dyn of
97− Just d -> onKnotUsed tier d
98− Nothing -> error $ "Type mismatch when examining the knot of "
99− <> show (typeRep @(k -> v)) <> ": expected " <> show (typeRep @d)
100− <> ", got " <> show (dynTypeRep dyn)
101− Nothing -> do
102− modify $ memoStateInsert @k @v k Started
103− v <- f
104− v' <- memoStateLookup @k @v k <$> get >>= \case
105− Just Started -> pure v
106− Just (TyingKnot dyn) -> case fromDynamic dyn of
107− Just d -> tieKnot tier d v
108− Nothing -> error $ "Type mismatch when tying the knot of "
109− <> show (typeRep @(k -> v)) <> ": expected " <> show (typeRep @d)
110− <> ", got " <> show (dynTypeRep dyn)
111− Just (Finished _) -> error $ "Unexpected Finished when memoizing "
112− <> show (typeRep @(k -> v))
113− Nothing -> pure v
114− -- Normally this would be an error, but the underlying monad can refuse
115− -- to remember memoization state
116− modify $ memoStateInsert @k @v k (Finished v')
117− pure v'
118−
119−-- | Disambiguate memoized computations with an arbitrary tag.
120−memoTaggedWithKnot
121− :: forall t k v d m s.
122− ( Typeable t, Typeable k, Typeable v, Typeable d
123− , Ord k, MonadMemo s m )
124− => KnotTier v d m -> m v -> k -> m v
125−memoTaggedWithKnot tier f k = withTypeable (typeRepKind $ typeRep @t) $
126− memoWithKnot tier f (Tagged @t k)
src/OpenAPI/Checker/Run.hsdeleted−30
@@ -1,30 +0,0 @@
1−module OpenAPI.Checker.Run
2− ( runChecker
3− , runReport
4− , module OpenAPI.Checker.Report
5− )
6−where
7−
8−import Data.HList
9−import Data.OpenApi (OpenApi)
10−import OpenAPI.Checker.Paths
11−import OpenAPI.Checker.Report
12−import OpenAPI.Checker.Subtree
13−import OpenAPI.Checker.Validate.OpenApi ()
14−
15−runChecker :: (OpenApi, OpenApi) -> ReportInput
16−runChecker (client, server) =
17− ReportInput
18− { breakingChanges = run client server
19− , nonBreakingChanges = run server client
20− }
21− where
22− toPC p c =
23− ProdCons
24− { producer = traced (step ClientSchema) p
25− , consumer = traced (step ServerSchema) c
26− }
27− run p c = either id mempty . runCompatFormula . checkCompatibility Root HNil $ toPC p c
28−
29−runReport :: ReportConfig -> (OpenApi, OpenApi) -> (Pandoc, ReportStatus)
30−runReport cfg = generateReport cfg . runChecker
src/OpenAPI/Checker/Validate/Products.hsdeleted−49
@@ -1,49 +0,0 @@
1−{- | Checks product-like entities. The key is some identificator for the product
2−element. Each element may be required or optional.
3−
4−One example of product is request parameters. There are optional and required
5−parameters. The client and server have possibly different set of
6−parameters. What we must check is if server requires some request parameter,
7−then this parameter must be presented by client and their schemas must match.
8−
9−So when we checking products we are checking from the server's (consumer)
10−perspective, ensuring that all parameters are provided by the client (producer)
11−and their schemas match.
12−
13−This module abstracts this logic for arbitrary elements -}
14−
15−module OpenAPI.Checker.Validate.Products
16− ( checkProducts
17− , ProductLike(..)
18− ) where
19−
20−import Data.Foldable
21−import Data.Map.Strict (Map)
22−import qualified Data.Map.Strict as M
23−import OpenAPI.Checker.Behavior
24−import OpenAPI.Checker.Paths
25−import OpenAPI.Checker.Subtree
26−
27−-- | Some entity which is product-like
28−data ProductLike a = ProductLike
29− { productValue :: a
30− , required :: Bool
31− }
32−
33−checkProducts
34− :: (Ord k, Issuable l)
35− => Paths q r l
36− -> (k -> Issue l)
37− -- ^ No required element found
38− -> (k -> ProdCons t -> CompatFormula' q AnIssue r ())
39− -> ProdCons (Map k (ProductLike t))
40− -> CompatFormula' q AnIssue r ()
41−checkProducts xs noElt check (ProdCons p c) = for_ (M.toList c) $ \(key, consElt) ->
42− case M.lookup key p of
43− Nothing -> case required consElt of
44− True -> issueAt xs $ noElt key
45− False -> pure ()
46− Just prodElt -> do
47− let
48− elts = ProdCons prodElt consElt
49− check key (productValue <$> elts)
src/OpenAPI/Checker/Validate/Schema.hsdeleted−1627
@@ -1,1627 +0,0 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−
3−module OpenAPI.Checker.Validate.Schema
4− ( JsonType (..)
5− , ForeachType (..)
6− , foldType
7− , forType_
8− , TypedValue (..)
9− , untypeValue
10− , Bound (..)
11− , schemaToFormula
12− , foldLattice
13− , Behave (..)
14− , describeJSONType
15− )
16−where
17−
18−import Algebra.Lattice
19−import Algebra.Lattice.Lifted
20−import Control.Applicative
21−import Control.Arrow
22−import Control.Comonad.Env hiding (env)
23−import Control.Monad.Reader hiding (ask)
24−import qualified Control.Monad.Reader as R
25−import Control.Monad.State
26−import qualified Control.Monad.Trans.Reader as R (liftCatch)
27−import qualified Control.Monad.Trans.Writer as W (liftCatch)
28−import Control.Monad.Writer
29−import qualified Data.Aeson as A
30−import qualified Data.ByteString.Lazy as BSL
31−import Data.Coerce
32−import Data.Foldable
33−import qualified Data.Foldable as F
34−import Data.Functor
35−import Data.Functor.Identity
36−import Data.HList
37−import qualified Data.HashMap.Strict as HM
38−import qualified Data.HashMap.Strict.InsOrd as IOHM
39−import Data.Int
40−import Data.Kind
41−import Data.List (sortBy)
42−import qualified Data.List as L
43−import qualified Data.List.NonEmpty as NE
44−import qualified Data.Map as M
45−import Data.Maybe
46−import Data.OpenApi hiding (get)
47−import Data.Ord
48−import Data.Ratio
49−import Data.Scientific
50−import qualified Data.Set as S
51−import Data.String
52−import Data.Text (Text)
53−import qualified Data.Text as T hiding (singleton)
54−import qualified Data.Text.Encoding as T
55−import Data.Typeable
56−import OpenAPI.Checker.Behavior
57−import OpenAPI.Checker.Memo
58−import OpenAPI.Checker.Orphans ()
59−import OpenAPI.Checker.Paths
60−import qualified OpenAPI.Checker.PathsPrefixTree as P
61−import OpenAPI.Checker.References
62−import OpenAPI.Checker.Subtree
63−import Text.Pandoc.Builder hiding (Format, Null)
64−import Text.Regex.Pcre2
65−
66−-- | Type of a JSON value
67−data JsonType
68− = Null
69− | Boolean
70− | Number
71− | String
72− | Array
73− | Object
74− deriving stock (Eq, Ord, Show)
75−
76−-- | A 'A.Value' whose type we know
77−data TypedValue :: JsonType -> Type where
78− TNull :: TypedValue 'Null
79− TBool :: !Bool -> TypedValue 'Boolean
80− TNumber :: !Scientific -> TypedValue 'Number
81− TString :: !Text -> TypedValue 'String
82− TArray :: !A.Array -> TypedValue 'Array
83− TObject :: !A.Object -> TypedValue 'Object
84−
85−deriving stock instance Eq (TypedValue t)
86−
87−deriving stock instance Ord (TypedValue t)
88−
89−deriving stock instance Show (TypedValue t)
90−
91−untypeValue :: TypedValue t -> A.Value
92−untypeValue TNull = A.Null
93−untypeValue (TBool b) = A.Bool b
94−untypeValue (TNumber n) = A.Number n
95−untypeValue (TString s) = A.String s
96−untypeValue (TArray a) = A.Array a
97−untypeValue (TObject o) = A.Object o
98−
99−data Bound a = Exclusive !a | Inclusive !a
100− deriving stock (Eq, Show, Functor)
101−
102−-- | The order is lexicographical on @a * Bool@.
103−instance Ord a => Ord (Bound a) where
104− Exclusive a `compare` Exclusive b = compare a b
105− Exclusive a `compare` Inclusive b = if a <= b then LT else GT
106− Inclusive a `compare` Exclusive b = if a < b then LT else GT
107− Inclusive a `compare` Inclusive b = compare a b
108−
109−data Property = Property
110− { propRequired :: Bool
111− , propFormula :: ForeachType JsonFormula
112− , propRefSchema :: Traced (Referenced Schema)
113− }
114− deriving stock (Eq, Ord, Show)
115−
116−-- | A primitive structural condition for the "top level" of a JSON value (of a specific type)
117−data Condition :: JsonType -> Type where
118− Exactly :: TypedValue t -> Condition t
119− Maximum :: !(Bound Scientific) -> Condition 'Number
120− Minimum
121− :: !(Down (Bound (Down Scientific)))
122− -> Condition 'Number -- ^ this has the right Ord
123− MultipleOf :: !Scientific -> Condition 'Number
124− NumberFormat :: !Format -> Condition 'Number
125− MaxLength :: !Integer -> Condition 'String
126− MinLength :: !Integer -> Condition 'String
127− Pattern :: !Pattern -> Condition 'String
128− StringFormat :: !Format -> Condition 'String
129− Items
130− :: !(ForeachType JsonFormula)
131− -> !(Traced (Referenced Schema))
132− -> Condition 'Array
133− MaxItems :: !Integer -> Condition 'Array
134− MinItems :: !Integer -> Condition 'Array
135− UniqueItems :: Condition 'Array
136− Properties
137− :: !(M.Map Text Property)
138− -> !(ForeachType JsonFormula) -- ^ formula for additional properties
139− -> !(Maybe (Traced (Referenced Schema))) -- ^ schema for additional properties, Nothing means bottom
140− -> Condition 'Object
141− MaxProperties :: !Integer -> Condition 'Object
142− MinProperties :: !Integer -> Condition 'Object
143−
144−showCondition :: Condition a -> Blocks
145−showCondition = \case
146− (Exactly v) -> para "The value should be:" <> showJSONValue (untypeValue v)
147− (Maximum b) -> para $ "The value should be less than " <> showBound b <> "."
148− (Minimum (Down b)) -> para $ "The value should be more than " <> showBound (getDown <$> b) <> "."
149− (MultipleOf n) -> para $ "The value should be a multiple of " <> show' n <> "."
150− (NumberFormat p) -> para $ "The number should have the following format:" <> code p <> "."
151− (Pattern p) -> para "The value should satisfy the following pattern (regular expression):" <> codeBlock p
152− (StringFormat p) -> para $ "The string should have the following format:" <> code p <> "."
153− (MaxLength p) -> para $ "The length of the string should be less than or equal to " <> show' p <> "."
154− (MinLength p) -> para $ "The length of the string should be more than or equal to " <> show' p <> "."
155− (Items i _) -> para "The items of the array should satisfy:" <> showForEachJsonFormula i
156− (MaxItems n) -> para $ "The length of the array should be less than or equal to " <> show' n <> "."
157− (MinItems n) -> para $ "The length of the array should be more than or equal to " <> show' n <> "."
158− UniqueItems -> para "The elements in the array should be unique."
159− (Properties props additional _) ->
160− bulletList $
161− (M.toList props
162− <&> (\(k, p) ->
163− para (code k)
164− <> para (strong $ if propRequired p then "Required" else "Optional")
165− <> showForEachJsonFormula (propFormula p)))
166− <> [ para (emph "Additional properties")
167− <> showForEachJsonFormula additional
168− ]
169− (MaxProperties n) -> para $ "The maximum number of fields should be " <> show' n <> "."
170− (MinProperties n) -> para $ "The minimum number of fields should be " <> show' n <> "."
171− where
172− showForEachJsonFormula :: ForeachType JsonFormula -> Blocks
173− showForEachJsonFormula i =
174− bulletList $
175− foldType
176− (\t f -> case getJsonFormula $ f i of
177− BottomFormula -> mempty
178− (DNF conds') ->
179− let conds = S.toList <$> S.toList conds'
180− in [ para (describeJSONType t)
181− <> bulletList
182− (conds <&> \case
183− [] -> para "Empty"
184− cond -> bulletList (showCondition <$> cond))
185− ])
186−
187−satisfiesTyped :: TypedValue t -> Condition t -> Bool
188−satisfiesTyped e (Exactly e') = e == e'
189−satisfiesTyped (TNumber n) (Maximum (Exclusive m)) = n < m
190−satisfiesTyped (TNumber n) (Maximum (Inclusive m)) = n <= m
191−satisfiesTyped (TNumber n) (Minimum (Down (Exclusive (Down m)))) = n > m
192−satisfiesTyped (TNumber n) (Minimum (Down (Inclusive (Down m)))) = n >= m
193−satisfiesTyped (TNumber n) (MultipleOf m) = denominator (toRational n / toRational m) == 1 -- TODO: could be better #36
194−satisfiesTyped (TNumber n) (NumberFormat f) = checkNumberFormat f n
195−satisfiesTyped (TString s) (MaxLength m) = fromIntegral (T.length s) <= m
196−satisfiesTyped (TString s) (MinLength m) = fromIntegral (T.length s) >= m
197−satisfiesTyped (TString s) (Pattern p) = isJust $ match p s -- TODO: regex stuff #32
198−satisfiesTyped (TString s) (StringFormat f) = checkStringFormat f s
199−satisfiesTyped (TArray a) (Items f _) = all (`satisfies` f) a
200−satisfiesTyped (TArray a) (MaxItems m) = fromIntegral (F.length a) <= m
201−satisfiesTyped (TArray a) (MinItems m) = fromIntegral (F.length a) >= m
202−satisfiesTyped (TArray a) UniqueItems = S.size (S.fromList $ F.toList a) == F.length a -- TODO: could be better #36
203−satisfiesTyped (TObject o) (Properties props additional _) =
204− all (`HM.member` o) (M.keys (M.filter propRequired props))
205− && all (\(k, v) -> satisfies v $ maybe additional propFormula $ M.lookup k props) (HM.toList o)
206−satisfiesTyped (TObject o) (MaxProperties m) = fromIntegral (HM.size o) <= m
207−satisfiesTyped (TObject o) (MinProperties m) = fromIntegral (HM.size o) >= m
208−
209−checkNumberFormat :: Format -> Scientific -> Bool
210−checkNumberFormat "int32" (toRational -> n) =
211− denominator n == 1
212− && n >= toRational (minBound :: Int32)
213− && n <= toRational (maxBound :: Int32)
214−checkNumberFormat "int64" (toRational -> n) =
215− denominator n == 1
216− && n >= toRational (minBound :: Int64)
217− && n <= toRational (maxBound :: Int64)
218−checkNumberFormat "float" _n = True
219−checkNumberFormat "double" _n = True
220−checkNumberFormat f _n = error $ "Invalid number format: " <> T.unpack f
221−
222−checkStringFormat :: Format -> Text -> Bool
223−checkStringFormat "byte" _s = True -- TODO: regex stuff #32
224−checkStringFormat "binary" _s = True
225−checkStringFormat "date" _s = True
226−checkStringFormat "date-time" _s = True
227−checkStringFormat "password" _s = True
228−checkStringFormat "uuid" _s = True
229−checkStringFormat f _s = error $ "Invalid string format: " <> T.unpack f
230−
231−deriving stock instance Eq (Condition t)
232−
233−deriving stock instance Ord (Condition t)
234−
235−deriving stock instance Show (Condition t)
236−
237−data SomeCondition where
238− SomeCondition :: Typeable t => Condition t -> SomeCondition
239−
240−instance Eq SomeCondition where
241− SomeCondition x == SomeCondition y = case cast x of
242− Just x' -> x' == y
243− Nothing -> False
244−
245−instance Ord SomeCondition where
246− compare (SomeCondition x) (SomeCondition y) = case cast x of
247− Just x' -> compare x' y
248− Nothing -> compare (typeRep x) (typeRep y)
249−
250−deriving stock instance Show SomeCondition
251−
252−-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
253−-- the formula is a disjunction of a set of conjuncts, each of which is a
254−-- conjunction of a set of some elementary formulas.
255−newtype DNF a
256− = DNF (S.Set (S.Set a))
257− deriving stock (Eq, Ord, Show)
258−
259−disjAdd :: Ord a => DNF a -> S.Set a -> DNF a
260−disjAdd (DNF yss) xs
261− | any (`S.isSubsetOf` xs) yss = DNF yss
262− | otherwise = DNF $ S.insert xs $ S.filter (not . S.isSubsetOf xs) yss
263−
264−instance Ord a => Lattice (DNF a) where
265− xss \/ DNF yss = S.foldl' disjAdd xss yss
266− DNF xss /\ DNF yss =
267− F.foldl' disjAdd bottom $
268− liftA2 S.union (S.toList xss) (S.toList yss)
269−
270−pattern BottomFormula :: DNF a
271−pattern BottomFormula <-
272− DNF (S.null -> True)
273− where
274− BottomFormula = DNF S.empty
275−
276−isSingleton :: S.Set a -> Maybe a
277−isSingleton s
278− | S.size s == 1 = S.lookupMin s
279− | otherwise = Nothing
280−
281−pattern Conjunct :: Ord a => [a] -> S.Set a
282−pattern Conjunct xs <-
283− (S.toList -> xs)
284− where
285− Conjunct = S.fromList
286−
287−{-# COMPLETE Conjunct #-}
288−
289−pattern SingleConjunct :: Ord a => [a] -> DNF a
290−pattern SingleConjunct xs <-
291− DNF (isSingleton -> Just (Conjunct xs))
292− where
293− SingleConjunct xs = DNF $ S.singleton $ Conjunct xs
294−
295−pattern TopFormula :: DNF a
296−pattern TopFormula <-
297− DNF (isSingleton -> Just (S.null -> True))
298− where
299− TopFormula = DNF $ S.singleton S.empty
300−
301−instance Ord a => BoundedJoinSemiLattice (DNF a) where
302− bottom = BottomFormula
303−
304−instance Ord a => BoundedMeetSemiLattice (DNF a) where
305− top = TopFormula
306−
307−foldLattice
308− :: BoundedLattice l
309− => (a -> l)
310− -> DNF a
311− -> l
312−foldLattice f (DNF xss) =
313− S.foldl'
314− (\z w ->
315− z \/ S.foldl' (\x y -> x /\ f y) top w)
316− bottom
317− xss
318−
319−newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
320− deriving stock (Eq, Ord, Show)
321− deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
322−
323−satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
324−satisfiesFormula val = foldLattice (satisfiesTyped val) . getJsonFormula
325−
326−data ForeachType (f :: JsonType -> Type) = ForeachType
327− { forNull :: f 'Null
328− , forBoolean :: f 'Boolean
329− , forNumber :: f 'Number
330− , forString :: f 'String
331− , forArray :: f 'Array
332− , forObject :: f 'Object
333− }
334−
335−satisfies :: A.Value -> ForeachType JsonFormula -> Bool
336−satisfies val p = case val of
337− A.Null -> satisfiesFormula TNull $ forNull p
338− A.Bool b -> satisfiesFormula (TBool b) $ forBoolean p
339− A.Number n -> satisfiesFormula (TNumber n) $ forNumber p
340− A.String s -> satisfiesFormula (TString s) $ forString p
341− A.Array a -> satisfiesFormula (TArray a) $ forArray p
342− A.Object o -> satisfiesFormula (TObject o) $ forObject p
343−
344−deriving stock instance (forall x. Typeable x => Eq (f x)) => Eq (ForeachType f)
345−
346−deriving stock instance (forall x. Typeable x => Ord (f x)) => Ord (ForeachType f)
347−
348−deriving stock instance (forall x. Typeable x => Show (f x)) => Show (ForeachType f)
349−
350−foldType
351− :: Monoid m
352− => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m)
353− -> m
354−foldType k =
355− k Null forNull
356− <> k Boolean forBoolean
357− <> k Number forNumber
358− <> k String forString
359− <> k Array forArray
360− <> k Object forObject
361−
362−forType_
363− :: Applicative m
364− => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m ())
365− -> m ()
366−forType_ k = do
367− k Null forNull
368− k Boolean forBoolean
369− k Number forNumber
370− k String forString
371− k Array forArray
372− k Object forObject
373− pure ()
374−
375−instance (forall x. Lattice (f x)) => Lattice (ForeachType f) where
376− f1 \/ f2 =
377− ForeachType
378− { forNull = forNull f1 \/ forNull f2
379− , forBoolean = forBoolean f1 \/ forBoolean f2
380− , forNumber = forNumber f1 \/ forNumber f2
381− , forString = forString f1 \/ forString f2
382− , forArray = forArray f1 \/ forArray f2
383− , forObject = forObject f1 \/ forObject f2
384− }
385− f1 /\ f2 =
386− ForeachType
387− { forNull = forNull f1 /\ forNull f2
388− , forBoolean = forBoolean f1 /\ forBoolean f2
389− , forNumber = forNumber f1 /\ forNumber f2
390− , forString = forString f1 /\ forString f2
391− , forArray = forArray f1 /\ forArray f2
392− , forObject = forObject f1 /\ forObject f2
393− }
394−
395−instance
396− (forall x. BoundedJoinSemiLattice (f x))
397− => BoundedJoinSemiLattice (ForeachType f)
398− where
399− bottom =
400− ForeachType
401− { forNull = bottom
402− , forBoolean = bottom
403− , forNumber = bottom
404− , forString = bottom
405− , forArray = bottom
406− , forObject = bottom
407− }
408−
409−instance
410− (forall x. BoundedMeetSemiLattice (f x))
411− => BoundedMeetSemiLattice (ForeachType f)
412− where
413− top =
414− ForeachType
415− { forNull = top
416− , forBoolean = top
417− , forNumber = top
418− , forString = top
419− , forArray = top
420− , forObject = top
421− }
422−
423−instance Steppable Schema (Referenced Schema) where
424− data Step Schema (Referenced Schema)
425− = AllOfStep Int
426− | OneOfStep Int
427− | AnyOfStep Int
428− | ItemsObjectStep
429− | ItemsArrayStep Int
430− | AdditionalPropertiesStep
431− | NotStep
432− deriving stock (Eq, Ord, Show)
433−
434−instance Steppable (Referenced Schema) (Referenced Schema) where
435− data Step (Referenced Schema) (Referenced Schema)
436− = -- | Invariant (for better memoization only): the "tail" of the trace is
437− -- the "least" of the traces of the conjuncted schemata
438− ConjunctedWith (NE.NonEmpty (Trace (Referenced Schema)))
439− | Partitioned PartitionLocation PartitionChoice
440− deriving stock (Eq, Ord, Show)
441−
442−instance Steppable Schema (Definitions (Referenced Schema)) where
443− data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
444− deriving stock (Eq, Ord, Show)
445−
446−instance Steppable Schema Discriminator where
447− data Step Schema Discriminator = DiscriminatorStep
448− deriving stock (Eq, Ord, Show)
449−
450−instance Steppable Discriminator (Definitions (Referenced Schema)) where
451− data Step Discriminator (Definitions (Referenced Schema)) = DiscriminatorMapping
452− deriving stock (Eq, Ord, Show)
453−
454−parseDiscriminatorValue :: Text -> Referenced Schema
455−parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of
456− A.Success x -> x
457− A.Error _ -> Ref $ Reference v
458−
459−-- | A fake writer monad that doesn't actually record anything and allows lazy recursion.
460−newtype Silent q f r a = Silent {runSilent :: a}
461− deriving stock (Functor)
462− deriving (Applicative, Monad) via Identity
463−
464−instance MonadWriter (P.PathsPrefixTree q f r) (Silent q f r) where
465− tell _ = Silent ()
466− listen (Silent x) = Silent (x, P.empty)
467− pass (Silent (x, _)) = Silent x
468−
469−instance MonadState (MemoState ()) (Silent q f r) where
470− get = Silent $ runIdentity $ runMemo () get
471− put _ = pure ()
472−
473−type MonadProcess m =
474− ( MonadReader (Traced (Definitions Schema)) m
475− , MonadWriter (P.PathsPrefixTree Behave AnIssue 'SchemaLevel) m
476− , MonadState (MemoState ()) m
477− )
478−
479−type SilentM = ReaderT (Traced (Definitions Schema)) (Silent Behave AnIssue 'SchemaLevel)
480−
481−warn :: MonadProcess m => Issue 'SchemaLevel -> m ()
482−warn issue = tell $ P.singleton $ AnItem Root $ anIssue issue
483−
484−-- | Ignore warnings but allow a recursive loop that lazily computes a recursive 'Condition'.
485−silently :: MonadProcess m => SilentM a -> m a
486−silently m = do
487− defs <- R.ask
488− pure . runSilent $ runReaderT m defs
489−
490−warnKnot :: MonadProcess m => KnotTier (ForeachType JsonFormula) () m
491−warnKnot =
492− KnotTier
493− { onKnotFound = warn UnguardedRecursion
494− , onKnotUsed = \_ -> pure bottom
495− , tieKnot = \_ -> pure
496− }
497−
498−processRefSchema
499− :: MonadProcess m
500− => Traced (Referenced Schema)
501− -> m (ForeachType JsonFormula)
502−processRefSchema x = do
503− defs <- R.ask
504− memoWithKnot warnKnot (processSchema $ dereference defs x) (ask x)
505−
506−tracedAllOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
507−tracedAllOf sch =
508− _schemaAllOf (extract sch) <&> \xs ->
509− [traced (ask sch >>> step (AllOfStep i)) x | (i, x) <- zip [0 ..] xs]
510−
511−tracedAnyOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
512−tracedAnyOf sch =
513− _schemaAnyOf (extract sch) <&> \xs ->
514− [traced (ask sch >>> step (AnyOfStep i)) x | (i, x) <- zip [0 ..] xs]
515−
516−tracedOneOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
517−tracedOneOf sch =
518− _schemaOneOf (extract sch) <&> \xs ->
519− [traced (ask sch >>> step (OneOfStep i)) x | (i, x) <- zip [0 ..] xs]
520−
521−tracedItems :: Traced Schema -> Maybe (Either (Traced (Referenced Schema)) [Traced (Referenced Schema)])
522−tracedItems sch =
523− _schemaItems (extract sch) <&> \case
524− OpenApiItemsObject x -> Left $ traced (ask sch >>> step ItemsObjectStep) x
525− OpenApiItemsArray xs ->
526− Right
527− [traced (ask sch >>> step (ItemsArrayStep i)) x | (i, x) <- zip [0 ..] xs]
528−
529−tracedAdditionalProperties :: Traced Schema -> Maybe (Either Bool (Traced (Referenced Schema)))
530−tracedAdditionalProperties sch =
531− _schemaAdditionalProperties (extract sch) <&> \case
532− AdditionalPropertiesAllowed b -> Left b
533− AdditionalPropertiesSchema x -> Right $ traced (ask sch >>> step AdditionalPropertiesStep) x
534−
535−tracedDiscriminator :: Traced Schema -> Maybe (Traced Discriminator)
536−tracedDiscriminator = sequence . stepTraced DiscriminatorStep . fmap _schemaDiscriminator
537−
538−tracedProperties :: Traced Schema -> IOHM.InsOrdHashMap Text (Traced (Referenced Schema))
539−tracedProperties sch =
540− IOHM.mapWithKey
541− (\k -> traced (ask sch >>> step PropertiesStep >>> step (InsOrdHashMapKeyStep k)))
542− (_schemaProperties $ extract sch)
543−
544−-- | Turn a schema into a tuple of 'JsonFormula's that describes the condition
545−-- for every possible type of a JSON value. The conditions are independent, and
546−-- are thus checked independently.
547−processSchema
548− :: MonadProcess m
549− => Traced Schema
550− -> m (ForeachType JsonFormula)
551−processSchema sch@(extract -> Schema {..}) = do
552− let singletonFormula :: Condition t -> JsonFormula t
553− singletonFormula f = JsonFormula $ SingleConjunct [f]
554−
555− allClauses <- case tracedAllOf sch of
556− Nothing -> pure []
557− Just [] -> [] <$ warn (InvalidSchema "no items in allOf")
558− Just xs -> mapM processRefSchema xs
559−
560− anyClause <- case tracedAnyOf sch of
561− Nothing -> pure top
562− Just [] -> bottom <$ warn (InvalidSchema "no items in anyOf")
563− Just xs -> joins <$> mapM processRefSchema xs
564−
565− oneClause <- case tracedOneOf sch of
566− Nothing -> pure top
567− Just [] -> bottom <$ warn (InvalidSchema "no items in oneOf")
568− Just xs -> do
569− checkOneOfDisjoint xs >>= \case
570− True -> pure ()
571− False -> warn (NotSupported "Could not determine that oneOf branches are disjoint")
572− joins <$> mapM processRefSchema xs
573−
574− case _schemaNot of
575− Nothing -> pure ()
576− Just _ -> warn (NotSupported "not clause is unsupported")
577−
578− let typeClause = case _schemaType of
579− Nothing -> top
580− Just OpenApiNull ->
581− bottom
582− { forNull = top
583− }
584− Just OpenApiBoolean ->
585− bottom
586− { forBoolean = top
587− }
588− Just OpenApiNumber ->
589− bottom
590− { forNumber = top
591− }
592− Just OpenApiInteger ->
593− bottom
594− { forNumber = singletonFormula $ MultipleOf 1
595− }
596− Just OpenApiString ->
597− bottom
598− { forString = top
599− }
600− Just OpenApiArray ->
601− bottom
602− { forArray = top
603− }
604− Just OpenApiObject ->
605− bottom
606− { forObject = top
607− }
608−
609− let valueEnum A.Null =
610− bottom
611− { forNull = singletonFormula $ Exactly TNull
612− }
613− valueEnum (A.Bool b) =
614− bottom
615− { forBoolean = singletonFormula $ Exactly $ TBool b
616− }
617− valueEnum (A.Number n) =
618− bottom
619− { forNumber = singletonFormula $ Exactly $ TNumber n
620− }
621− valueEnum (A.String s) =
622− bottom
623− { forString = singletonFormula $ Exactly $ TString s
624− }
625− valueEnum (A.Array a) =
626− bottom
627− { forArray = singletonFormula $ Exactly $ TArray a
628− }
629− valueEnum (A.Object o) =
630− bottom
631− { forObject = singletonFormula $ Exactly $ TObject o
632− }
633− enumClause <- case _schemaEnum of
634− Nothing -> pure top
635− Just [] -> bottom <$ warn (InvalidSchema "no items in enum")
636− Just xs -> pure $ joins (valueEnum <$> xs)
637−
638− let maximumClause = case _schemaMaximum of
639− Nothing -> top
640− Just n ->
641− top
642− { forNumber = singletonFormula $
643− Maximum $
644− case _schemaExclusiveMaximum of
645− Just True -> Exclusive n
646− _ -> Inclusive n
647− }
648−
649− minimumClause = case _schemaMinimum of
650− Nothing -> top
651− Just n ->
652− top
653− { forNumber = singletonFormula $
654− Minimum $
655− Down $
656− case _schemaExclusiveMinimum of
657− Just True -> Exclusive $ Down n
658− _ -> Inclusive $ Down n
659− }
660−
661− multipleOfClause = case _schemaMultipleOf of
662− Nothing -> top
663− Just n ->
664− top
665− { forNumber = singletonFormula $ MultipleOf n
666− }
667−
668− formatClause <- case _schemaFormat of
669− Nothing -> pure top
670− Just f
671− | f `elem` ["int32", "int64", "float", "double"] ->
672− pure
673− top
674− { forNumber = singletonFormula $ NumberFormat f
675− }
676− Just f
677− | f `elem` ["byte", "binary", "date", "date-time", "password", "uuid"] ->
678− pure
679− top
680− { forString = singletonFormula $ StringFormat f
681− }
682− Just f -> top <$ warn (NotSupported $ "Unknown format: " <> f)
683−
684− let maxLengthClause = case _schemaMaxLength of
685− Nothing -> top
686− Just n ->
687− top
688− { forString = singletonFormula $ MaxLength n
689− }
690−
691− minLengthClause = case _schemaMinLength of
692− Nothing -> top
693− Just n ->
694− top
695− { forString = singletonFormula $ MinLength n
696− }
697−
698− patternClause = case _schemaPattern of
699− Nothing -> top
700− Just p ->
701− top
702− { forString = singletonFormula $ Pattern p
703− }
704−
705− itemsClause <- case tracedItems sch of
706− Nothing -> pure top
707− Just (Left rs) -> do
708− f <- silently $ processRefSchema rs
709− pure top {forArray = singletonFormula $ Items f rs}
710− Just (Right _) -> top <$ warn (NotSupported "array in items is not supported")
711−
712− let maxItemsClause = case _schemaMaxItems of
713− Nothing -> top
714− Just n ->
715− top
716− { forArray = singletonFormula $ MaxItems n
717− }
718−
719− minItemsClause = case _schemaMinItems of
720− Nothing -> top
721− Just n ->
722− top
723− { forArray = singletonFormula $ MinItems n
724− }
725−
726− uniqueItemsClause = case _schemaUniqueItems of
727− Just True ->
728− top
729− { forArray = singletonFormula UniqueItems
730− }
731− _ -> top
732−
733− (addProps, addPropSchema) <- case tracedAdditionalProperties sch of
734− Just (Right rs) -> (,Just rs) <$> silently (processRefSchema rs)
735− Just (Left False) -> pure (bottom, Nothing)
736− _ -> pure (top, Just $ traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty)
737− propList <- forM (S.toList . S.fromList $ IOHM.keys _schemaProperties <> _schemaRequired) $ \k -> do
738− (f, psch) <- case IOHM.lookup k $ tracedProperties sch of
739− Just rs -> (,rs) <$> silently (processRefSchema rs)
740− Nothing ->
741− let fakeSchema = traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty
742− in -- The mempty here is incorrect, but if addPropSchema was Nothing, then
743− -- addProps is bottom, and k is in _schemaRequired. We handle this situation
744− -- below and short-circuit the entire Properties condition to bottom
745− pure (addProps, fromMaybe fakeSchema addPropSchema)
746− pure (k, Property (k `elem` _schemaRequired) f psch)
747− let allBottom f = getAll $
748− foldType $ \_ ty -> case getJsonFormula $ ty f of
749− BottomFormula -> All True
750− _ -> All False
751− allTop f = getAll $
752− foldType $ \_ ty -> case getJsonFormula $ ty f of
753− TopFormula -> All True
754− _ -> All False
755− -- remove optional fields whose schemata match that of additional props
756− propMap = M.filter (\p -> propRequired p || propFormula p /= addProps) $ M.fromList propList
757− propertiesClause
758− | any (\p -> propRequired p && allBottom (propFormula p)) propMap =
759− bottom -- if any required field has unsatisfiable schema
760− | M.null propMap
761− , allTop addProps =
762− top -- if all fields are optional and have trivial schemata
763− | otherwise =
764− top
765− { forObject = singletonFormula $ Properties propMap addProps addPropSchema
766− }
767−
768− maxPropertiesClause = case _schemaMaxProperties of
769− Nothing -> top
770− Just n ->
771− top
772− { forObject = singletonFormula $ MaxProperties n
773− }
774−
775− minPropertiesClause = case _schemaMinProperties of
776− Nothing -> top
777− Just n ->
778− top
779− { forObject = singletonFormula $ MinProperties n
780− }
781−
782− nullableClause
783− | Just True <- _schemaNullable =
784− bottom
785− { forNull = singletonFormula $ Exactly TNull
786− }
787− | otherwise = bottom
788−
789− pure $
790− nullableClause
791− \/ meets
792− (allClauses
793− <> [ anyClause
794− , oneClause
795− , typeClause
796− , enumClause
797− , maximumClause
798− , minimumClause
799− , multipleOfClause
800− , formatClause
801− , maxLengthClause
802− , minLengthClause
803− , patternClause
804− , itemsClause
805− , maxItemsClause
806− , minItemsClause
807− , uniqueItemsClause
808− , propertiesClause
809− , maxPropertiesClause
810− , minPropertiesClause
811− ])
812−
813−{- TODO: ReadOnly/WriteOnly #68 -}
814−
815−checkOneOfDisjoint :: MonadProcess m => [Traced (Referenced Schema)] -> m Bool
816−checkOneOfDisjoint = const $ pure True -- TODO #69
817−
818−schemaToFormula
819− :: Traced (Definitions Schema)
820− -> Traced Schema
821− -> (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
822−schemaToFormula defs rs = runWriter . (`runReaderT` defs) . runMemo () $ processSchema rs
823−
824−checkFormulas
825− :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
826− => HList xs
827− -> Behavior 'SchemaLevel
828− -> ProdCons (Traced (Definitions Schema))
829− -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
830− -> SemanticCompatFormula ()
831−checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
832− case P.toList ep ++ P.toList ec of
833− issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
834− [] -> do
835− -- We have the following isomorphisms:
836− -- (A ⊂ X ∪ Y) = (A ⊂ X) \/ (A ⊂ Y)
837− -- (A ⊂ X ∩ Y) = (A ⊂ X) /\ (A ⊂ Y)
838− -- (A ⊂ ⊤) = 1
839− -- (X ∪ Y ⊂ B) = (X ⊂ B) /\ (Y ⊂ B)
840− -- (∅ ⊂ B) = 1
841− -- The remaining cases are, notably, not isomorphisms:
842− -- 1) (A ⊂ ∅) <= 0
843− -- 2) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
844− -- 3) (⊤ ⊂ B) <= 0
845− -- Therefore we have the isomorphisms with (∃ and ∀ being the N-ary
846− -- versions of \/ and /\ respectively):
847− -- (⋃_i ⋂_j A[i,j]) ⊂ (⋃_k ⋂_l B[k,l])
848− -- = ∃k ∀l ∀i, (⋂_j A[i,j]) ⊂ B[k,l]
849− -- = ∀i ∃k ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
850− -- with the caveat that the the set over which k ranges is nonempty.
851− -- This is because 1) is not an isomorphism.
852− -- Our disjunction loses information, so it makes sense to nest it as
853− -- deeply as possible, hence we choose the latter representation.
854− --
855− -- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
856− -- function, with the understanding that ∃j, A[j] ⊂ B is a sufficient,
857− -- but not necessary condition (because of 2) and 3)).
858− --
859− -- If k ranges over an empty set, we have the isomorphism:
860− -- (⋃_i ⋂_j A[i,j]) ⊂ ∅ = ∀i, (⋂_j A[i,j]) ⊂ ∅
861− -- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
862− -- shortcut of ∃j, A[j] ⊂ ∅ hardly helps.
863− --
864− -- Disjunctions tend to erase informative error messages, so we may want
865− -- to avoid them. This can be formally done as follows: if we can
866− -- partition the universal set into a disjoint union of some parts:
867− -- ⊤ = ⊔_α P[α]
868− -- such that the conjuncts in our disjunctive normal form are subordinate
869− -- to the partition:
870− -- ∀i ∃α, (⋂_j A[i,j]) ⊂ P[α]
871− -- ∀k ∃α, (⋂_l B[k,l]) ⊂ P[α]
872− -- then we can partition the sets over which i and k range into partitions
873− -- I[α] and K[α], and then in each "bucket" verify the inclusion in the
874− -- aforementioned way:
875− -- ∀α, (⋃_i∈I[α] ⋂_j A[i,j]) ⊂ (⋃_k∈K[α] ⋂_l B[k,l])
876− -- = ∀α ∀i∈I[α] ∃k∈K[α] ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
877− -- We already somewhat do this by partitioning JSON into types, but we can
878− -- additionally partition e.g. "enum" fields or existence of particular
879− -- properties. This works especially well if we manage to ensure K[α] are
880− -- 1-element sets.
881− --
882− -- Since the set:
883− -- (⋃_i∈I[α] ⋂_j A[i,j]) = (⋃_i ⋂_j A[i,j]) ∩ P[α]
884− -- does not actually appear in the source schema, we need to construct it
885− -- ourselves and come up with a name for it.
886− let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
887− -- Specifically handle the case when a schema's type has been
888− -- restricted from "all" to specific types: if all types were allowed
889− -- in the producer and not all types are allowed in the consumer, it's
890− -- usually easier to say what's left than what's removed
891− when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
892− forType_ $ \tyName ty -> do
893− let beh' = beh >>> step (OfType tyName)
894− case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
895− (DNF pss, BottomFormula) -> unless typesRestricted $ do
896− -- don't repeat the TypesRestricted issue
897− F.for_ pss $ \(Conjunct ps) -> checkContradiction beh' ps
898− (DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
899− F.for_ cs $ checkImplication env beh' ps -- avoid disjunction if there's only one conjunct
900− (TopFormula, DNF css) ->
901− -- producer is "open" (allows any value), but consumer has restrictions.
902− -- In this case we want to show which restrictions were added. (instead
903− -- of showing an empty list restrictions that couldn't be satisfied.)
904− F.for_ css $ \(Conjunct cs) -> F.for_ cs $ checkImplication env beh' []
905− (pss', css') -> F.for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
906− (mPart, ProdCons pf cf) -> do
907− let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
908− case (getJsonFormula pf, getJsonFormula cf) of
909− (DNF pss, BottomFormula) -> F.for_ pss $ \(Conjunct ps) -> checkContradiction beh'' ps
910− (DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
911− F.for_ cs $ checkImplication env beh'' ps
912− -- unlucky:
913− (DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
914− anyOfAt
915− beh'
916− (issueFromConjunct Nothing ps)
917− [F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
918− pure ()
919− where
920− anyBottomTypes f = getAny $
921− foldType $ \_ ty -> case getJsonFormula $ ty f of
922− BottomFormula -> Any True
923− _ -> mempty
924− nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
925− BottomFormula -> mempty
926− _ -> [tyName]
927− issueFromConjunct :: Typeable t => Partition -> [Condition t] -> Issue 'TypedSchemaLevel
928− issueFromConjunct _ ps
929− | Just e <- findExactly ps
930− , all (satisfiesTyped e) ps =
931− EnumDoesntSatisfy $ untypeValue e -- what does this look like when partitioned?
932− issueFromConjunct mPart ps = NoMatchingCondition mPart $ SomeCondition <$> ps
933−
934−data PartitionData
935− = DByEnumValue (DNF (S.Set A.Value))
936− | DByProperties (DNF (S.Set Text, S.Set Text)) -- optional, required
937− deriving stock (Eq, Ord, Show)
938−
939−data PartitionChoice
940− = CByEnumValue (S.Set A.Value)
941− | CByProperties (S.Set Text) (S.Set Text) -- included, excluded
942− deriving stock (Eq, Ord, Show)
943−
944−conjPart :: PartitionData -> PartitionData -> Maybe PartitionData
945−conjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss /\ yss
946−conjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss /\ yss
947−conjPart _ _ = Nothing
948−
949−disjPart :: PartitionData -> PartitionData -> Maybe PartitionData
950−disjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss \/ yss
951−disjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss \/ yss
952−disjPart _ _ = Nothing
953−
954−data PartitionLocation
955− = PHere
956− | PInProperty Text PartitionLocation
957− deriving stock (Eq, Ord, Show)
958−
959−newtype Partitions = Partitions (M.Map PartitionLocation (S.Set PartitionData))
960− deriving stock (Eq, Ord, Show)
961−
962−instance Lattice Partitions where
963− Partitions xss /\ Partitions yss = Partitions $ M.unionWith conj xss yss
964− where
965− conj xs ys = S.fromList . catMaybes $ liftA2 conjPart (S.toList xs) (S.toList ys)
966− Partitions xss \/ Partitions yss = Partitions $ M.intersectionWith disj xss yss
967− where
968− disj xs ys = S.fromList . catMaybes $ liftA2 disjPart (S.toList xs) (S.toList ys)
969−
970−instance BoundedMeetSemiLattice Partitions where
971− top = Partitions M.empty
972−
973−-- The lattice has no bottom, but we use 'Lifted' to adjoin a free bottom element
974−
975−type PartitionM = ReaderT (Traced (Definitions Schema)) (State (MemoState ()))
976−
977−ignoreKnot :: KnotTier (Lifted Partitions) () PartitionM
978−ignoreKnot =
979− KnotTier
980− { onKnotFound = pure ()
981− , onKnotUsed = \_ -> pure bottom
982− , tieKnot = \_ -> pure
983− }
984−
985−singletonPart :: PartitionData -> Lifted Partitions
986−singletonPart = Lift . Partitions . M.singleton PHere . S.singleton
987−
988−partitionSchema :: Traced Schema -> PartitionM (Lifted Partitions)
989−partitionSchema sch = do
990− allClauses <- case tracedAllOf sch of
991− Nothing -> pure []
992− Just xs -> mapM partitionRefSchema xs
993−
994− anyClause <- case tracedAnyOf sch of
995− Nothing -> pure top
996− Just xs -> joins <$> mapM partitionRefSchema xs
997−
998− oneClause <- case tracedOneOf sch of
999− Nothing -> pure top
1000− Just xs -> joins <$> mapM partitionRefSchema xs
1001−
1002− byEnumClause <- case _schemaEnum $ extract sch of
1003− Nothing -> pure top
1004− Just xs ->
1005− pure . singletonPart $
1006− DByEnumValue $ SingleConjunct [S.fromList xs]
1007−
1008− -- We can only partition by presence of a property if additional properties
1009− -- are disallowed, and the property is not optional
1010− let reqd = S.fromList $ _schemaRequired $ extract sch
1011− byPropertiesClause <- case _schemaAdditionalProperties $ extract sch of
1012− Just (AdditionalPropertiesAllowed False) -> do
1013− let props = S.fromList . IOHM.keys . _schemaProperties $ extract sch
1014− pure . singletonPart $
1015− DByProperties $ SingleConjunct [(props S.\\ reqd, props `S.intersection` reqd)]
1016− _ -> pure top
1017−
1018− -- We can partition on something nested in a property only if the property is
1019− -- required
1020− let reqdProps = IOHM.filterWithKey (\k _ -> k `S.member` reqd) $ tracedProperties sch
1021− inPropertiesClauses <- forM (IOHM.toList reqdProps) $ \(k, rs) -> do
1022− f <- partitionRefSchema rs
1023− pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
1024−
1025− pure $ meets $ allClauses <> [anyClause, oneClause, byEnumClause, byPropertiesClause] <> inPropertiesClauses
1026−
1027−partitionRefSchema :: Traced (Referenced Schema) -> PartitionM (Lifted Partitions)
1028−partitionRefSchema x = do
1029− defs <- R.ask
1030− memoWithKnot ignoreKnot (partitionSchema $ dereference defs x) (ask x)
1031−
1032−newtype LiftA f a = LiftA {getLiftA :: f a}
1033− deriving newtype (Functor, Applicative)
1034−
1035−instance (Lattice a, Applicative f) => Lattice (LiftA f a) where
1036− (/\) = liftA2 (/\)
1037− (\/) = liftA2 (\/)
1038−
1039−instance (BoundedJoinSemiLattice a, Applicative f) => BoundedJoinSemiLattice (LiftA f a) where
1040− bottom = pure bottom
1041−
1042−instance (BoundedMeetSemiLattice a, Applicative f) => BoundedMeetSemiLattice (LiftA f a) where
1043− top = pure top
1044−
1045−partitionCondition :: Condition t -> PartitionM (Lifted Partitions)
1046−partitionCondition = \case
1047− Exactly x ->
1048− pure . singletonPart $
1049− DByEnumValue $ SingleConjunct [S.singleton $ untypeValue x]
1050− Properties props _ madd -> do
1051− let byProps = case madd of
1052− Just _ -> top
1053− Nothing ->
1054− singletonPart $
1055− DByProperties $
1056− SingleConjunct
1057− [ ( M.keysSet $ M.filter (not . propRequired) props
1058− , M.keysSet $ M.filter propRequired props
1059− )
1060− ]
1061− inProps <- forM (M.toList $ M.filter propRequired props) $ \(k, prop) -> do
1062− f <- partitionRefSchema $ propRefSchema prop
1063− pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
1064− pure $ byProps /\ meets inProps
1065− _ -> pure top
1066−
1067−partitionJsonFormulas
1068− :: ProdCons (Traced (Definitions Schema))
1069− -> ProdCons (JsonFormula t)
1070− -> Lifted Partitions
1071−partitionJsonFormulas defs pc = producer pcPart \/ consumer pcPart
1072− where
1073− pcPart = partitionFormula <$> defs <*> pc
1074− partitionFormula def (JsonFormula xss) = runIdentity . runMemo () . (`runReaderT` def) $ do
1075− getLiftA . foldLattice (LiftA . partitionCondition) $ xss
1076−
1077−selectPartition :: Lifted Partitions -> Maybe (PartitionLocation, S.Set PartitionChoice)
1078−selectPartition Bottom = Nothing
1079−selectPartition (Lift (Partitions m)) =
1080− go [(loc, part) | (loc, parts) <- sortBy (comparing $ locLength . fst) $ M.toList m, part <- S.toList parts]
1081− where
1082− locLength :: PartitionLocation -> Int
1083− locLength = walk 0
1084− where
1085− walk !n PHere = n
1086− walk !n (PInProperty _ l) = walk (n + 1) l
1087− go [] = Nothing
1088− -- Skip partitioning by property for now
1089− go ((_, DByProperties _) : ps) = go ps
1090− -- Don't partition by enum value at the root (this reports removed enum values as contradictions in their respective partitions)
1091− go ((PHere, DByEnumValue _) : ps) = go ps
1092− go ((loc, DByEnumValue (DNF xss)) : ps)
1093− -- Check that no disjunction branches are unresticted
1094− | Just enums <- traverse (fmap (foldr1 S.intersection) . NE.nonEmpty . S.toList) . S.toList $ xss =
1095− -- TODO: improve
1096− Just (loc, S.map (CByEnumValue . S.singleton) $ S.unions enums)
1097− | otherwise = go ps
1098−
1099−-- This essentially has 3 cases:
1100−-- Nothing -- we have produced a bottom schema
1101−-- Just (False, _) -- there's been no change to the schema
1102−-- Just (True, x) -- x is a new schema
1103−type IntersectionM = ReaderT (Traced (Definitions Schema)) (WriterT Any Maybe)
1104−
1105−mBottom :: IntersectionM a
1106−mBottom = lift . lift $ Nothing
1107−
1108−catchBottom :: IntersectionM a -> IntersectionM a -> IntersectionM a
1109−catchBottom act handler = R.liftCatch (W.liftCatch (\a h -> a <|> h ())) act (\_ -> handler)
1110−
1111−mChange :: IntersectionM ()
1112−mChange = tell $ Any True
1113−
1114−intersectSchema
1115− :: PartitionLocation
1116− -> PartitionChoice
1117− -> Traced Schema
1118− -> IntersectionM Schema
1119−intersectSchema loc part sch = do
1120− allOf' <- forM (tracedAllOf sch) $ \rss ->
1121− -- Assuming i ranges over a nonempty set (checked in processSchema)
1122− -- (⋂_i A[i]) ∩ X = ⋂_i (A[i] ∩ X)
1123− -- If any intersections are empty, the result is empty. If any intersections are a change, the result is a change.
1124− traverse (intersectRefSchema loc part) rss
1125− anyOf' <- forM (tracedAnyOf sch) $ \rss -> do
1126− -- (⋃_i A[i]) ∩ X = ⋃_i (A[i] ∩ X)
1127− -- Collect only the nonempty A[i] ∩ X, unless there are none, in which case the result is empty.
1128− -- If any schema is empty, we remove it from the list which constitutes a change.
1129− mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
1130− case catMaybes mSchemas of
1131− [] -> mBottom
1132− schs -> pure schs
1133− oneOf' <- forM (tracedOneOf sch) $ \rss -> do
1134− -- Same as anyOf'. By intersecting we're only making them more disjoint if anything.
1135− mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
1136− case catMaybes mSchemas of
1137− [] -> mBottom
1138− schs -> pure schs
1139− let sch' = (extract sch) {_schemaAllOf = allOf', _schemaAnyOf = anyOf', _schemaOneOf = oneOf'}
1140− -- Now the local changes:
1141− case loc of
1142− PInProperty k loc' -> case IOHM.lookup k $ tracedProperties sch of
1143− Nothing -> error $ "Partitioning via absent property: " <> T.unpack k
1144− Just prop -> do
1145− prop' <- intersectRefSchema loc' part prop
1146− pure $ sch' {_schemaProperties = IOHM.adjust (const prop') k $ _schemaProperties sch'}
1147− PHere -> case part of
1148− CByEnumValue vals -> do
1149− enum' <- case _schemaEnum sch' of
1150− Nothing -> do
1151− mChange
1152− pure $ S.toList vals
1153− Just xs -> do
1154− when (any (`S.notMember` vals) xs) mChange
1155− case filter (`S.member` vals) xs of
1156− [] -> mBottom
1157− xs' -> pure xs'
1158− pure $ sch' {_schemaEnum = Just enum'}
1159− CByProperties {} -> error "CByProperties not implemented"
1160−
1161−intersectRefSchema
1162− :: PartitionLocation
1163− -> PartitionChoice
1164− -> Traced (Referenced Schema)
1165− -> IntersectionM (Referenced Schema)
1166−intersectRefSchema loc part rs = do
1167− defs <- R.ask
1168− Inline <$> intersectSchema loc part (dereference defs rs)
1169−
1170−intersectCondition :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> Condition t -> DNF (Condition t)
1171−intersectCondition _defs PHere (CByEnumValue values) cond@(Exactly x) =
1172− if untypeValue x `S.member` values then SingleConjunct [cond] else bottom
1173−intersectCondition defs (PInProperty k loc) part cond@(Properties props add madd) = case M.lookup k props of
1174− Nothing -> SingleConjunct [cond] -- shouldn't happen
1175− Just prop -> case runWriterT . (`runReaderT` defs) $ intersectRefSchema loc part $ propRefSchema prop of
1176− Just (rs', Any True) ->
1177− let trs' = traced (ask (propRefSchema prop) >>> step (Partitioned loc part)) rs'
1178− in SingleConjunct [Properties (M.insert k prop {propRefSchema = trs'} props) add madd]
1179− Just (_, Any False) -> SingleConjunct [cond]
1180− Nothing -> bottom
1181−intersectCondition _defs _loc _part cond = SingleConjunct [cond]
1182−
1183−intersectFormula :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> JsonFormula t -> JsonFormula t
1184−intersectFormula defs loc part = JsonFormula . foldLattice (intersectCondition defs loc part) . getJsonFormula
1185−
1186−type Partition = Maybe (PartitionLocation, PartitionChoice)
1187−
1188−tryPartition :: ProdCons (Traced (Definitions Schema)) -> ProdCons (JsonFormula t) -> [(Partition, ProdCons (JsonFormula t))]
1189−tryPartition defs pc = case selectPartition $ partitionJsonFormulas defs pc of
1190− Nothing -> [(Nothing, pc)]
1191− Just (loc, parts) -> [(Just (loc, part), intersectFormula <$> defs <*> pure loc <*> pure part <*> pc) | part <- S.toList parts]
1192−
1193−checkContradiction
1194− :: Behavior 'TypedSchemaLevel
1195− -> [Condition t]
1196− -> SemanticCompatFormula ()
1197−checkContradiction beh _ = issueAt beh NoContradiction -- TODO #70
1198−
1199−tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
1200−tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
1201− (rs NE.:| []) -> rs
1202− (rs1 NE.:| rs2 : rss) ->
1203− traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
1204− Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
1205−
1206−checkImplication
1207− :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
1208− => HList xs
1209− -> Behavior 'TypedSchemaLevel
1210− -> [Condition t]
1211− -> Condition t
1212− -> SemanticCompatFormula ()
1213−checkImplication env beh prods cons = case findExactly prods of
1214− Just e
1215− | all (satisfiesTyped e) prods ->
1216− if satisfiesTyped e cons
1217− then pure ()
1218− else issueAt beh (EnumDoesntSatisfy $ untypeValue e)
1219− | otherwise -> pure () -- vacuously true
1220− Nothing -> case cons of
1221− -- the above code didn't catch it, so there's no Exactly condition on the lhs
1222− Exactly e -> issueAt beh (NoMatchingEnum $ untypeValue e)
1223− Maximum m -> case findRelevant min (\case Maximum m' -> Just m'; _ -> Nothing) prods of
1224− Just m' ->
1225− if m' <= m
1226− then pure ()
1227− else issueAt beh (MatchingMaximumWeak $ ProdCons {producer = m', consumer = m})
1228− Nothing -> issueAt beh (NoMatchingMaximum m)
1229− Minimum m -> case findRelevant max (\case Minimum m' -> Just m'; _ -> Nothing) prods of
1230− Just m' ->
1231− if m' >= m
1232− then pure ()
1233− else issueAt beh (MatchingMinimumWeak ProdCons {producer = coerce m', consumer = coerce m})
1234− Nothing -> issueAt beh (NoMatchingMinimum (coerce m))
1235− MultipleOf m -> case findRelevant lcmScientific (\case MultipleOf m' -> Just m'; _ -> Nothing) prods of
1236− Just m' ->
1237− if lcmScientific m m' == m'
1238− then pure ()
1239− else issueAt beh (MatchingMultipleOfWeak $ ProdCons {producer = m', consumer = m})
1240− Nothing -> issueAt beh (NoMatchingMultipleOf m)
1241− NumberFormat f ->
1242− if any (\case NumberFormat f' -> f == f'; _ -> False) prods
1243− then pure ()
1244− else issueAt beh (NoMatchingFormat f)
1245− MaxLength m -> case findRelevant min (\case MaxLength m' -> Just m'; _ -> Nothing) prods of
1246− Just m' ->
1247− if m' <= m
1248− then pure ()
1249− else issueAt beh (MatchingMaxLengthWeak $ ProdCons {producer = m', consumer = m})
1250− Nothing -> issueAt beh (NoMatchingMaxLength m)
1251− MinLength m -> case findRelevant max (\case MinLength m' -> Just m'; _ -> Nothing) prods of
1252− Just m' ->
1253− if m' >= m
1254− then pure ()
1255− else issueAt beh (MatchingMinLengthWeak $ ProdCons {producer = m', consumer = m})
1256− Nothing -> issueAt beh (NoMatchingMinLength m)
1257− Pattern p ->
1258− if any (\case Pattern p' -> p == p'; _ -> False) prods
1259− then pure ()
1260− else issueAt beh (NoMatchingPattern p) -- TODO: regex comparison #32
1261− StringFormat f ->
1262− if any (\case StringFormat f' -> f == f'; _ -> False) prods
1263− then pure ()
1264− else issueAt beh (NoMatchingFormat f)
1265− Items _ cons' -> case findRelevant (<>) (\case Items _ rs -> Just (rs NE.:| []); _ -> Nothing) prods of
1266− Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
1267− Nothing -> issueAt beh NoMatchingItems
1268− MaxItems m -> case findRelevant min (\case MaxItems m' -> Just m'; _ -> Nothing) prods of
1269− Just m' ->
1270− if m' <= m
1271− then pure ()
1272− else issueAt beh (MatchingMaxItemsWeak ProdCons {producer = m', consumer = m})
1273− Nothing -> issueAt beh (NoMatchingMaxItems m)
1274− MinItems m -> case findRelevant max (\case MinItems m' -> Just m'; _ -> Nothing) prods of
1275− Just m' ->
1276− if m' >= m
1277− then pure ()
1278− else issueAt beh (MatchingMinItemsWeak ProdCons {producer = m', consumer = m})
1279− Nothing -> issueAt beh (NoMatchingMinItems m)
1280− UniqueItems ->
1281− if any (== UniqueItems) $ prods
1282− then pure ()
1283− else issueAt beh NoMatchingUniqueItems
1284− Properties props _ madd -> case findRelevant (<>) (\case Properties props' _ madd' -> Just $ (props', madd') NE.:| []; _ -> Nothing) prods of
1285− Just pm ->
1286− anyOfAt beh NoMatchingProperties $
1287− NE.toList pm <&> \(props', madd') -> do
1288− F.for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
1289− let beh' = beh >>> step (InProperty k)
1290− go sch sch' = checkCompatibility beh' env (ProdCons sch sch')
1291− case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
1292− -- producer does not require field, but consumer does (can fail)
1293− (False, True) -> issueAt beh' PropertyNowRequired
1294− _ -> pure ()
1295− case (M.lookup k props', madd', M.lookup k props, madd) of
1296− -- (producer, additional producer, consumer, additional consumer)
1297− (Nothing, Nothing, _, _) -> pure () -- vacuously: the producer asserts that this field cannot exist,
1298− -- and the consumer either doesn't require it, or it does and we've already raised an error about it.
1299− (_, _, Nothing, Nothing) -> issueAt beh' UnexpectedProperty
1300− (Just p', _, Just p, _) -> go (propRefSchema p') (propRefSchema p)
1301− (Nothing, Just add', Just p, _) ->
1302− clarifyIssue (AnItem beh' (anIssue AdditionalToProperty)) $
1303− go add' (propRefSchema p)
1304− (Just p', _, Nothing, Just add) ->
1305− clarifyIssue (AnItem beh' (anIssue PropertyToAdditional)) $
1306− go (propRefSchema p') add
1307− (Nothing, Just _, Nothing, Just _) -> pure ()
1308− pure ()
1309− case (madd', madd) of
1310− (Nothing, _) -> pure () -- vacuously
1311− (_, Nothing) -> issueAt beh NoAdditionalProperties
1312− (Just add', Just add) -> checkCompatibility (beh >>> step InAdditionalProperty) env (ProdCons add' add)
1313− pure ()
1314− Nothing -> issueAt beh NoMatchingProperties
1315− MaxProperties m -> case findRelevant min (\case MaxProperties m' -> Just m'; _ -> Nothing) prods of
1316− Just m' ->
1317− if m' <= m
1318− then pure ()
1319− else issueAt beh (MatchingMaxPropertiesWeak ProdCons {producer = m', consumer = m})
1320− Nothing -> issueAt beh (NoMatchingMaxProperties m)
1321− MinProperties m -> case findRelevant max (\case MinProperties m' -> Just m'; _ -> Nothing) prods of
1322− Just m' ->
1323− if m' >= m
1324− then pure ()
1325− else issueAt beh (MatchingMinPropertiesWeak ProdCons {producer = m', consumer = m})
1326− Nothing -> issueAt beh (NoMatchingMinProperties m)
1327− where
1328− findRelevant combine extr =
1329− fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr
1330− lcmScientific (toRational -> a) (toRational -> b) =
1331− fromRational $ lcm (numerator a) (numerator b) % gcd (denominator a) (denominator b)
1332−
1333−findExactly :: [Condition t] -> Maybe (TypedValue t)
1334−findExactly (Exactly x : _) = Just x
1335−findExactly (_ : xs) = findExactly xs
1336−findExactly [] = Nothing
1337−
1338−instance Issuable 'TypedSchemaLevel where
1339− data Issue 'TypedSchemaLevel
1340− = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
1341− EnumDoesntSatisfy A.Value
1342− | -- | consumer only expects a specific value which the producer does not produce.
1343− NoMatchingEnum A.Value
1344− | -- | consumer declares a maximum numeric value ($1), producer doesn't
1345− NoMatchingMaximum (Bound Scientific)
1346− | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
1347− MatchingMaximumWeak (ProdCons (Bound Scientific))
1348− | -- | consumer declares a minimum numeric value, producer doesn't
1349− NoMatchingMinimum (Bound Scientific)
1350− | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
1351− MatchingMinimumWeak (ProdCons (Bound Scientific))
1352− | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
1353− NoMatchingMultipleOf Scientific
1354− | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
1355− MatchingMultipleOfWeak (ProdCons Scientific)
1356− | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
1357− NoMatchingFormat Format
1358− | -- | consumer declares a maximum length of the string ($1), producer doesn't.
1359− NoMatchingMaxLength Integer
1360− | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
1361− MatchingMaxLengthWeak (ProdCons Integer)
1362− | -- | consumer declares a minimum length of the string ($1), producer doesn't.
1363− NoMatchingMinLength Integer
1364− | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
1365− MatchingMinLengthWeak (ProdCons Integer)
1366− | -- | consumer declares the string value must match a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
1367− NoMatchingPattern Pattern
1368− | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
1369− NoMatchingItems
1370− | -- | consumer declares a maximum length of the array ($1), producer doesn't.
1371− NoMatchingMaxItems Integer
1372− | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
1373− MatchingMaxItemsWeak (ProdCons Integer)
1374− | -- | consumer declares a minimum length of the array ($1), producer doesn't.
1375− NoMatchingMinItems Integer
1376− | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
1377− MatchingMinItemsWeak (ProdCons Integer)
1378− | -- | consumer declares that items must be unique, producer doesn't
1379− NoMatchingUniqueItems
1380− | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
1381− NoMatchingProperties
1382− | -- | producer allows additional properties, consumer doesn't
1383− NoAdditionalProperties
1384− | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
1385− NoMatchingMaxProperties Integer
1386− | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
1387− MatchingMaxPropertiesWeak (ProdCons Integer)
1388− | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
1389− NoMatchingMinProperties Integer
1390− | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
1391− MatchingMinPropertiesWeak (ProdCons Integer)
1392− | -- | producer declares that the value must satisfy a disjunction of some conditions, but consumer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
1393− NoMatchingCondition Partition [SomeCondition]
1394− | -- | producer indicates that values of this type are now allowed, but the consumer does not do so (currently we only check immediate contradictions, c.f. #70)
1395− -- AKA consumer does not have the type
1396− NoContradiction
1397− deriving stock (Eq, Ord, Show)
1398− issueIsUnsupported _ = False
1399− describeIssue Forward (EnumDoesntSatisfy v) = para "The following enum value was removed:" <> showJSONValue v
1400− describeIssue Backward (EnumDoesntSatisfy v) = para "The following enum value was added:" <> showJSONValue v
1401− describeIssue Forward (NoMatchingEnum v) = para "The following enum value has been added:" <> showJSONValue v
1402− describeIssue Backward (NoMatchingEnum v) = para "The following enum value has been removed:" <> showJSONValue v
1403− describeIssue Forward (NoMatchingMaximum b) = para $ "Upper bound has been added:" <> showBound b <> "."
1404− describeIssue Backward (NoMatchingMaximum b) = para $ "Upper bound has been removed:" <> showBound b <> "."
1405− describeIssue _ (MatchingMaximumWeak (ProdCons p c)) = para $ "Upper bound changed from " <> showBound p <> " to " <> showBound c <> "."
1406− describeIssue Forward (NoMatchingMinimum b) = para $ "Lower bound has been added: " <> showBound b <> "."
1407− describeIssue Backward (NoMatchingMinimum b) = para $ "Lower bound has been removed: " <> showBound b <> "."
1408− describeIssue _ (MatchingMinimumWeak (ProdCons p c)) = para $ "Lower bound changed from " <> showBound p <> " to " <> showBound c <> "."
1409− describeIssue Forward (NoMatchingMultipleOf n) = para $ "Value is now a multiple of " <> show' n <> "."
1410− describeIssue Backward (NoMatchingMultipleOf n) = para $ "Value is no longer a multiple of " <> show' n <> "."
1411− describeIssue _ (MatchingMultipleOfWeak (ProdCons p c)) = para $ "Value changed from being a multiple of " <> show' p <> " to being a multiple of " <> show' c <> "."
1412− describeIssue Forward (NoMatchingFormat f) = para $ "Format added: " <> code f <> "."
1413− describeIssue Backward (NoMatchingFormat f) = para $ "Format removed: " <> code f <> "."
1414− describeIssue Forward (NoMatchingMaxLength n) = para $ "Maximum length added: " <> show' n <> "."
1415− describeIssue Backward (NoMatchingMaxLength n) = para $ "Maximum length removed: " <> show' n <> "."
1416− describeIssue _ (MatchingMaxLengthWeak (ProdCons p c)) = para $ "Maximum length of the string changed from " <> show' p <> " to " <> show' c <> "."
1417− describeIssue Forward (NoMatchingMinLength n) = para $ "Minimum length of the string added: " <> show' n <> "."
1418− describeIssue Backward (NoMatchingMinLength n) = para $ "Minimum length of the string removed: " <> show' n <> "."
1419− describeIssue _ (MatchingMinLengthWeak (ProdCons p c)) = para $ "Minimum length of the string changed from " <> show' p <> " to " <> show' c <> "."
1420− describeIssue Forward (NoMatchingPattern p) = para "Pattern (regular expression) added: " <> codeBlock p
1421− describeIssue Backward (NoMatchingPattern p) = para "Pattern (regular expression) removed: " <> codeBlock p
1422− describeIssue Forward NoMatchingItems = para "Array item schema has been added."
1423− describeIssue Backward NoMatchingItems = para "Array item schema has been removed."
1424− describeIssue Forward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been added " <> show' n <> "."
1425− describeIssue Backward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been removed " <> show' n <> "."
1426− describeIssue _ (MatchingMaxItemsWeak (ProdCons p c)) = para $ "Maximum length of the array changed from " <> show' p <> " to " <> show' c <> "."
1427− describeIssue Forward (NoMatchingMinItems n) = para $ "Minimum length of the array added: " <> show' n <> "."
1428− describeIssue Backward (NoMatchingMinItems n) = para $ "Minimum length of the array removed: " <> show' n <> "."
1429− describeIssue _ (MatchingMinItemsWeak (ProdCons p c)) = para $ "Minimum length of the array changed from " <> show' p <> " to " <> show' c <> "."
1430− describeIssue Forward NoMatchingUniqueItems = para "Items are now required to be unique."
1431− describeIssue Backward NoMatchingUniqueItems = para "Items are no longer required to be unique."
1432− describeIssue Forward NoMatchingProperties = para "Property added."
1433− describeIssue Backward NoMatchingProperties = para "Property removed."
1434− describeIssue Forward NoAdditionalProperties = para "Additional properties have been removed."
1435− describeIssue Backward NoAdditionalProperties = para "Additional properties have been added."
1436− describeIssue Forward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been added: " <> show' n <> "."
1437− describeIssue Backward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been removed: " <> show' n <> "."
1438− describeIssue _ (MatchingMaxPropertiesWeak (ProdCons p c)) = para $ "Maximum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1439− describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
1440− describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
1441− describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1442− describeIssue _ (NoMatchingCondition mPart conds) =
1443− para
1444− (case mPart of
1445− Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this)"
1446− Just locPart ->
1447− showPartition locPart
1448− <> " – could not verify that the following conditions hold (please file a bug if you see this):")
1449− <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
1450− describeIssue Forward NoContradiction = para "The value has been removed."
1451− describeIssue Backward NoContradiction = para "The value has been added."
1452−
1453−showJSONValue :: A.Value -> Blocks
1454−showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1455−
1456−showJSONValueInline :: A.Value -> Inlines
1457−showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1458−
1459−showBound :: Show a => Bound a -> Inlines
1460−showBound (Inclusive x) = show' x <> " inclusive"
1461−showBound (Exclusive x) = show' x <> " exclusive"
1462−
1463−show' :: Show x => x -> Inlines
1464−show' = str . T.pack . show
1465−
1466−instance Issuable 'SchemaLevel where
1467− data Issue 'SchemaLevel
1468− = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
1469− NotSupported Text
1470− | -- | The schema is actually invalid
1471− InvalidSchema Text
1472− | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
1473− UnguardedRecursion
1474− | -- | Producer doesn't place any restrictions on the types, but the consumer does. List what types remain available in the consumer.
1475− TypesRestricted [JsonType]
1476− | -- | in the producer this field used to be handled as part of "additionalProperties", and the consumer this is a specific "properties" entry. Only thrown when this change actually causes other issues
1477− AdditionalToProperty
1478− | -- | in the consumer this field used to be handled as part of "additionalProperties", and the producer this is a specific "properties" entry. Only thrown when this change actually causes other issues
1479− PropertyToAdditional
1480− | -- | consumer requires a property that is not required/allowed in the producer
1481− PropertyNowRequired
1482− | -- | producer allows a property that is not allowed in the consumer
1483− UnexpectedProperty
1484− deriving stock (Eq, Ord, Show)
1485− issueIsUnsupported = \case
1486− NotSupported _ -> True
1487− InvalidSchema _ -> True
1488− UnguardedRecursion -> True
1489− _ -> False
1490−
1491− describeIssue _ (NotSupported i) =
1492− para (emph "Encountered a feature that OpenApi Diff does not support: " <> text i <> ".")
1493− describeIssue _ (InvalidSchema i) =
1494− para (emph "The schema is invalid: " <> text i <> ".")
1495− describeIssue _ UnguardedRecursion =
1496− para "Encountered recursion that is too complex for OpenApi Diff to untangle."
1497− describeIssue Forward (TypesRestricted tys) = case tys of
1498− [] -> para "No longer has any valid values." -- weird
1499− _ -> para "Values are now limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
1500− describeIssue Backward (TypesRestricted tys) = case tys of
1501− [] -> para "Any value of any type is now allowed." -- weird
1502− _ -> para "Values are no longer limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
1503− describeIssue Forward AdditionalToProperty = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
1504− describeIssue Backward AdditionalToProperty = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
1505− describeIssue Forward PropertyToAdditional = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
1506− describeIssue Backward PropertyToAdditional = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
1507− describeIssue Forward PropertyNowRequired = para "The property has become required."
1508− describeIssue Backward PropertyNowRequired = para "The property may not be present."
1509− describeIssue Forward UnexpectedProperty = para "The property has been removed."
1510− describeIssue Backward UnexpectedProperty = para "The property has been added."
1511−
1512−instance Behavable 'SchemaLevel 'TypedSchemaLevel where
1513− data Behave 'SchemaLevel 'TypedSchemaLevel
1514− = OfType JsonType
1515− deriving stock (Eq, Ord, Show)
1516−
1517− describeBehaviour (OfType t) = describeJSONType t
1518−
1519−describeJSONType :: IsString s => JsonType -> s
1520−describeJSONType = \case
1521− Null -> "Null"
1522− Boolean -> "Boolean"
1523− Number -> "Number"
1524− String -> "String"
1525− Array -> "Array"
1526− Object -> "Object"
1527−
1528−instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
1529− data Behave 'TypedSchemaLevel 'TypedSchemaLevel
1530− = InPartition (PartitionLocation, PartitionChoice)
1531− deriving stock (Eq, Ord, Show)
1532−
1533− describeBehaviour (InPartition partition) = showPartition partition
1534−
1535−showPartition :: (PartitionLocation, PartitionChoice) -> Inlines
1536−showPartition = \case
1537− (partition, CByEnumValue (S.toList -> [v])) ->
1538− "In cases where " <> renderPartitionLocation partition <> " is " <> showJSONValueInline v
1539− (partition, CByEnumValue (S.toList -> vs)) ->
1540− "In cases where " <> renderPartitionLocation partition <> " has values: "
1541− <> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
1542− (partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
1543− "In cases where " <> renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
1544− (partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
1545− "In cases where " <> renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
1546− (partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
1547− "In cases where " <> renderPartitionLocation partition
1548− <> " contains the properties "
1549− <> listCodes incl
1550− <> " and does not contain the properties "
1551− <> listCodes excl
1552− where
1553− listCodes :: [Text] -> Inlines
1554− listCodes = fold . L.intersperse ", " . fmap code
1555− renderPartitionLocation :: PartitionLocation -> Inlines
1556− renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
1557− where
1558− renderPartitionLocation' :: PartitionLocation -> Text
1559− renderPartitionLocation' PHere = mempty
1560− renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
1561−
1562−instance Behavable 'TypedSchemaLevel 'SchemaLevel where
1563− data Behave 'TypedSchemaLevel 'SchemaLevel
1564− = InItems
1565− | InProperty Text
1566− | InAdditionalProperty
1567− deriving stock (Eq, Ord, Show)
1568−
1569− describeBehaviour InItems = "Items"
1570− describeBehaviour (InProperty p) = "Property " <> code p
1571− describeBehaviour InAdditionalProperty = "Additional properties"
1572−
1573−instance Subtree Schema where
1574− type SubtreeLevel Schema = 'SchemaLevel
1575− type CheckEnv Schema = '[ProdCons (Traced (Definitions Schema))]
1576− checkStructuralCompatibility env pc = do
1577− structuralEq $ fmap _schemaRequired <$> pc
1578− structuralEq $ fmap _schemaNullable <$> pc
1579− structuralMaybeWith (structuralList env) $ tracedAllOf <$> pc
1580− structuralMaybeWith (structuralList env) $ tracedOneOf <$> pc
1581− structuralMaybe env $ sequence . stepTraced NotStep . fmap _schemaNot <$> pc
1582− structuralMaybeWith (structuralList env) $ tracedAnyOf <$> pc
1583− iohmStructural env $ stepTraced PropertiesStep . fmap _schemaProperties <$> pc
1584− structuralMaybeWith structuralAdditionalProperties $ tracedAdditionalProperties <$> pc
1585− structuralMaybeWith structuralDiscriminator $ tracedDiscriminator <$> pc
1586− structuralEq $ fmap _schemaReadOnly <$> pc
1587− structuralEq $ fmap _schemaWriteOnly <$> pc
1588− structuralEq $ fmap _schemaXml <$> pc
1589− structuralEq $ fmap _schemaMaxProperties <$> pc
1590− structuralEq $ fmap _schemaMinProperties <$> pc
1591− structuralEq $ fmap _schemaDefault <$> pc
1592− structuralEq $ fmap _schemaType <$> pc
1593− structuralEq $ fmap _schemaFormat <$> pc
1594− structuralMaybeWith structuralItems $ tracedItems <$> pc
1595− structuralEq $ fmap _schemaMaximum <$> pc
1596− structuralEq $ fmap _schemaExclusiveMaximum <$> pc
1597− structuralEq $ fmap _schemaMinimum <$> pc
1598− structuralEq $ fmap _schemaExclusiveMinimum <$> pc
1599− structuralEq $ fmap _schemaMaxLength <$> pc
1600− structuralEq $ fmap _schemaMinLength <$> pc
1601− structuralEq $ fmap _schemaPattern <$> pc
1602− structuralEq $ fmap _schemaMaxItems <$> pc
1603− structuralEq $ fmap _schemaMinItems <$> pc
1604− structuralEq $ fmap _schemaUniqueItems <$> pc
1605− structuralEq $ fmap _schemaEnum <$> pc
1606− structuralEq $ fmap _schemaMultipleOf <$> pc
1607− pure ()
1608− where
1609− structuralAdditionalProperties
1610− (ProdCons (Left x) (Left y)) = unless (x == y) structuralIssue
1611− structuralAdditionalProperties
1612− (ProdCons (Right x) (Right y)) =
1613− checkSubstructure env $ ProdCons x y
1614− structuralAdditionalProperties _ = structuralIssue
1615− structuralDiscriminator pc' = do
1616− structuralEq $ fmap _discriminatorPropertyName <$> pc'
1617− iohmStructural env $
1618− stepTraced DiscriminatorMapping . fmap (fmap parseDiscriminatorValue . _discriminatorMapping) <$> pc'
1619− pure ()
1620− structuralItems (ProdCons (Left a) (Left b)) =
1621− checkSubstructure env $ ProdCons a b
1622− structuralItems (ProdCons (Right a) (Right b)) =
1623− structuralList env $ ProdCons a b
1624− structuralItems _ = structuralIssue
1625− checkSemanticCompatibility env beh schs = do
1626− let defs = getH env
1627− checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs
src/OpenAPI/Checker/Validate/Sums.hsdeleted−24
@@ -1,24 +0,0 @@
1−module OpenAPI.Checker.Validate.Sums
2− ( checkSums
3− ) where
4−
5−import Data.Foldable
6−import Data.Map.Strict (Map)
7−import qualified Data.Map.Strict as M
8−import OpenAPI.Checker.Behavior
9−import OpenAPI.Checker.Subtree
10−import OpenAPI.Checker.Paths
11−
12−checkSums
13− :: (Ord k, Issuable l)
14− => Paths q r l
15− -> (k -> Issue l)
16− -> (k -> ProdCons t -> CompatFormula' q AnIssue r ())
17− -> ProdCons (Map k t)
18− -> CompatFormula' q AnIssue r ()
19−checkSums xs noElt check (ProdCons p c) = for_ (M.toList p) $ \(key, prodElt) ->
20− case M.lookup key c of
21− Nothing -> issueAt xs $ noElt key
22− Just consElt ->
23− let sumElts = ProdCons prodElt consElt
24− in check key sumElts
stack.yaml+18−3
@@ -1,8 +1,23 @@
1−resolver: lts-18.2
1+resolver: lts-22.34
2+
3+packages:
4+ - .
2
5
3extra-deps:
6extra-deps:
7+ - base16-bytestring-1.0.2.0
8+ - freer-simple-1.2.1.2
9+ - generics-sop-0.5.1.4
10+ - github-0.29
11+ - http-link-header-1.2.1
4 - open-union-0.4.0.0
12 - open-union-0.4.0.0
13+ - th-abstraction-0.7.0.0
5 - type-fun-0.1.3
14 - type-fun-0.1.3
15+ - typerep-map-0.6.0.0
16+
17+# Required for freer-simple and typerep-map
18+allow-newer: true
6
19
7−apply-ghc-options: everything
20+nix:
8−rebuild-ghc-options: true
21+# packages: [pkg-config yq zlib]
22+ shell-file: ./nix/shell.nix
23+ pure: false
stack.yaml.lock+25−4
@@ -18,9 +18,30 @@ packages:
18 sha256: b977d42525f0b4223959918d18e7905085283e937493c395077b608cd19b42c1
18 sha256: b977d42525f0b4223959918d18e7905085283e937493c395077b608cd19b42c1
19 original:
19 original:
20 hackage: type-fun-0.1.3
20 hackage: type-fun-0.1.3
21+- completed:
22+ hackage: github-0.26@sha256:a9d4046325c3eb28cdc7bef2c3f5bb213328caeae0b7dce6f51de655f0bffaa1,7162
23+ pantry-tree:
24+ size: 7511
25+ sha256: b71aab2984b268030c9e2617043575681134c1fe60dffbd5596e659c0a3e9aec
26+ original:
27+ hackage: github-0.26
28+- completed:
29+ hackage: base16-bytestring-0.1.1.7@sha256:0021256a9628971c08da95cb8f4d0d72192f3bb8a7b30b55c080562d17c43dd3,2231
30+ pantry-tree:
31+ size: 454
32+ sha256: 2dc99cd67293b09204098202d0bdcdcf89d2b8c69ce6c8f4e31deadbdab2a7f9
33+ original:
34+ hackage: base16-bytestring-0.1.1.7
35+- completed:
36+ hackage: http-link-header-1.0.3.1@sha256:9917c26635a46eff5d36c5344a4ae42fad2b5de79c0e300363bddd5eec66facd,1937
37+ pantry-tree:
38+ size: 990
39+ sha256: 155feaf856daf5b55491048c8c4726ba765a8fe79263ebdbfc82f6c385a15715
40+ original:
41+ hackage: http-link-header-1.0.3.1
21snapshots:
42snapshots:
22- completed:
43- completed:
23− size: 585392
44+ size: 587393
24− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/2.yaml
45+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/25.yaml
25− sha256: 7abb45c0cc5eb349448b66d8753655542d45d387ad26970419282eab3d860724
46+ sha256: 1b74fb5e970497b5aefae56703f1bd44aa648bd1a5ef95c1eb8c29775087e2bf
26− original: lts-18.2
47+ original: lts-18.25
test/README.md+1−1
@@ -30,7 +30,7 @@ This test would read the files `a.yaml` and `b.yaml` using the `Yaml.decodeFileT
30
30
31## Supported feature tests
31## Supported feature tests
32
32
33−If a test starts with either `x ` or `v `, the test is assumed to test the support of some OpenApi functionality. If the test begins `x ` it means that the feature is expected to be not supported, and the result will only be checked to have an "unsupported" flag set. If it starts with `v ` the test is expected to be supported and the result is compared to some file on disk.
33+If a test starts with either `x ` or `v `, the test is assumed to test the support of some OpenAPI functionality. If the test begins `x ` it means that the feature is expected to be not supported, and the result will only be checked to have an "unsupported" flag set. If it starts with `v ` the test is expected to be supported and the result is compared to some file on disk.
34
34
35The result will be reflected in the generated compatibility matrix.
35The result will be reflected in the generated compatibility matrix.
36
36
test/Spec/Golden/Extra.hs+48−44
@@ -1,8 +1,8 @@
1module Spec.Golden.Extra
1module Spec.Golden.Extra
2− ( getGoldenInputs
2+ ( getGoldenInputs,
3− , getGoldenInputsUniform
3+ getGoldenInputsUniform,
4− , goldenInputsTree
4+ goldenInputsTree,
5− , goldenInputsTreeUniform
5+ goldenInputsTreeUniform,
6 )
6 )
7where
7where
8
8
@@ -19,12 +19,12 @@ data TestInput t
19 | TestInputLeaf TestName t FilePath
19 | TestInputLeaf TestName t FilePath
20 deriving stock (Functor)
20 deriving stock (Functor)
21
21
22−getGoldenInputs
22+getGoldenInputs ::
23− :: (Each s t (FilePath, FilePath -> IO a) a)
23+ (Each s t (FilePath, FilePath -> IO a) a) =>
24− => TestName
24+ TestName ->
25− -> FilePath
25+ FilePath ->
26− -> s
26+ s ->
27− -> IO (TestInput t)
27+ IO (TestInput t)
28getGoldenInputs name filepath inp = do
28getGoldenInputs name filepath inp = do
29 dirs' <- listDirectory filepath >>= filterM (doesDirectoryExist . (filepath </>))
29 dirs' <- listDirectory filepath >>= filterM (doesDirectoryExist . (filepath </>))
30 case dirs' of
30 case dirs' of
@@ -39,51 +39,55 @@ getGoldenInputs name filepath inp = do
39 TestInputNode name
39 TestInputNode name
40 <$> forM dirs (\dir -> getGoldenInputs dir (filepath </> dir) inp)
40 <$> forM dirs (\dir -> getGoldenInputs dir (filepath </> dir) inp)
41
41
42−getGoldenInputsUniform
42+getGoldenInputsUniform ::
43− :: (Each t h (FilePath, FilePath -> IO a) a)
43+ (Each t h (FilePath, FilePath -> IO a) a) =>
44− => (Each s t FilePath (FilePath, FilePath -> IO a))
44+ (Each s t FilePath (FilePath, FilePath -> IO a)) =>
45− => TestName
45+ TestName ->
46− -> (FilePath -> IO a)
46+ (FilePath -> IO a) ->
47− -> FilePath
47+ FilePath ->
48− -> s
48+ s ->
49− -> IO (TestInput h)
49+ IO (TestInput h)
50getGoldenInputsUniform name f filepath inp = getGoldenInputs name filepath $ inp & each %~ (,f)
50getGoldenInputsUniform name f filepath inp = getGoldenInputs name filepath $ inp & each %~ (,f)
51
51
52−goldenInputsTree
52+goldenInputsTree ::
53− :: (Each s t (FilePath, FilePath -> IO a) a)
53+ (Each s t (FilePath, FilePath -> IO a) a) =>
54− => TestName
54+ TestName ->
55− -> FilePath -- ^ Root path
55+ -- | Root path
56+ FilePath ->
56− -> FilePath -- ^ Name of golden file
57+ -- | Name of golden file
57− -> s
58+ FilePath ->
59+ s ->
58− -> (t -> IO BSL.ByteString)
60+ (t -> IO BSL.ByteString) ->
59− -> IO TestTree
61+ IO TestTree
60−goldenInputsTree name filepath golden inp f = do
62+goldenInputsTree name filepath golden inp f =
61 runTestInputTree golden f <$> getGoldenInputs name filepath inp
63 runTestInputTree golden f <$> getGoldenInputs name filepath inp
62
64
63−runTestInputTree
65+runTestInputTree ::
64− :: FilePath
66+ FilePath ->
65− -> (t -> IO BSL.ByteString)
67+ (t -> IO BSL.ByteString) ->
66− -> TestInput t
68+ TestInput t ->
67− -> TestTree
69+ TestTree
68runTestInputTree golden f (TestInputNode name rest) =
70runTestInputTree golden f (TestInputNode name rest) =
69 testGroup name (runTestInputTree golden f <$> rest)
71 testGroup name (runTestInputTree golden f <$> rest)
70runTestInputTree golden f (TestInputLeaf name t path) =
72runTestInputTree golden f (TestInputLeaf name t path) =
71 goldenVsStringDiff
73 goldenVsStringDiff
72 name
74 name
73− (\ref new -> ["diff", "-u", ref, new])
75+ (\ref new -> ["diff-yaml", ref, new])
74 (path </> golden)
76 (path </> golden)
75 (f t)
77 (f t)
76
78
77−goldenInputsTreeUniform
79+goldenInputsTreeUniform ::
78− :: ( Each t h (FilePath, FilePath -> IO a) a
80+ ( Each t h (FilePath, FilePath -> IO a) a
79− , Each s t FilePath (FilePath, FilePath -> IO a)
81+ , Each s t FilePath (FilePath, FilePath -> IO a)
80− )
82+ ) =>
81− => String
83+ String ->
82− -> FilePath -- ^ Root path
84+ -- | Root path
85+ FilePath ->
83− -> FilePath -- ^ Name of golden file
86+ -- | Name of golden file
84− -> s
87+ FilePath ->
88+ s ->
85− -> (FilePath -> IO a)
89+ (FilePath -> IO a) ->
86− -> (h -> IO BSL.ByteString)
90+ (h -> IO BSL.ByteString) ->
87− -> IO TestTree
91+ IO TestTree
88goldenInputsTreeUniform name filepath golden inp h =
92goldenInputsTreeUniform name filepath golden inp h =
89 goldenInputsTree name filepath golden (inp & each %~ (,h))
93 goldenInputsTree name filepath golden (inp & each %~ (,h))
test/Spec/Golden/TraceTree.hs+7−6
@@ -1,5 +1,5 @@
1module Spec.Golden.TraceTree
1module Spec.Golden.TraceTree
2− ( tests
2+ ( tests,
3 )
3 )
4where
4where
5
5
@@ -7,13 +7,14 @@ import Control.Category
7import Control.Exception
7import Control.Exception
8import qualified Data.ByteString.Lazy as BSL
8import qualified Data.ByteString.Lazy as BSL
9import Data.Default
9import Data.Default
10+import Data.OpenApi.Compare.Run
11+import Data.OpenApi.Compare.Validate.OpenApi ()
10import Data.Text (Text)
12import Data.Text (Text)
11import qualified Data.Text.Encoding as T
13import qualified Data.Text.Encoding as T
12import qualified Data.Yaml as Yaml
14import qualified Data.Yaml as Yaml
13−import OpenAPI.Checker.Run
14−import OpenAPI.Checker.Validate.OpenApi ()
15import Spec.Golden.Extra
15import Spec.Golden.Extra
16import Test.Tasty (TestTree, testGroup)
16import Test.Tasty (TestTree, testGroup)
17+import Text.Pandoc.Builder
17import Text.Pandoc.Class
18import Text.Pandoc.Class
18import Text.Pandoc.Options
19import Text.Pandoc.Options
19import Text.Pandoc.Writers
20import Text.Pandoc.Writers
@@ -24,7 +25,7 @@ tests = do
24 traceTreeTests <-
25 traceTreeTests <-
25 goldenInputsTreeUniform
26 goldenInputsTreeUniform
26 "TraceTree"
27 "TraceTree"
27− "test/golden/common"
28+ "test/golden"
28 "trace-tree.yaml"
29 "trace-tree.yaml"
29 ("a.yaml", "b.yaml")
30 ("a.yaml", "b.yaml")
30 Yaml.decodeFileThrow
31 Yaml.decodeFileThrow
@@ -33,11 +34,11 @@ tests = do
33 reportTests <-
34 reportTests <-
34 goldenInputsTreeUniform
35 goldenInputsTreeUniform
35 "Report"
36 "Report"
36− "test/golden/common"
37+ "test/golden"
37 "report.md"
38 "report.md"
38 ("a.yaml", "b.yaml")
39 ("a.yaml", "b.yaml")
39 Yaml.decodeFileThrow
40 Yaml.decodeFileThrow
40− (runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . fst . runReport def)
41+ (runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . doc . fst . runReport def)
41
42
42 return $ testGroup "Golden tests" [traceTreeTests, reportTests]
43 return $ testGroup "Golden tests" [traceTreeTests, reportTests]
43
44
test/golden/common/bad-oneof-unchanged/a.yamlnew file+39
@@ -0,0 +1,39 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ oneOf:
26+ - type: object
27+ required: ["tag", "prop_A"]
28+ properties:
29+ tag:
30+ enum: ["A"]
31+ prop_A:
32+ type: string
33+ - type: object
34+ required: ["tag", "prop_B"]
35+ properties:
36+ tag:
37+ enum: ["A"]
38+ prop_B:
39+ type: number
test/golden/common/bad-oneof-unchanged/b.yamlnew file+39
@@ -0,0 +1,39 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ oneOf:
26+ - type: object
27+ required: ["tag", "prop_B"]
28+ properties:
29+ tag:
30+ enum: ["A"]
31+ prop_B:
32+ type: number
33+ - type: object
34+ required: ["tag", "prop_A"]
35+ properties:
36+ tag:
37+ enum: ["A"]
38+ prop_A:
39+ type: string
test/golden/common/bad-oneof-unchanged/report.mdnew file+19
@@ -0,0 +1,19 @@
1+# Summary
2+
3+| ❌ Breaking changes | ⚠️ Non-breaking changes | [❓ Unsupported feature changes](#unsupported-changes) |
4+|---------------------|-------------------------|--------------------------------------------------------|
5+| 0 | 0 | 2 |
6+
7+# <span id="unsupported-changes"></span>❓ Unsupported feature changes
8+
9+## **POST** /test
10+
11+### ➡️☁️ JSON Request
12+
13+Could not deduce that `oneOf` cases don't overlap. Treating the `oneOf`
14+as an `anyOf`. Reported errors might not be accurate.
15+
16+### ⬅️☁️ JSON Response – 200
17+
18+Could not deduce that `oneOf` cases don't overlap. Treating the `oneOf`
19+as an `anyOf`. Reported errors might not be accurate.
test/golden/common/bad-oneof-unchanged/trace-tree.yamlnew file+18
@@ -0,0 +1,18 @@
1+forwardChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema: OneOfNotDisjoint
7+ WithStatusCode 200:
8+ ResponsePayload:
9+ PayloadSchema: OneOfNotDisjoint
10+backwardChanges:
11+ AtPath "/test":
12+ InOperation PostMethod:
13+ InRequest:
14+ InPayload:
15+ PayloadSchema: OneOfNotDisjoint
16+ WithStatusCode 200:
17+ ResponsePayload:
18+ PayloadSchema: OneOfNotDisjoint
test/golden/common/enum-added/report.md+7−7
@@ -1,14 +1,14 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱⬅️ JSON Response – 200
11+### ⬅️☁️ JSON Response – 200
12
12
13#### `$(String)`
13#### `$(String)`
14
14
@@ -18,11 +18,11 @@ The following enum value was added:
18"C"
18"C"
19```
19```
20
20
21−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
21+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
22
22
23## **POST** /test
23## **POST** /test
24
24
25−### 📱➡️ JSON Request
25+### ➡️☁️ JSON Request
26
26
27#### `$(String)`
27#### `$(String)`
28
28
test/golden/common/enum-added/trace-tree.yaml+2−2
@@ -1,11 +1,11 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200:
4 WithStatusCode 200:
5 ResponsePayload:
5 ResponsePayload:
6 PayloadSchema:
6 PayloadSchema:
7 OfType String: EnumDoesntSatisfy (String "C")
7 OfType String: EnumDoesntSatisfy (String "C")
8−nonBreakingChanges:
8+backwardChanges:
9 AtPath "/test":
9 AtPath "/test":
10 InOperation PostMethod:
10 InOperation PostMethod:
11 InRequest:
11 InRequest:
test/golden/common/enum-anyof/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/enum-anyof/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/id/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/id/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/json/recursive/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/json/recursive/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/maximum-lowered/report.md+7−7
@@ -1,24 +1,24 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### `$(Number)`
13#### `$(Number)`
14
14
15Upper bound changed from 3.0 inclusive to 2.0 inclusive.
15Upper bound changed from 3.0 inclusive to 2.0 inclusive.
16
16
17−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
18
18
19## **POST** /test
19## **POST** /test
20
20
21−### 📱⬅️ JSON Response – 200
21+### ⬅️☁️ JSON Response – 200
22
22
23#### `$(Number)`
23#### `$(Number)`
24
24
test/golden/common/maximum-lowered/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -6,7 +6,7 @@ breakingChanges:
6 PayloadSchema:
6 PayloadSchema:
7 OfType Number: MatchingMaximumWeak (ProdCons {producer = Inclusive 3.0,
7 OfType Number: MatchingMaximumWeak (ProdCons {producer = Inclusive 3.0,
8 consumer = Inclusive 2.0})
8 consumer = Inclusive 2.0})
9−nonBreakingChanges:
9+backwardChanges:
10 AtPath "/test":
10 AtPath "/test":
11 InOperation PostMethod:
11 InOperation PostMethod:
12 WithStatusCode 200:
12 WithStatusCode 200:
test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InParam "test1": ParamEmptinessIncompatible
4 InParam "test1": ParamEmptinessIncompatible
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/parameters/allowEmptyValue/set/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/allowEmptyValue/set/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 InParam "test1": ParamEmptinessIncompatible
5 InParam "test1": ParamEmptinessIncompatible
test/golden/common/pathItem/operation/parameters/change/report.md+7−7
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
@@ -14,9 +14,9 @@
14
14
15##### `$(String)`
15##### `$(String)`
16
16
17−The value has been removed.
17+The type has been removed.
18
18
19−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
19+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
20
20
21## **POST** /test
21## **POST** /test
22
22
@@ -26,4 +26,4 @@ The value has been removed.
26
26
27##### `$(Number)`
27##### `$(Number)`
28
28
29−The value has been added.
29+The type has been added.
test/golden/common/pathItem/operation/parameters/change/trace-tree.yaml+4−4
@@ -1,12 +1,12 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InParam "test":
4 InParam "test":
5 InParamSchema:
5 InParamSchema:
6− OfType String: NoContradiction
6+ OfType String: TypeBecomesEmpty
7−nonBreakingChanges:
7+backwardChanges:
8 AtPath "/test":
8 AtPath "/test":
9 InOperation PostMethod:
9 InOperation PostMethod:
10 InParam "test":
10 InParam "test":
11 InParamSchema:
11 InParamSchema:
12− OfType Number: NoContradiction
12+ OfType Number: TypeBecomesEmpty
test/golden/common/pathItem/operation/parameters/required/false/add/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/pathItem/operation/parameters/required/false/add/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/pathItem/operation/parameters/required/false/del/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/pathItem/operation/parameters/required/false/del/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/pathItem/operation/parameters/required/reset/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/required/reset/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 InParam "test1": ParamRequired
5 InParam "test1": ParamRequired
test/golden/common/pathItem/operation/parameters/required/set/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/required/set/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InParam "test1": ParamRequired
4 InParam "test1": ParamRequired
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/parameters/required/true/add/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/required/true/add/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod: ParamNotMatched "test2"
3 InOperation PostMethod: ParamNotMatched "test2"
4−nonBreakingChanges: {}
4+backwardChanges: {}
test/golden/common/pathItem/operation/parameters/required/true/del/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/parameters/required/true/del/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod: ParamNotMatched "test2"
4 InOperation PostMethod: ParamNotMatched "test2"
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/add/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/add/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 InRequest: RequestMediaTypeNotFound application/x-www-form-urlencoded
5 InRequest: RequestMediaTypeNotFound application/x-www-form-urlencoded
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+9−9
@@ -1,25 +1,25 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### `$(String)`
13#### `$(String)`
14
14
15−The value has been removed.
15+The type has been removed.
16
16
17−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
18
18
19## **POST** /test
19## **POST** /test
20
20
21−### 📱➡️ JSON Request
21+### ➡️☁️ JSON Request
22
22
23#### `$(Number)`
23#### `$(Number)`
24
24
25−The value has been added.
25+The type has been added.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/trace-tree.yaml+4−4
@@ -1,14 +1,14 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
5 InPayload:
5 InPayload:
6 PayloadSchema:
6 PayloadSchema:
7− OfType String: NoContradiction
7+ OfType String: TypeBecomesEmpty
8−nonBreakingChanges:
8+backwardChanges:
9 AtPath "/test":
9 AtPath "/test":
10 InOperation PostMethod:
10 InOperation PostMethod:
11 InRequest:
11 InRequest:
12 InPayload:
12 InPayload:
13 PayloadSchema:
13 PayloadSchema:
14− OfType Number: NoContradiction
14+ OfType Number: TypeBecomesEmpty
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest: RequestMediaTypeNotFound application/x-www-form-urlencoded
4 InRequest: RequestMediaTypeNotFound application/x-www-form-urlencoded
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/requestBody/required/reset/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/requestBody/required/reset/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 InRequest: RequestBodyRequired
5 InRequest: RequestBodyRequired
test/golden/common/pathItem/operation/requestBody/required/set/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/requestBody/required/set/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest: RequestBodyRequired
4 InRequest: RequestBodyRequired
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/responses/add/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/add/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod: ConsumerDoesntHaveResponseCode 500
3 InOperation PostMethod: ConsumerDoesntHaveResponseCode 500
4−nonBreakingChanges: {}
4+backwardChanges: {}
test/golden/common/pathItem/operation/responses/change/headers/mandatory/add/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/change/headers/mandatory/add/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 WithStatusCode 200: ProducerDoesntHaveResponseHeader "Test2"
5 WithStatusCode 200: ProducerDoesntHaveResponseHeader "Test2"
test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200: ProducerDoesntHaveResponseHeader "Test2"
4 WithStatusCode 200: ProducerDoesntHaveResponseHeader "Test2"
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/responses/change/headers/optional/add/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/pathItem/operation/responses/change/headers/optional/add/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/pathItem/operation/responses/change/headers/optional/del/report.md+3−3
@@ -1,5 +1,5 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4−|---------------------|------------------------|-------------------------------|
4+|---------------------|-------------------------|
5−| 0 | 0 | 0 |
5+| 0 | 0 |
test/golden/common/pathItem/operation/responses/change/headers/optional/del/trace-tree.yaml+2−2
@@ -1,2 +1,2 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges: {}
2+backwardChanges: {}
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | 🙆 Non-breaking changes | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 1 | 0 | 0 |
5+| 1 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200: ConsumerDoesntHaveMediaType application/x-www-form-urlencoded
4 WithStatusCode 200: ConsumerDoesntHaveMediaType application/x-www-form-urlencoded
5−nonBreakingChanges: {}
5+backwardChanges: {}
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+9−9
@@ -1,25 +1,25 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱⬅️ JSON Response – 200
11+### ⬅️☁️ JSON Response – 200
12
12
13#### `$(Number)`
13#### `$(Number)`
14
14
15−The value has been added.
15+The type has been added.
16
16
17−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
18
18
19## **POST** /test
19## **POST** /test
20
20
21−### 📱⬅️ JSON Response – 200
21+### ⬅️☁️ JSON Response – 200
22
22
23#### `$(String)`
23#### `$(String)`
24
24
25−The value has been removed.
25+The type has been removed.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/trace-tree.yaml+4−4
@@ -1,14 +1,14 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200:
4 WithStatusCode 200:
5 ResponsePayload:
5 ResponsePayload:
6 PayloadSchema:
6 PayloadSchema:
7− OfType Number: NoContradiction
7+ OfType Number: TypeBecomesEmpty
8−nonBreakingChanges:
8+backwardChanges:
9 AtPath "/test":
9 AtPath "/test":
10 InOperation PostMethod:
10 InOperation PostMethod:
11 WithStatusCode 200:
11 WithStatusCode 200:
12 ResponsePayload:
12 ResponsePayload:
13 PayloadSchema:
13 PayloadSchema:
14− OfType String: NoContradiction
14+ OfType String: TypeBecomesEmpty
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/del/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/del/trace-tree.yaml+2−2
@@ -1,5 +1,5 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod:
4 InOperation PostMethod:
5 WithStatusCode 200: ConsumerDoesntHaveMediaType application/x-www-form-urlencoded
5 WithStatusCode 200: ConsumerDoesntHaveMediaType application/x-www-form-urlencoded
test/golden/common/pathItem/operation/responses/del/report.md+4−4
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| ⚠️ Breaking changes | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| ❌ Breaking changes | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|---------------------|-------------------------------------------------|-------------------------------|
4+|---------------------|--------------------------------------------------|
5−| 0 | 1 | 0 |
5+| 0 | 1 |
6
6
7−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
7+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
test/golden/common/pathItem/operation/responses/del/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges: {}
1+forwardChanges: {}
2−nonBreakingChanges:
2+backwardChanges:
3 AtPath "/test":
3 AtPath "/test":
4 InOperation PostMethod: ConsumerDoesntHaveResponseCode 500
4 InOperation PostMethod: ConsumerDoesntHaveResponseCode 500
test/golden/common/property-removed-additional/report.md+7−7
@@ -1,14 +1,14 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 3 | 3 | 0 |
5+| 3 | 3 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱⬅️ JSON Response – 200
11+### ⬅️☁️ JSON Response – 200
12
12
13#### `$.property2`
13#### `$.property2`
14
14
@@ -21,11 +21,11 @@
21
21
223. The property may not be present.
223. The property may not be present.
23
23
24−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
24+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
25
25
26## **POST** /test
26## **POST** /test
27
27
28−### 📱➡️ JSON Request
28+### ➡️☁️ JSON Request
29
29
30#### `$.property2`
30#### `$.property2`
31
31
test/golden/common/property-removed-additional/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200:
4 WithStatusCode 200:
@@ -9,7 +9,7 @@ breakingChanges:
9 - TypesRestricted [Number]
9 - TypesRestricted [Number]
10 - AdditionalToProperty
10 - AdditionalToProperty
11 - PropertyNowRequired
11 - PropertyNowRequired
12−nonBreakingChanges:
12+backwardChanges:
13 AtPath "/test":
13 AtPath "/test":
14 InOperation PostMethod:
14 InOperation PostMethod:
15 InRequest:
15 InRequest:
test/golden/common/property-removed/report.md+6−22
@@ -1,37 +1,21 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|-------------------------|
5−| 2 | 2 | 0 |
5+| 2 | 0 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### `$.property2`
13#### `$.property2`
14
14
15The property has been removed.
15The property has been removed.
16
16
17−### 📱⬅️ JSON Response – 200
17+### ⬅️☁️ JSON Response – 200
18
18
19#### `$.property2`
19#### `$.property2`
20
20
21The property may not be present.
21The property may not be present.
22−
23−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
24−
25−## **POST** /test
26−
27−### 📱➡️ JSON Request
28−
29−#### `$.property2`
30−
31−The property may not be present.
32−
33−### 📱⬅️ JSON Response – 200
34−
35−#### `$.property2`
36−
37−The property has been removed.
test/golden/common/property-removed/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -11,7 +11,7 @@ breakingChanges:
11 PayloadSchema:
11 PayloadSchema:
12 OfType Object:
12 OfType Object:
13 InProperty "property2": PropertyNowRequired
13 InProperty "property2": PropertyNowRequired
14−nonBreakingChanges:
14+backwardChanges:
15 AtPath "/test":
15 AtPath "/test":
16 InOperation PostMethod:
16 InOperation PostMethod:
17 InRequest:
17 InRequest:
test/golden/common/property-required/report.md+7−7
@@ -1,24 +1,24 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### `$.property2`
13#### `$.property2`
14
14
15The property has become required.
15The property has become required.
16
16
17−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
18
18
19## **POST** /test
19## **POST** /test
20
20
21−### 📱⬅️ JSON Response – 200
21+### ⬅️☁️ JSON Response – 200
22
22
23#### `$.property2`
23#### `$.property2`
24
24
test/golden/common/property-required/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -6,7 +6,7 @@ breakingChanges:
6 PayloadSchema:
6 PayloadSchema:
7 OfType Object:
7 OfType Object:
8 InProperty "property2": PropertyNowRequired
8 InProperty "property2": PropertyNowRequired
9−nonBreakingChanges:
9+backwardChanges:
10 AtPath "/test":
10 AtPath "/test":
11 InOperation PostMethod:
11 InOperation PostMethod:
12 WithStatusCode 200:
12 WithStatusCode 200:
test/golden/common/random-example/a.yamlnew file+56
@@ -0,0 +1,56 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Swagger Petstore
5+ license:
6+ name: MIT
7+servers:
8+ - url: https://example.com
9+paths:
10+ /pets:
11+ get:
12+ parameters:
13+ - name: limit
14+ in: query
15+ required: false
16+ schema:
17+ type: integer
18+ maximum: 20
19+ responses:
20+ "200":
21+ description: ""
22+ headers:
23+ x-next:
24+ schema:
25+ type: string
26+ content:
27+ application/json:
28+ schema:
29+ $ref: "#/components/schemas/Pets"
30+ post:
31+ requestBody:
32+ content:
33+ application/json:
34+ schema:
35+ $ref: "#/components/schemas/Pet"
36+ responses:
37+ "201":
38+ description: ""
39+components:
40+ schemas:
41+ Pet:
42+ type: object
43+ required:
44+ - id
45+ - name
46+ properties:
47+ id:
48+ type: integer
49+ name:
50+ type: string
51+ minLength: 3
52+ maxLength: 10
53+ Pets:
54+ type: array
55+ items:
56+ $ref: "#/components/schemas/Pet"
test/golden/common/random-example/b.yamlnew file+58
@@ -0,0 +1,58 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.1.0
4+ title: Swagger Petstore
5+ license:
6+ name: MIT
7+servers:
8+ - url: https://example.com
9+paths:
10+ /pets:
11+ get:
12+ parameters:
13+ - name: limit
14+ in: query
15+ required: false
16+ schema:
17+ type: integer
18+ maximum: 30
19+ responses:
20+ "200":
21+ description: ""
22+ headers:
23+ x-next:
24+ schema:
25+ type: string
26+ content:
27+ application/json:
28+ schema:
29+ $ref: "#/components/schemas/Pets"
30+ post:
31+ requestBody:
32+ content:
33+ application/json:
34+ schema:
35+ $ref: "#/components/schemas/Pet"
36+ responses:
37+ "201":
38+ description: ""
39+components:
40+ schemas:
41+ Pet:
42+ type: object
43+ required:
44+ - id
45+ - name
46+ properties:
47+ id:
48+ type: integer
49+ name:
50+ type: string
51+ minLength: 1
52+ maxLength: 15
53+ weight:
54+ type: integer
55+ Pets:
56+ type: array
57+ items:
58+ $ref: "#/components/schemas/Pet"
test/golden/common/random-example/report.mdnew file+71
@@ -0,0 +1,71 @@
1+# Summary
2+
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4+|------------------------------------------|--------------------------------------------------|
5+| 5 | 6 |
6+
7+# <span id="breaking-changes"></span>❌ Breaking changes
8+
9+## **GET** /pets
10+
11+### ⬅️☁️ JSON Response – 200
12+
13+#### `$[*].name(String)`
14+
15+1. Maximum length of the string changed from 10 to 15.
16+
17+2. Minimum length of the string changed from 3 to 1.
18+
19+## **POST** /pets
20+
21+### ➡️☁️ JSON Request
22+
23+#### `$.weight`
24+
25+1. Values are now limited to the following types:
26+
27+ - Number
28+
29+2. The property was previously implicitly described by the catch-all
30+ "additional properties" case. It is now explicitly defined.
31+
32+#### `$.weight(Number)`
33+
34+Value is now a multiple of 1.0.
35+
36+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
37+
38+## **GET** /pets
39+
40+### Parameter limit
41+
42+#### JSON Schema
43+
44+##### `$(Number)`
45+
46+Upper bound changed from 20.0 inclusive to 30.0 inclusive.
47+
48+### ⬅️☁️ JSON Response – 200
49+
50+#### `$[*].weight`
51+
52+1. Values are now limited to the following types:
53+
54+ - Number
55+
56+2. The property was previously implicitly described by the catch-all
57+ "additional properties" case. It is now explicitly defined.
58+
59+#### `$[*].weight(Number)`
60+
61+Value is now a multiple of 1.0.
62+
63+## **POST** /pets
64+
65+### ➡️☁️ JSON Request
66+
67+#### `$.name(String)`
68+
69+1. Maximum length of the string changed from 10 to 15.
70+
71+2. Minimum length of the string changed from 3 to 1.
test/golden/common/random-example/trace-tree.yamlnew file+48
@@ -0,0 +1,48 @@
1+forwardChanges:
2+ AtPath "/pets":
3+ InOperation GetMethod:
4+ WithStatusCode 200:
5+ ResponsePayload:
6+ PayloadSchema:
7+ OfType Array:
8+ InItems:
9+ OfType Object:
10+ InProperty "name":
11+ OfType String:
12+ - MatchingMaxLengthWeak (ProdCons {producer = 15, consumer = 10})
13+ - MatchingMinLengthWeak (ProdCons {producer = 1, consumer = 3})
14+ InOperation PostMethod:
15+ InRequest:
16+ InPayload:
17+ PayloadSchema:
18+ OfType Object:
19+ InProperty "weight":
20+ - TypesRestricted [Number]
21+ - AdditionalToProperty
22+ - OfType Number: NoMatchingMultipleOf 1.0
23+backwardChanges:
24+ AtPath "/pets":
25+ InOperation GetMethod:
26+ InParam "limit":
27+ InParamSchema:
28+ OfType Number: MatchingMaximumWeak (ProdCons {producer = Inclusive 30.0,
29+ consumer = Inclusive 20.0})
30+ WithStatusCode 200:
31+ ResponsePayload:
32+ PayloadSchema:
33+ OfType Array:
34+ InItems:
35+ OfType Object:
36+ InProperty "weight":
37+ - TypesRestricted [Number]
38+ - AdditionalToProperty
39+ - OfType Number: NoMatchingMultipleOf 1.0
40+ InOperation PostMethod:
41+ InRequest:
42+ InPayload:
43+ PayloadSchema:
44+ OfType Object:
45+ InProperty "name":
46+ OfType String:
47+ - MatchingMaxLengthWeak (ProdCons {producer = 15, consumer = 10})
48+ - MatchingMinLengthWeak (ProdCons {producer = 1, consumer = 3})
test/golden/common/recursive/report.md+7−7
@@ -1,14 +1,14 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **GET** /api/foo
9## **GET** /api/foo
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### `$.leaf.value.foo(String)`
13#### `$.leaf.value.foo(String)`
14
14
@@ -18,11 +18,11 @@ The following enum value has been added:
18"a"
18"a"
19```
19```
20
20
21−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
21+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
22
22
23## **GET** /api/foo
23## **GET** /api/foo
24
24
25−### 📱⬅️ JSON Response – 200
25+### ⬅️☁️ JSON Response – 200
26
26
27#### `$.leaf.value.foo(String)`
27#### `$.leaf.value.foo(String)`
28
28
test/golden/common/recursive/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/api/foo":
2 AtPath "/api/foo":
3 InOperation GetMethod:
3 InOperation GetMethod:
4 InRequest:
4 InRequest:
@@ -11,7 +11,7 @@ breakingChanges:
11 OfType Object:
11 OfType Object:
12 InProperty "foo":
12 InProperty "foo":
13 OfType String: NoMatchingEnum (String "a")
13 OfType String: NoMatchingEnum (String "a")
14−nonBreakingChanges:
14+backwardChanges:
15 AtPath "/api/foo":
15 AtPath "/api/foo":
16 InOperation GetMethod:
16 InOperation GetMethod:
17 WithStatusCode 200:
17 WithStatusCode 200:
test/golden/common/security-scheme/report.md+5−5
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 2 | 1 | 0 |
5+| 2 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /oauth/check
9## **POST** /oauth/check
10
10
@@ -22,7 +22,7 @@ Security scheme has been removed.
22
22
23Security scheme has been removed.
23Security scheme has been removed.
24
24
25−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
25+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
26
26
27## **POST** /oauth/check
27## **POST** /oauth/check
28
28
test/golden/common/security-scheme/trace-tree.yaml+2−2
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/oauth/sign_out":
2 AtPath "/oauth/sign_out":
3 InOperation GetMethod:
3 InOperation GetMethod:
4 SecurityRequirementStep 1:
4 SecurityRequirementStep 1:
@@ -7,7 +7,7 @@ breakingChanges:
7 InOperation PostMethod:
7 InOperation PostMethod:
8 SecurityRequirementStep 0:
8 SecurityRequirementStep 0:
9 SecuritySchemeStep "oauth": SecuritySchemeNotMatched
9 SecuritySchemeStep "oauth": SecuritySchemeNotMatched
10−nonBreakingChanges:
10+backwardChanges:
11 AtPath "/oauth/check":
11 AtPath "/oauth/check":
12 InOperation PostMethod:
12 InOperation PostMethod:
13 SecurityRequirementStep 0:
13 SecurityRequirementStep 0:
test/golden/common/servers/report.md+5−5
@@ -1,10 +1,10 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 3 | 1 | 0 |
5+| 3 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **GET** /pets
9## **GET** /pets
10
10
@@ -18,7 +18,7 @@ The server was removed.
18
18
192. A variable has been changed from being open to being closed.
192. A variable has been changed from being open to being closed.
20
20
21−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
21+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
22
22
23## **GET** /pets
23## **GET** /pets
24
24
test/golden/common/servers/trace-tree.yaml+2−2
@@ -1,11 +1,11 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/pets":
2 AtPath "/pets":
3 InOperation GetMethod:
3 InOperation GetMethod:
4 InServer "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}":
4 InServer "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}":
5 - EnumValueNotConsumed 1 "a"
5 - EnumValueNotConsumed 1 "a"
6 - ConsumerNotOpen 7
6 - ConsumerNotOpen 7
7 InServer "http://missing.url": ServerNotMatched
7 InServer "http://missing.url": ServerNotMatched
8−nonBreakingChanges:
8+backwardChanges:
9 AtPath "/pets":
9 AtPath "/pets":
10 InOperation GetMethod:
10 InOperation GetMethod:
11 InServer "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}": EnumValueNotConsumed
11 InServer "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}": EnumValueNotConsumed
test/golden/common/tuple-items/heterogeneous-component-mismatch/a.yamlnew file+28
@@ -0,0 +1,28 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: number
28+ - type: string
test/golden/common/tuple-items/heterogeneous-component-mismatch/b.yamlnew file+28
@@ -0,0 +1,28 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: number
28+ - type: object
test/golden/common/tuple-items/heterogeneous-component-mismatch/report.mdnew file+37
@@ -0,0 +1,37 @@
1+# Summary
2+
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4+|------------------------------------------|--------------------------------------------------|
5+| 2 | 2 |
6+
7+# <span id="breaking-changes"></span>❌ Breaking changes
8+
9+## **POST** /test
10+
11+### ➡️☁️ JSON Request
12+
13+#### `$[1](String)`
14+
15+The type has been removed.
16+
17+### ⬅️☁️ JSON Response – 200
18+
19+#### `$[1](Object)`
20+
21+The type has been added.
22+
23+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
24+
25+## **POST** /test
26+
27+### ➡️☁️ JSON Request
28+
29+#### `$[1](Object)`
30+
31+The type has been added.
32+
33+### ⬅️☁️ JSON Response – 200
34+
35+#### `$[1](String)`
36+
37+The type has been removed.
test/golden/common/tuple-items/heterogeneous-component-mismatch/trace-tree.yamlnew file+30
@@ -0,0 +1,30 @@
1+forwardChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Array:
8+ InItem 1:
9+ OfType String: TypeBecomesEmpty
10+ WithStatusCode 200:
11+ ResponsePayload:
12+ PayloadSchema:
13+ OfType Array:
14+ InItem 1:
15+ OfType Object: TypeBecomesEmpty
16+backwardChanges:
17+ AtPath "/test":
18+ InOperation PostMethod:
19+ InRequest:
20+ InPayload:
21+ PayloadSchema:
22+ OfType Array:
23+ InItem 1:
24+ OfType Object: TypeBecomesEmpty
25+ WithStatusCode 200:
26+ ResponsePayload:
27+ PayloadSchema:
28+ OfType Array:
29+ InItem 1:
30+ OfType String: TypeBecomesEmpty
test/golden/common/tuple-items/heterogeneous-length-mismatch/a.yamlnew file+29
@@ -0,0 +1,29 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: number
28+ - type: string
29+ - type: object
test/golden/common/tuple-items/heterogeneous-length-mismatch/b.yamlnew file+28
@@ -0,0 +1,28 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: number
28+ - type: object
test/golden/common/tuple-items/heterogeneous-length-mismatch/report.mdnew file+21
@@ -0,0 +1,21 @@
1+# Summary
2+
3+| [❌ Breaking changes](#breaking-changes) | ⚠️ Non-breaking changes |
4+|------------------------------------------|-------------------------|
5+| 2 | 0 |
6+
7+# <span id="breaking-changes"></span>❌ Breaking changes
8+
9+## **POST** /test
10+
11+### ➡️☁️ JSON Request
12+
13+#### `$(Array)`
14+
15+Tuple length changed from 3 to 2.
16+
17+### ⬅️☁️ JSON Response – 200
18+
19+#### `$(Array)`
20+
21+Tuple length changed from 3 to 2.
test/golden/common/tuple-items/heterogeneous-length-mismatch/trace-tree.yamlnew file+26
@@ -0,0 +1,26 @@
1+forwardChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Array: TupleItemsLengthChanged (ProdCons {producer = 3, consumer
8+ = 2})
9+ WithStatusCode 200:
10+ ResponsePayload:
11+ PayloadSchema:
12+ OfType Array: TupleItemsLengthChanged (ProdCons {producer = 2, consumer
13+ = 3})
14+backwardChanges:
15+ AtPath "/test":
16+ InOperation PostMethod:
17+ InRequest:
18+ InPayload:
19+ PayloadSchema:
20+ OfType Array: TupleItemsLengthChanged (ProdCons {producer = 2, consumer
21+ = 3})
22+ WithStatusCode 200:
23+ ResponsePayload:
24+ PayloadSchema:
25+ OfType Array: TupleItemsLengthChanged (ProdCons {producer = 3, consumer
26+ = 2})
test/golden/common/tuple-items/homogeneous-component-mismatch/a.yamlnew file+28
@@ -0,0 +1,28 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: string
28+ - type: number
test/golden/common/tuple-items/homogeneous-component-mismatch/b.yamlnew file+29
@@ -0,0 +1,29 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ minItems: 2
27+ maxItems: 2
28+ items:
29+ type: number
test/golden/common/tuple-items/homogeneous-component-mismatch/report.mdnew file+45
@@ -0,0 +1,45 @@
1+# Summary
2+
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4+|------------------------------------------|--------------------------------------------------|
5+| 4 | 2 |
6+
7+# <span id="breaking-changes"></span>❌ Breaking changes
8+
9+## **POST** /test
10+
11+### ➡️☁️ JSON Request
12+
13+#### `$(Array)`
14+
15+The array is no longer explicitly defined as a tuple.
16+
17+#### `$[0](String)`
18+
19+The type has been removed.
20+
21+### ⬅️☁️ JSON Response – 200
22+
23+#### `$(Array)`
24+
25+The array is no longer explicitly defined as a tuple.
26+
27+#### `$[0](Number)`
28+
29+The type has been added.
30+
31+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
32+
33+## **POST** /test
34+
35+### ➡️☁️ JSON Request
36+
37+#### `$[0](Number)`
38+
39+The type has been added.
40+
41+### ⬅️☁️ JSON Response – 200
42+
43+#### `$[0](String)`
44+
45+The type has been removed.
test/golden/common/tuple-items/homogeneous-component-mismatch/trace-tree.yamlnew file+34
@@ -0,0 +1,34 @@
1+forwardChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Array:
8+ - TupleToArray
9+ - InItem 0:
10+ OfType String: TypeBecomesEmpty
11+ WithStatusCode 200:
12+ ResponsePayload:
13+ PayloadSchema:
14+ OfType Array:
15+ - ArrayToTuple
16+ - InItem 0:
17+ OfType Number: TypeBecomesEmpty
18+backwardChanges:
19+ AtPath "/test":
20+ InOperation PostMethod:
21+ InRequest:
22+ InPayload:
23+ PayloadSchema:
24+ OfType Array:
25+ - ArrayToTuple
26+ - InItem 0:
27+ OfType Number: TypeBecomesEmpty
28+ WithStatusCode 200:
29+ ResponsePayload:
30+ PayloadSchema:
31+ OfType Array:
32+ - TupleToArray
33+ - InItem 0:
34+ OfType String: TypeBecomesEmpty
test/golden/common/tuple-items/homogeneous-length-mismatch/a.yamlnew file+56
@@ -0,0 +1,56 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test1:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+ /test2:
23+ post:
24+ requestBody:
25+ content:
26+ application/json:
27+ schema:
28+ $ref: "#/components/schemas/Test"
29+ responses:
30+ '200':
31+ description: test
32+ content:
33+ application/json:
34+ schema:
35+ $ref: "#/components/schemas/Test"
36+ /test3:
37+ post:
38+ requestBody:
39+ content:
40+ application/json:
41+ schema:
42+ $ref: "#/components/schemas/Test"
43+ responses:
44+ '200':
45+ description: test
46+ content:
47+ application/json:
48+ schema:
49+ $ref: "#/components/schemas/Test"
50+components:
51+ schemas:
52+ Test:
53+ type: array
54+ items:
55+ - type: number
56+ - type: number
test/golden/common/tuple-items/homogeneous-length-mismatch/b.yamlnew file+67
@@ -0,0 +1,67 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test1:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test1"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test1"
22+ /test2:
23+ post:
24+ requestBody:
25+ content:
26+ application/json:
27+ schema:
28+ $ref: "#/components/schemas/Test2"
29+ responses:
30+ '200':
31+ description: test
32+ content:
33+ application/json:
34+ schema:
35+ $ref: "#/components/schemas/Test2"
36+ /test3:
37+ post:
38+ requestBody:
39+ content:
40+ application/json:
41+ schema:
42+ $ref: "#/components/schemas/Test3"
43+ responses:
44+ '200':
45+ description: test
46+ content:
47+ application/json:
48+ schema:
49+ $ref: "#/components/schemas/Test3"
50+components:
51+ schemas:
52+ Test1:
53+ type: array
54+ maxItems: 2
55+ items:
56+ type: number
57+ Test2:
58+ type: array
59+ minItems: 2
60+ items:
61+ type: number
62+ Test3:
63+ type: array
64+ minItems: 3
65+ maxItems: 3
66+ items:
67+ type: number
test/golden/common/tuple-items/homogeneous-length-mismatch/report.mdnew file+73
@@ -0,0 +1,73 @@
1+# Summary
2+
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4+|------------------------------------------|--------------------------------------------------|
5+| 5 | 5 |
6+
7+# <span id="breaking-changes"></span>❌ Breaking changes
8+
9+## **POST** /test1
10+
11+### ⬅️☁️ JSON Response – 200
12+
13+#### `$(Array)`
14+
15+The array is no longer explicitly defined as a tuple.
16+
17+## **POST** /test2
18+
19+### ⬅️☁️ JSON Response – 200
20+
21+#### `$(Array)`
22+
23+The array is no longer explicitly defined as a tuple.
24+
25+## **POST** /test3
26+
27+### ➡️☁️ JSON Request
28+
29+#### `$(Array)`
30+
31+Minimum length of the array changed from 2 to 3.
32+
33+### ⬅️☁️ JSON Response – 200
34+
35+#### `$(Array)`
36+
37+1. Tuple length changed from 2 to 3.
38+
39+2. The array is no longer explicitly defined as a tuple.
40+
41+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
42+
43+## **POST** /test1
44+
45+### ➡️☁️ JSON Request
46+
47+#### `$(Array)`
48+
49+The array is no longer explicitly defined as a tuple.
50+
51+## **POST** /test2
52+
53+### ➡️☁️ JSON Request
54+
55+#### `$(Array)`
56+
57+The array is no longer explicitly defined as a tuple.
58+
59+## **POST** /test3
60+
61+### ➡️☁️ JSON Request
62+
63+#### `$(Array)`
64+
65+1. Tuple length changed from 2 to 3.
66+
67+2. The array is no longer explicitly defined as a tuple.
68+
69+### ⬅️☁️ JSON Response – 200
70+
71+#### `$(Array)`
72+
73+Minimum length of the array changed from 2 to 3.
test/golden/common/tuple-items/homogeneous-length-mismatch/trace-tree.yamlnew file+52
@@ -0,0 +1,52 @@
1+forwardChanges:
2+ AtPath "/test3":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Array: MatchingMinItemsWeak (ProdCons {producer = 2, consumer =
8+ 3})
9+ WithStatusCode 200:
10+ ResponsePayload:
11+ PayloadSchema:
12+ OfType Array:
13+ - TupleItemsLengthChanged (ProdCons {producer = 3, consumer = 2})
14+ - ArrayToTuple
15+ AtPath "/test1":
16+ InOperation PostMethod:
17+ WithStatusCode 200:
18+ ResponsePayload:
19+ PayloadSchema:
20+ OfType Array: NoMatchingTupleItems
21+ AtPath "/test2":
22+ InOperation PostMethod:
23+ WithStatusCode 200:
24+ ResponsePayload:
25+ PayloadSchema:
26+ OfType Array: NoMatchingTupleItems
27+backwardChanges:
28+ AtPath "/test3":
29+ InOperation PostMethod:
30+ InRequest:
31+ InPayload:
32+ PayloadSchema:
33+ OfType Array:
34+ - TupleItemsLengthChanged (ProdCons {producer = 3, consumer = 2})
35+ - ArrayToTuple
36+ WithStatusCode 200:
37+ ResponsePayload:
38+ PayloadSchema:
39+ OfType Array: MatchingMinItemsWeak (ProdCons {producer = 2, consumer =
40+ 3})
41+ AtPath "/test1":
42+ InOperation PostMethod:
43+ InRequest:
44+ InPayload:
45+ PayloadSchema:
46+ OfType Array: NoMatchingTupleItems
47+ AtPath "/test2":
48+ InOperation PostMethod:
49+ InRequest:
50+ InPayload:
51+ PayloadSchema:
52+ OfType Array: NoMatchingTupleItems
test/golden/common/tuple-items/homogeneous-vector/a.yamlnew file+28
@@ -0,0 +1,28 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ items:
27+ - type: number
28+ - type: number
test/golden/common/tuple-items/homogeneous-vector/b.yamlnew file+29
@@ -0,0 +1,29 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ type: array
26+ minItems: 2
27+ maxItems: 2
28+ items:
29+ type: number
test/golden/common/tuple-items/homogeneous-vector/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# Summary
2+
3+| ❌ Breaking changes | ⚠️ Non-breaking changes |
4+|---------------------|-------------------------|
5+| 0 | 0 |
test/golden/common/tuple-items/homogeneous-vector/trace-tree.yamlnew file+2
@@ -0,0 +1,2 @@
1+forwardChanges: {}
2+backwardChanges: {}
test/golden/common/type-changing/report.md+19−19
@@ -1,14 +1,14 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 6 | 6 | 0 |
5+| 6 | 6 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test1
9## **POST** /test1
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13Values are now limited to the following types:
13Values are now limited to the following types:
14
14
@@ -16,7 +16,7 @@ Values are now limited to the following types:
16
16
17## **POST** /test2
17## **POST** /test2
18
18
19−### 📱➡️ JSON Request
19+### ➡️☁️ JSON Request
20
20
21Values are now limited to the following types:
21Values are now limited to the following types:
22
22
@@ -28,23 +28,23 @@ Values are now limited to the following types:
28
28
29## **POST** /test3
29## **POST** /test3
30
30
31−### 📱➡️ JSON Request
31+### ➡️☁️ JSON Request
32
32
33#### `$(Array)`
33#### `$(Array)`
34
34
35−The value has been removed.
35+The type has been removed.
36
36
37## **POST** /test4
37## **POST** /test4
38
38
39−### 📱➡️ JSON Request
39+### ➡️☁️ JSON Request
40
40
41#### `$(Object)`
41#### `$(Object)`
42
42
43−The value has been removed.
43+The type has been removed.
44
44
45## **POST** /test5
45## **POST** /test5
46
46
47−### 📱➡️ JSON Request
47+### ➡️☁️ JSON Request
48
48
49Values are now limited to the following types:
49Values are now limited to the following types:
50
50
@@ -54,11 +54,11 @@ Values are now limited to the following types:
54
54
55Value is now a multiple of 1.0.
55Value is now a multiple of 1.0.
56
56
57−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
57+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
58
58
59## **POST** /test1
59## **POST** /test1
60
60
61−### 📱⬅️ JSON Response – 200
61+### ⬅️☁️ JSON Response – 200
62
62
63Values are now limited to the following types:
63Values are now limited to the following types:
64
64
@@ -66,7 +66,7 @@ Values are now limited to the following types:
66
66
67## **POST** /test2
67## **POST** /test2
68
68
69−### 📱⬅️ JSON Response – 200
69+### ⬅️☁️ JSON Response – 200
70
70
71Values are now limited to the following types:
71Values are now limited to the following types:
72
72
@@ -78,23 +78,23 @@ Values are now limited to the following types:
78
78
79## **POST** /test3
79## **POST** /test3
80
80
81−### 📱⬅️ JSON Response – 200
81+### ⬅️☁️ JSON Response – 200
82
82
83#### `$(Array)`
83#### `$(Array)`
84
84
85−The value has been removed.
85+The type has been removed.
86
86
87## **POST** /test4
87## **POST** /test4
88
88
89−### 📱⬅️ JSON Response – 200
89+### ⬅️☁️ JSON Response – 200
90
90
91#### `$(Object)`
91#### `$(Object)`
92
92
93−The value has been removed.
93+The type has been removed.
94
94
95## **POST** /test5
95## **POST** /test5
96
96
97−### 📱⬅️ JSON Response – 200
97+### ⬅️☁️ JSON Response – 200
98
98
99Values are now limited to the following types:
99Values are now limited to the following types:
100
100
test/golden/common/type-changing/trace-tree.yaml+6−6
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test5":
2 AtPath "/test5":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -11,13 +11,13 @@ breakingChanges:
11 InRequest:
11 InRequest:
12 InPayload:
12 InPayload:
13 PayloadSchema:
13 PayloadSchema:
14− OfType Array: NoContradiction
14+ OfType Array: TypeBecomesEmpty
15 AtPath "/test4":
15 AtPath "/test4":
16 InOperation PostMethod:
16 InOperation PostMethod:
17 InRequest:
17 InRequest:
18 InPayload:
18 InPayload:
19 PayloadSchema:
19 PayloadSchema:
20− OfType Object: NoContradiction
20+ OfType Object: TypeBecomesEmpty
21 AtPath "/test1":
21 AtPath "/test1":
22 InOperation PostMethod:
22 InOperation PostMethod:
23 InRequest:
23 InRequest:
@@ -28,7 +28,7 @@ breakingChanges:
28 InRequest:
28 InRequest:
29 InPayload:
29 InPayload:
30 PayloadSchema: TypesRestricted [Number,Array,Object]
30 PayloadSchema: TypesRestricted [Number,Array,Object]
31−nonBreakingChanges:
31+backwardChanges:
32 AtPath "/test5":
32 AtPath "/test5":
33 InOperation PostMethod:
33 InOperation PostMethod:
34 WithStatusCode 200:
34 WithStatusCode 200:
@@ -41,13 +41,13 @@ nonBreakingChanges:
41 WithStatusCode 200:
41 WithStatusCode 200:
42 ResponsePayload:
42 ResponsePayload:
43 PayloadSchema:
43 PayloadSchema:
44− OfType Array: NoContradiction
44+ OfType Array: TypeBecomesEmpty
45 AtPath "/test4":
45 AtPath "/test4":
46 InOperation PostMethod:
46 InOperation PostMethod:
47 WithStatusCode 200:
47 WithStatusCode 200:
48 ResponsePayload:
48 ResponsePayload:
49 PayloadSchema:
49 PayloadSchema:
50− OfType Object: NoContradiction
50+ OfType Object: TypeBecomesEmpty
51 AtPath "/test1":
51 AtPath "/test1":
52 InOperation PostMethod:
52 InOperation PostMethod:
53 WithStatusCode 200:
53 WithStatusCode 200:
test/golden/common/unguarded-recursive/a.yaml → test/golden/common/unguarded-recursive/basic/a.yamlrenamed

The content is the same.

test/golden/common/unguarded-recursive/b.yaml → test/golden/common/unguarded-recursive/basic/b.yamlrenamed

The content is the same.

test/golden/common/unguarded-recursive/basic/report.mdnew file+13
@@ -0,0 +1,13 @@
1+# Summary
2+
3+| ❌ Breaking changes | ⚠️ Non-breaking changes | [❓ Unsupported feature changes](#unsupported-changes) |
4+|---------------------|-------------------------|--------------------------------------------------------|
5+| 0 | 0 | 1 |
6+
7+# <span id="unsupported-changes"></span>❓ Unsupported feature changes
8+
9+## **GET** /api/foo
10+
11+### ⬅️☁️ JSON Response – 200
12+
13+Encountered recursion that is too complex for CompaREST to untangle.
test/golden/common/unguarded-recursive/trace-tree.yaml → test/golden/common/unguarded-recursive/basic/trace-tree.yamlrenamed+2−2
@@ -1,10 +1,10 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/api/foo":
2 AtPath "/api/foo":
3 InOperation GetMethod:
3 InOperation GetMethod:
4 WithStatusCode 200:
4 WithStatusCode 200:
5 ResponsePayload:
5 ResponsePayload:
6 PayloadSchema: UnguardedRecursion
6 PayloadSchema: UnguardedRecursion
7−nonBreakingChanges:
7+backwardChanges:
8 AtPath "/api/foo":
8 AtPath "/api/foo":
9 InOperation GetMethod:
9 InOperation GetMethod:
10 WithStatusCode 200:
10 WithStatusCode 200:
test/golden/common/unguarded-recursive/report.mddeleted−13
@@ -1,13 +0,0 @@
1−# Summary
2−
3−| ⚠️ Breaking changes | 🙆 Non-breaking changes | [🤷 Unsupported feature changes](#unsupported-changes) |
4−|---------------------|------------------------|-------------------------------------------------------|
5−| 0 | 0 | 1 |
6−
7−# <span id="unsupported-changes"></span>🤷 Unsupported feature changes
8−
9−## **GET** /api/foo
10−
11−### 📱⬅️ JSON Response – 200
12−
13−Encountered recursion that is too complex for OpenApi Diff to untangle.
test/golden/common/unguarded-recursive/silent/a.yamlnew file+25
@@ -0,0 +1,25 @@
1+components:
2+ schemas:
3+ A:
4+ type: object
5+ properties:
6+ bar:
7+ $ref: "#/components/schemas/B"
8+ B:
9+ anyOf:
10+ - $ref: "#/components/schemas/B"
11+ type: object
12+openapi: 3.0.0
13+info:
14+ version: ""
15+ title: ""
16+paths:
17+ /foo:
18+ get:
19+ responses:
20+ "200":
21+ content:
22+ application/json;charset=utf-8:
23+ schema:
24+ $ref: "#/components/schemas/A"
25+ description: ""
test/golden/common/unguarded-recursive/silent/b.yamlnew file+19
@@ -0,0 +1,19 @@
1+components:
2+ schemas: {}
3+openapi: 3.0.0
4+info:
5+ version: ""
6+ title: ""
7+paths:
8+ /foo:
9+ get:
10+ responses:
11+ "200":
12+ content:
13+ application/json;charset=utf-8:
14+ schema:
15+ type: object
16+ properties:
17+ bar:
18+ type: object
19+ description: ""
test/golden/common/unguarded-recursive/silent/report.mdnew file+15
@@ -0,0 +1,15 @@
1+# Summary
2+
3+| ❌ Breaking changes | ⚠️ Non-breaking changes | [❓ Unsupported feature changes](#unsupported-changes) |
4+|---------------------|-------------------------|--------------------------------------------------------|
5+| 0 | 0 | 1 |
6+
7+# <span id="unsupported-changes"></span>❓ Unsupported feature changes
8+
9+## **GET** /foo
10+
11+### ⬅️☁️ JSON Response – 200
12+
13+#### `$.bar`
14+
15+Encountered recursion that is too complex for CompaREST to untangle.
test/golden/common/unguarded-recursive/silent/trace-tree.yamlnew file+16
@@ -0,0 +1,16 @@
1+forwardChanges:
2+ AtPath "/foo":
3+ InOperation GetMethod:
4+ WithStatusCode 200:
5+ ResponsePayload:
6+ PayloadSchema:
7+ OfType Object:
8+ InProperty "bar": UnguardedRecursion
9+backwardChanges:
10+ AtPath "/foo":
11+ InOperation GetMethod:
12+ WithStatusCode 200:
13+ ResponsePayload:
14+ PayloadSchema:
15+ OfType Object:
16+ InProperty "bar": UnguardedRecursion
test/golden/common/variant-added/report.md+11−11
@@ -1,25 +1,25 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 1 | 1 | 0 |
5+| 1 | 1 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱⬅️ JSON Response – 200
11+### ⬅️☁️ JSON Response – 200
12
12
13−#### In cases where `$.tag` is `"C"`
13+#### `$(Object)`
14
14
15−The value has been added.
15+The case where `$.tag` is `"C"` – has been added.
16
16
17−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
18
18
19## **POST** /test
19## **POST** /test
20
20
21−### 📱➡️ JSON Request
21+### ➡️☁️ JSON Request
22
22
23−#### In cases where `$.tag` is `"C"`
23+#### `$(Object)`
24
24
25−The value has been added.
25+The case where `$.tag` is `"C"` – has been added.
test/golden/common/variant-added/trace-tree.yaml+6−6
@@ -1,16 +1,16 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 WithStatusCode 200:
4 WithStatusCode 200:
5 ResponsePayload:
5 ResponsePayload:
6 PayloadSchema:
6 PayloadSchema:
7− OfType Object:
8− InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
7+ OfType Object: PartitionBecomesEmpty (PInProperty "tag" PHere,CByEnumValue
9−nonBreakingChanges:
8+ (fromList [String "C"]))
9+backwardChanges:
10 AtPath "/test":
10 AtPath "/test":
11 InOperation PostMethod:
11 InOperation PostMethod:
12 InRequest:
12 InRequest:
13 InPayload:
13 InPayload:
14 PayloadSchema:
14 PayloadSchema:
15− OfType Object:
16− InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
15+ OfType Object: PartitionBecomesEmpty (PInProperty "tag" PHere,CByEnumValue
16+ (fromList [String "C"]))
test/golden/common/variant-changed/report.md+13−13
@@ -1,45 +1,45 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 2 | 2 | 0 |
5+| 2 | 2 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### In cases where `$.tag` is `"B"`
13#### In cases where `$.tag` is `"B"`
14
14
15##### `$.prop_B(Number)`
15##### `$.prop_B(Number)`
16
16
17−The value has been removed.
17+The type has been removed.
18
18
19−### 📱⬅️ JSON Response – 200
19+### ⬅️☁️ JSON Response – 200
20
20
21#### In cases where `$.tag` is `"B"`
21#### In cases where `$.tag` is `"B"`
22
22
23##### `$.prop_B(String)`
23##### `$.prop_B(String)`
24
24
25−The value has been added.
25+The type has been added.
26
26
27−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
27+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
28
28
29## **POST** /test
29## **POST** /test
30
30
31−### 📱➡️ JSON Request
31+### ➡️☁️ JSON Request
32
32
33#### In cases where `$.tag` is `"B"`
33#### In cases where `$.tag` is `"B"`
34
34
35##### `$.prop_B(String)`
35##### `$.prop_B(String)`
36
36
37−The value has been added.
37+The type has been added.
38
38
39−### 📱⬅️ JSON Response – 200
39+### ⬅️☁️ JSON Response – 200
40
40
41#### In cases where `$.tag` is `"B"`
41#### In cases where `$.tag` is `"B"`
42
42
43##### `$.prop_B(Number)`
43##### `$.prop_B(Number)`
44
44
45−The value has been removed.
45+The type has been removed.
test/golden/common/variant-changed/trace-tree.yaml+6−6
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -7,15 +7,15 @@ breakingChanges:
7 OfType Object:
7 OfType Object:
8 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
8 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
9 InProperty "prop_B":
9 InProperty "prop_B":
10− OfType Number: NoContradiction
10+ OfType Number: TypeBecomesEmpty
11 WithStatusCode 200:
11 WithStatusCode 200:
12 ResponsePayload:
12 ResponsePayload:
13 PayloadSchema:
13 PayloadSchema:
14 OfType Object:
14 OfType Object:
15 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
15 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
16 InProperty "prop_B":
16 InProperty "prop_B":
17− OfType String: NoContradiction
17+ OfType String: TypeBecomesEmpty
18−nonBreakingChanges:
18+backwardChanges:
19 AtPath "/test":
19 AtPath "/test":
20 InOperation PostMethod:
20 InOperation PostMethod:
21 InRequest:
21 InRequest:
@@ -24,11 +24,11 @@ nonBreakingChanges:
24 OfType Object:
24 OfType Object:
25 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
25 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
26 InProperty "prop_B":
26 InProperty "prop_B":
27− OfType String: NoContradiction
27+ OfType String: TypeBecomesEmpty
28 WithStatusCode 200:
28 WithStatusCode 200:
29 ResponsePayload:
29 ResponsePayload:
30 PayloadSchema:
30 PayloadSchema:
31 OfType Object:
31 OfType Object:
32 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
32 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
33 InProperty "prop_B":
33 InProperty "prop_B":
34− OfType Number: NoContradiction
34+ OfType Number: TypeBecomesEmpty
test/golden/common/variant-deep-changed/report.md+13−13
@@ -1,45 +1,45 @@
1# Summary
1# Summary
2
2
3−| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
3+| [❌ Breaking changes](#breaking-changes) | [⚠️ Non-breaking changes](#non-breaking-changes) |
4−|------------------------------------------|-------------------------------------------------|-------------------------------|
4+|------------------------------------------|--------------------------------------------------|
5−| 2 | 2 | 0 |
5+| 2 | 2 |
6
6
7−# <span id="breaking-changes"></span>⚠️ Breaking changes
7+# <span id="breaking-changes"></span>❌ Breaking changes
8
8
9## **POST** /test
9## **POST** /test
10
10
11−### 📱➡️ JSON Request
11+### ➡️☁️ JSON Request
12
12
13#### In cases where `$.desc.name` is `"B"`
13#### In cases where `$.desc.name` is `"B"`
14
14
15##### `$.prop_B(Number)`
15##### `$.prop_B(Number)`
16
16
17−The value has been removed.
17+The type has been removed.
18
18
19−### 📱⬅️ JSON Response – 200
19+### ⬅️☁️ JSON Response – 200
20
20
21#### In cases where `$.desc.name` is `"B"`
21#### In cases where `$.desc.name` is `"B"`
22
22
23##### `$.prop_B(String)`
23##### `$.prop_B(String)`
24
24
25−The value has been added.
25+The type has been added.
26
26
27−# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
27+# <span id="non-breaking-changes"></span>⚠️ Non-breaking changes
28
28
29## **POST** /test
29## **POST** /test
30
30
31−### 📱➡️ JSON Request
31+### ➡️☁️ JSON Request
32
32
33#### In cases where `$.desc.name` is `"B"`
33#### In cases where `$.desc.name` is `"B"`
34
34
35##### `$.prop_B(String)`
35##### `$.prop_B(String)`
36
36
37−The value has been added.
37+The type has been added.
38
38
39−### 📱⬅️ JSON Response – 200
39+### ⬅️☁️ JSON Response – 200
40
40
41#### In cases where `$.desc.name` is `"B"`
41#### In cases where `$.desc.name` is `"B"`
42
42
43##### `$.prop_B(Number)`
43##### `$.prop_B(Number)`
44
44
45−The value has been removed.
45+The type has been removed.
test/golden/common/variant-deep-changed/trace-tree.yaml+6−6
@@ -1,4 +1,4 @@
1−breakingChanges:
1+forwardChanges:
2 AtPath "/test":
2 AtPath "/test":
3 InOperation PostMethod:
3 InOperation PostMethod:
4 InRequest:
4 InRequest:
@@ -7,15 +7,15 @@ breakingChanges:
7 OfType Object:
7 OfType Object:
8 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
8 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
9 InProperty "prop_B":
9 InProperty "prop_B":
10− OfType Number: NoContradiction
10+ OfType Number: TypeBecomesEmpty
11 WithStatusCode 200:
11 WithStatusCode 200:
12 ResponsePayload:
12 ResponsePayload:
13 PayloadSchema:
13 PayloadSchema:
14 OfType Object:
14 OfType Object:
15 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
15 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
16 InProperty "prop_B":
16 InProperty "prop_B":
17− OfType String: NoContradiction
17+ OfType String: TypeBecomesEmpty
18−nonBreakingChanges:
18+backwardChanges:
19 AtPath "/test":
19 AtPath "/test":
20 InOperation PostMethod:
20 InOperation PostMethod:
21 InRequest:
21 InRequest:
@@ -24,11 +24,11 @@ nonBreakingChanges:
24 OfType Object:
24 OfType Object:
25 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
25 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
26 InProperty "prop_B":
26 InProperty "prop_B":
27− OfType String: NoContradiction
27+ OfType String: TypeBecomesEmpty
28 WithStatusCode 200:
28 WithStatusCode 200:
29 ResponsePayload:
29 ResponsePayload:
30 PayloadSchema:
30 PayloadSchema:
31 OfType Object:
31 OfType Object:
32 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
32 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
33 InProperty "prop_B":
33 InProperty "prop_B":
34− OfType Number: NoContradiction
34+ OfType Number: TypeBecomesEmpty