├── .eslintrc ├── .github ├── CODEOWNERS └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── LICENSE ├── README.md ├── docs ├── tree.md └── walker.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── setupTests.ts ├── src ├── __tests__ │ ├── __fixtures__ │ │ ├── arrays │ │ │ ├── additional-empty.json │ │ │ ├── additional-false.json │ │ │ ├── additional-schema.json │ │ │ ├── additional-true.json │ │ │ ├── of-allofs.json │ │ │ ├── of-arrays.json │ │ │ ├── of-objects.json │ │ │ ├── of-refs.json │ │ │ ├── with-multiple-arrayish-items.json │ │ │ ├── with-ordered-items.json │ │ │ └── with-single-arrayish-items.json │ │ ├── combiners │ │ │ ├── allOfs │ │ │ │ ├── base.json │ │ │ │ ├── circular-regular-level.json │ │ │ │ ├── circular-top-level.json │ │ │ │ ├── complex.json │ │ │ │ ├── nested-refs.json │ │ │ │ ├── todo-full-2.json │ │ │ │ ├── todo-full.json │ │ │ │ └── with-type.json │ │ │ └── oneof-with-array-type.json │ │ ├── default-schema.json │ │ ├── formats-schema.json │ │ ├── objects │ │ │ ├── additional-empty.json │ │ │ ├── additional-false.json │ │ │ ├── additional-schema.json │ │ │ └── additional-true.json │ │ ├── recursive-schema.json │ │ ├── references │ │ │ ├── base.json │ │ │ ├── circular-with-overrides.json │ │ │ ├── nullish.json │ │ │ └── with-overrides.json │ │ ├── stress-schema.json │ │ └── tickets.schema.json │ ├── __snapshots__ │ │ └── tree.spec.ts.snap │ ├── tree.spec.ts │ └── utils │ │ └── printTree.ts ├── accessors │ ├── __tests__ │ │ ├── getAnnotations.spec.ts │ │ └── getValidations.spec.ts │ ├── getAnnotations.ts │ ├── getCombiners.ts │ ├── getPrimaryType.ts │ ├── getRequired.ts │ ├── getTypes.ts │ ├── getValidations.ts │ ├── guards │ │ └── isValidType.ts │ ├── inferType.ts │ ├── isDeprecated.ts │ └── unwrap.ts ├── errors.ts ├── guards │ ├── index.ts │ └── nodes.ts ├── index.ts ├── mergers │ ├── mergeAllOf.ts │ └── mergeOneOrAnyOf.ts ├── nodes │ ├── BaseNode.ts │ ├── BooleanishNode.ts │ ├── ReferenceNode.ts │ ├── RegularNode.ts │ ├── RootNode.ts │ ├── index.ts │ ├── mirrored │ │ ├── MirroredReferenceNode.ts │ │ ├── MirroredRegularNode.ts │ │ └── index.ts │ └── types.ts ├── tree │ ├── index.ts │ ├── tree.ts │ └── types.ts ├── types.ts ├── utils │ ├── guards.ts │ ├── index.ts │ └── pick.ts └── walker │ ├── index.ts │ ├── types.ts │ └── walker.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@stoplight", 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "./tsconfig.json" 6 | }, 7 | "rules": { 8 | "no-param-reassign": "off", 9 | "@typescript-eslint/strict-boolean-expressions": "error", 10 | "@typescript-eslint/prefer-nullish-coalescing": "error", 11 | "@typescript-eslint/prefer-for-of": "error", 12 | "@typescript-eslint/no-throw-literal": "error", 13 | "@typescript-eslint/prefer-optional-chain": "error", 14 | "@typescript-eslint/no-floating-promises": ["error", { "ignoreVoid": true }], 15 | "no-console": "error", 16 | "no-undefined": "error" 17 | }, 18 | "overrides": [ 19 | { 20 | "files": ["*.spec.{ts,tsx}"], 21 | "env": { 22 | "jest": true 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @stoplightio/void-crew 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - beta 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: 12 18 | 19 | - name: Install dependencies 20 | run: yarn --frozen-lockfile 21 | 22 | - name: Build all 23 | run: yarn build 24 | 25 | - name: Upload built artifacts 26 | uses: actions/upload-artifact@v2 27 | with: 28 | name: build 29 | path: dist 30 | retention-days: 1 # mostly just for passing to publish, so retention is not needed 31 | 32 | publish: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: Download the build 41 | uses: actions/download-artifact@v2 42 | with: 43 | name: build 44 | path: dist 45 | 46 | - name: Install dependencies 47 | run: yarn --frozen-lockfile 48 | 49 | - name: Semantic release 50 | run: yarn release 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - beta 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node: 16 | - '10.18' 17 | - '12.x' 18 | - '14.x' 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@v2-beta 25 | with: 26 | node-version: ${{ matrix.node }} 27 | 28 | - name: Install dependencies 29 | run: yarn --frozen-lockfile 30 | 31 | - name: Lint 32 | run: yarn lint 33 | 34 | - name: Test 35 | run: yarn test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | /docs-auto 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | .vscode 23 | .idea 24 | .nyc_output 25 | .DS_Store 26 | .awcache 27 | .cache-loader 28 | .cache 29 | .rpt2_cache 30 | 31 | # logs 32 | *.log* 33 | *-debug.log* 34 | *-error.log* 35 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 15 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@stoplight/eslint-config/prettier.config'), 3 | }; 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018 Stoplight, Inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @stoplight/json-schema-tree 2 | 3 | 4 | 5 | ### Use cases 6 | 7 | - json-schema-viewer 8 | - json-schema-editor 9 | - masking 10 | 11 | ### Installation 12 | 13 | Supported in modern browsers and Node.JS (>=10.18). 14 | 15 | ```bash 16 | # latest stable 17 | yarn add @stoplight/json-schema-tree 18 | ``` 19 | 20 | ### Usage 21 | 22 | ```js 23 | import { SchemaTree, SchemaNodeKind, isRegularNode } from '@stoplight/json-schema-tree'; 24 | 25 | const tree = new SchemaTree(mySchema); 26 | const ALLOWED_DEPTH = 2; 27 | 28 | tree.walker.hookInto('stepIn', node => tree.walker.depth <= ALLOWED_DEPTH); // if flattening is needed, this might need to be tweaked to account for the scenarios where certain nodes can be merged (i.e. arrays) 29 | 30 | tree.walker.hookInto('filter', node => { 31 | return !isRegularNode(node) || node.types === null || !node.types.includes(SchemaNodeKind.Integer); // if a schema property is of type integer, it won't be included in the tree 32 | }); 33 | 34 | tree.populate(); 35 | 36 | tree.root; // populated tree 37 | ``` 38 | 39 | ### Contributing 40 | 41 | 1. Clone repo. 42 | 2. Create / checkout `feature/{name}`, `chore/{name}`, or `fix/{name}` branch. 43 | 3. Install deps: `yarn`. 44 | 4. Make your changes. 45 | 5. Run tests: `yarn test.prod`. 46 | 6. Stage relevant files to git. 47 | 7. Commit: `yarn commit`. _NOTE: Commits that don't follow the 48 | [conventional](https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional) format will be 49 | rejected. `yarn commit` creates this format for you, or you can put it together manually and then do a regular 50 | `git commit`._ 51 | 8. Push: `git push`. 52 | 9. Open PR targeting the `master` branch. 53 | -------------------------------------------------------------------------------- /docs/tree.md: -------------------------------------------------------------------------------- 1 | # Tree 2 | 3 | Currently, the tree may contain up to 5 kinds of nodes. 4 | 5 | - RootNode - a single tree cannot have more than a single instance of RootNode 6 | - RegularNode and MirroredRegularNode - used for everything other than a schema fragment containing a `$ref` 7 | - ReferenceNode and MirroredReferenceNode - used only for fragments with a `$ref` included 8 | 9 | ## Mirroring - mirrored nodes 10 | 11 | Although the name might sound a bit odd, it is exactly what these nodes are. They literally represent their standard 12 | variant. Mirrored nodes are used to represent the same portion of schema fragment. However, there are properties that do 13 | vary. These are `id` and `children`. Each node has always unique `id`, regardless of their type. Moreover, to avoid 14 | entering some infinite loop, `children` (if any) are processed shallowly. 15 | 16 | For a more end-to-end example, I encourage you to execute the sample provided below in [circular $refs](#circular-refs) 17 | section. 18 | 19 | ### Caution 20 | 21 | **DO NOT** use `instanceof` checks for verifying whether a particular node is of a regular kind. Each regular node has 22 | `Symbol.hasInstance` trait implemented that will return true if you use a regular or mirrored sort of node on the RHS of 23 | condition. 24 | 25 | ``` 26 | myNode instanceof RegularNode; // DO NOT DO IT 27 | 28 | if (!isMirroredNode(myNode)) { 29 | // this is okay 30 | } 31 | ``` 32 | 33 | Do note that the fact a mirrored node exists in tree does not necessarily mean the tree has no boundaries. Mirrored 34 | nodes are also used for fragments that were already processed previously. 35 | 36 | ## $refs 37 | 38 | ### Resolving 39 | 40 | By default, we attempt to resolve all local $refs leveraging `resolveInlineRef` util from `@stoplight/json`. If you wish 41 | to provide a custom resolver, you can supply a `resolver` option to a tree. 42 | 43 | ```js 44 | import { SchemaTree } from '@stoplight/json-schema-tree'; 45 | 46 | const tree = new SchemaTree(schema, { 47 | refResolver(ref, propertyPath, fragment) { 48 | // ref has a pointer and a source 49 | // do something or throw if resolving cannot be performed 50 | }, 51 | }); 52 | ``` 53 | 54 | Retaining all $refs in tree is possible as well. In such case, you should pass `null` as the value of `refResolver`. 55 | 56 | ```js 57 | import { SchemaTree } from '@stoplight/json-schema-tree'; 58 | 59 | const tree = new SchemaTree(schema, { 60 | refResolver: null, // I will not try to resolve any $refs 61 | }); 62 | ``` 63 | 64 | It is worth mentioning that we do not resolve the whole schema upfront. $refs are resolved only when they are actually 65 | seen (processed) during the traversal. 66 | 67 | ### Circular $refs 68 | 69 | If your schema contains circular $refs, certain branches of tree will have mirrored nodes as some of their leaf nodes. 70 | That said, the size of the tree will be infinite. 71 | 72 | #### Example schema with indirect circular references 73 | 74 | ```json 75 | { 76 | "type": "object", 77 | "properties": { 78 | "foo": { 79 | "type": "array", 80 | "items": { 81 | "type": "object", 82 | "properties": { 83 | "user": { 84 | "$ref": "#/properties/bar" 85 | } 86 | } 87 | } 88 | }, 89 | "bar": { 90 | "$ref": "#/properties/baz" 91 | }, 92 | "baz": { 93 | "$ref": "#/properties/foo" 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ```js 100 | import { SchemaTree } from '@stoplight/json-schema-tree'; 101 | 102 | const tree = new SchemaTree(schema); 103 | tree.populate(); 104 | 105 | expect(tree.root.children[0].children[0].children[0].children[0].children[0].children[0].path).toEqual([ 106 | 'properties', 107 | 'foo', 108 | 'items', 109 | 'properties', 110 | 'user', 111 | 'items', 112 | 'properties', 113 | 'user', 114 | ]); 115 | expect(tree.root.children[0].children[2].children[0].children[0].children[0].children[0].path).toEqual([ 116 | 'properties', 117 | 'baz', 118 | 'items', 119 | 'properties', 120 | 'user', 121 | 'items', 122 | 'properties', 123 | 'user', 124 | ]); 125 | 126 | // etc. 127 | ``` 128 | 129 | **CAUTION** 130 | 131 | Always use `isMirroredNode` guard when traversing the processed tree. If you forget to do it, you will enter recursive 132 | loops at times. 133 | 134 | ```js 135 | function traverse(node) { 136 | if (isMirroredNode(node)) { 137 | // alright, it's a mirrored node, I can do something with it 138 | } else { 139 | // continue 140 | } 141 | } 142 | ``` 143 | -------------------------------------------------------------------------------- /docs/walker.md: -------------------------------------------------------------------------------- 1 | # Walker 2 | 3 | Walker is responsible for traversing the schema and fills the tree with nodes. 4 | 5 | A good example of robust integration can be found 6 | [here](https://github.com/stoplightio/json-schema-viewer/blob/4cc585424390459bf27c41e2818343a6d5bf249e/src/tree/tree.ts). 7 | 8 | ## Hooks 9 | 10 | - `filter` - can be leveraged in case you do not care about certain kinds of schemas, i.e. anyOf or oneOf in OAS2. 11 | - `stepIn` - can be used if you do not wish to enter a given child. Useful for JSV and alike when you want to process N 12 | level of tree. 13 | 14 | ## Events 15 | 16 | Walker emits a number of events to 17 | 18 | ```ts 19 | export type WalkerNodeEventHandler = (node: SchemaNode) => void; 20 | export type WalkerFragmentEventHandler = (node: SchemaFragment) => void; 21 | export type WalkerErrorEventHandler = (ex: Error) => void; 22 | 23 | export type WalkerEmitter = { 24 | enterNode: WalkerNodeEventHandler; // emitted right after a node is created 25 | exitNode: WalkerNodeEventHandler; // emitted when a given node is fully processed 26 | 27 | includeNode: WalkerNodeEventHandler; // emitted when filter hook does not exist or returns a true value 28 | skipNode: WalkerNodeEventHandler; // emitted when filter hook returns false value and therefore node is skipped 29 | 30 | stepInNode: WalkerNodeEventHandler; // dispatched when we step into a given node (i.e object, array, or combiner) 31 | stepOutNode: WalkerNodeEventHandler; // emitted when we are on the top level again 32 | stepOverNode: WalkerNodeEventHandler; // emitted when stepIn hook returned a false value 33 | 34 | enterFragment: WalkerFragmentEventHandler; // dispatched when a given schema fragment is about to processed 35 | exitFragment: WalkerFragmentEventHandler; // dispatched when a particular schema fragment is fully processed 36 | 37 | error: WalkerErrorEventHandler; // any meaningful error such as allOf merging error or resolving error 38 | }; 39 | ``` 40 | 41 | ## Expanding 42 | 43 | ```js 44 | // Make sure that both `filter` and `stepIn` hook do not intefere. 45 | 46 | tree.walker.restoreWalkerAtNode(someNodeToExpand); 47 | tree.populate(); 48 | ``` 49 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: process.cwd(), 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | setupFilesAfterEnv: ['./setupTests.ts'], 6 | testMatch: ['/src/**/__tests__/*.(ts|js)?(x)'], 7 | transform: { 8 | '\\.tsx?$': 'ts-jest', 9 | }, 10 | coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/', '/__stories__/', '__mocks__/', 'types.ts'], 11 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 12 | globals: { 13 | 'ts-jest': { 14 | tsconfig: 'tsconfig.build.json', 15 | diagnostics: { 16 | ignoreCodes: [151001], 17 | }, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stoplight/json-schema-tree", 3 | "version": "0.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "sideEffects": false, 7 | "homepage": "https://github.com/stoplightio/json-schema-tree", 8 | "bugs": "https://github.com/stoplightio/json-schema-tree/issues", 9 | "author": "Stoplight ", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/stoplightio/json-schema-tree" 13 | }, 14 | "license": "Apache-2.0", 15 | "main": "src/index.ts", 16 | "files": [ 17 | "**/*" 18 | ], 19 | "engines": { 20 | "node": ">=10.18" 21 | }, 22 | "scripts": { 23 | "build": "sl-scripts bundle --sourcemap", 24 | "commit": "git-cz", 25 | "lint": "yarn lint.prettier && yarn lint.eslint", 26 | "lint.fix": "yarn lint.prettier --write && yarn lint.eslint --fix", 27 | "lint.eslint": "eslint --cache --cache-location .cache/ --ext=.js,.ts src", 28 | "lint.prettier": "prettier --ignore-path .eslintignore --check docs/**/*.md README.md", 29 | "release": "sl-scripts release", 30 | "release.dryRun": "sl-scripts release --dry-run --debug", 31 | "test": "jest", 32 | "test.prod": "yarn lint && yarn test --coverage --maxWorkers=2", 33 | "test.update": "yarn test --updateSnapshot", 34 | "test.watch": "yarn test --watch", 35 | "yalc": "yarn build && (cd dist && yalc publish)" 36 | }, 37 | "peerDependencies": {}, 38 | "dependencies": { 39 | "@stoplight/json": "^3.12.0", 40 | "@stoplight/json-schema-merge-allof": "^0.8.0", 41 | "@stoplight/lifecycle": "^2.3.2", 42 | "@types/json-schema": "^7.0.7", 43 | "magic-error": "0.0.1" 44 | }, 45 | "devDependencies": { 46 | "@rollup/plugin-typescript": "3", 47 | "@stoplight/eslint-config": "^2.0.0", 48 | "@stoplight/scripts": "8.2.1", 49 | "@stoplight/types": "^12.0.0", 50 | "@types/jest": "^26.0.19", 51 | "@types/node": "^14.14.14", 52 | "@types/treeify": "^1.0.0", 53 | "@typescript-eslint/eslint-plugin": "^4.11.0", 54 | "@typescript-eslint/parser": "^4.11.0", 55 | "babel-jest": "^26.6.3", 56 | "eslint": "^7.16.0", 57 | "eslint-plugin-import": "^2.20.2", 58 | "eslint-plugin-jest": "^24.1.3", 59 | "eslint-plugin-prettier": "^3.3.0", 60 | "eslint-plugin-react": "^7.21.5", 61 | "eslint-plugin-react-hooks": "^4.2.0", 62 | "eslint-plugin-simple-import-sort": "^7.0.0", 63 | "fast-glob": "^3.2.4", 64 | "jest": "^26.6.2", 65 | "prettier": "^2.2.1", 66 | "treeify": "^1.1.0", 67 | "ts-jest": "^26.4.4", 68 | "typescript": "^4.1.3" 69 | }, 70 | "resolutions": { 71 | "cross-spawn": "7.0.5" 72 | }, 73 | "lint-staged": { 74 | "*.{ts,tsx}": [ 75 | "eslint --fix --cache --cache-location .cache" 76 | ], 77 | "docs/**/*.md": [ 78 | "prettier --ignore-path .eslintignore --write" 79 | ], 80 | "README.md": [ 81 | "prettier --write" 82 | ] 83 | }, 84 | "husky": { 85 | "hooks": { 86 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 87 | "pre-commit": "lint-staged" 88 | } 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "node_modules/cz-conventional-changelog" 93 | } 94 | }, 95 | "commitlint": { 96 | "extends": [ 97 | "@commitlint/config-conventional" 98 | ] 99 | }, 100 | "release": { 101 | "extends": "@stoplight/scripts/release" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import * as path from 'path'; 3 | 4 | const BASE_PATH = process.cwd(); 5 | 6 | const plugins = [ 7 | typescript({ 8 | tsconfig: path.resolve(BASE_PATH, 'tsconfig.build.json'), 9 | include: ['src/**/*.{ts,tsx}'], 10 | }), 11 | ]; 12 | 13 | export default [ 14 | { 15 | input: path.resolve(BASE_PATH, 'src/index.ts'), 16 | plugins, 17 | output: [ 18 | { 19 | file: path.resolve(BASE_PATH, 'dist/index.cjs.js'), 20 | format: 'cjs', 21 | }, 22 | { 23 | file: path.resolve(BASE_PATH, 'dist/index.es.js'), 24 | format: 'esm', 25 | }, 26 | ], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoplightio/json-schema-tree/636840c263b426d62e699aa6ca83da9acf283e67/setupTests.ts -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/additional-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "additionalItems": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/additional-false.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "additionalItems": false 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/additional-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "additionalItems": { 4 | "type": "object", 5 | "properties": { 6 | "baz": { 7 | "type": "number" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/additional-true.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "additionalItems": true 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/of-allofs.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Test", 3 | "type": "object", 4 | "properties": { 5 | "array-all-objects": { 6 | "type": "array", 7 | "items": { 8 | "allOf": [ 9 | { 10 | "properties": { 11 | "foo": { 12 | "type": "string" 13 | } 14 | } 15 | }, 16 | { 17 | "properties": { 18 | "bar": { 19 | "type": "string" 20 | } 21 | } 22 | } 23 | ], 24 | "type": "object" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/of-arrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "type": "object", 5 | "properties": { 6 | "bar": { 7 | "type": "integer" 8 | }, 9 | "foo": { 10 | "type": "array", 11 | "items": { 12 | "type": "array" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/of-objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "xml": { 4 | "name": "Pet" 5 | }, 6 | "properties": { 7 | "propertyIsArrayOfObjects": { 8 | "type": ["array"], 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "ArrayObjectProperty": { 13 | "type": "string" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/of-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./models/todo-full.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/with-multiple-arrayish-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": [ 4 | { 5 | "type": "number" 6 | }, 7 | { 8 | "type": "object", 9 | "properties": { 10 | "code": { 11 | "type": "number" 12 | }, 13 | "msg": { 14 | "type": "string" 15 | }, 16 | "ref": { 17 | "type": "string" 18 | } 19 | }, 20 | "required": ["code", "msg"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/with-ordered-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": [ 4 | { 5 | "type": "number" 6 | }, 7 | { 8 | "type": "string" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/arrays/with-single-arrayish-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": [ 4 | { 5 | "type": "object", 6 | "properties": { 7 | "code": { 8 | "type": "number" 9 | }, 10 | "msg": { 11 | "type": "string" 12 | }, 13 | "ref": { 14 | "type": "string" 15 | } 16 | }, 17 | "required": ["code", "msg"] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "AllOfMergeObjects": { 5 | "allOf": [ 6 | { 7 | "properties": { 8 | "Object1Property": { 9 | "type": "string", 10 | "minLength": 1, 11 | "x-val": "lol" 12 | } 13 | } 14 | }, 15 | { 16 | "properties": { 17 | "Object2Property": { 18 | "type": "number", 19 | "maximum": 2 20 | } 21 | } 22 | } 23 | ], 24 | "type": "object" 25 | }, 26 | "AllOfMergeValidations": { 27 | "allOf": [ 28 | { 29 | "minLength": 1 30 | }, 31 | { 32 | "maxLength": 2 33 | } 34 | ], 35 | "type": "string" 36 | }, 37 | "AllOfMergeTakeMoreLogicalValidation": { 38 | "allOf": [ 39 | { 40 | "maximum": 1 41 | }, 42 | { 43 | "maximum": 2 44 | } 45 | ], 46 | "type": "number" 47 | }, 48 | "AllOfMergeObjectPropertyValidations": { 49 | "allOf": [ 50 | { 51 | "properties": { 52 | "Property": { 53 | "type": "string", 54 | "minLength": 1 55 | } 56 | } 57 | }, 58 | { 59 | "properties": { 60 | "Property": { 61 | "type": "string", 62 | "maxLength": 2 63 | } 64 | } 65 | } 66 | ], 67 | "type": "object" 68 | }, 69 | "AllOfMergeRefs": { 70 | "allOf": [ 71 | { "$ref": "#/definitions/ref1" }, 72 | { 73 | "type": "object", 74 | "properties": { 75 | "zipCode": { 76 | "type": "string" 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | }, 83 | 84 | "definitions": { 85 | "ref1": { 86 | "type": "object", 87 | "properties": { 88 | "street_address": { 89 | "type": "string" 90 | }, 91 | "city": { 92 | "type": "string" 93 | }, 94 | "state": { 95 | "type": "string" 96 | } 97 | }, 98 | "required": ["street_address", "city", "state"] 99 | }, 100 | "ref2": { 101 | "type": "object", 102 | "properties": { 103 | "firstName": { 104 | "type": "string" 105 | }, 106 | "lastName": { 107 | "type": "string" 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/circular-regular-level.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "someProp": { 4 | "$ref": "#/__bundled__/repo" 5 | } 6 | }, 7 | "__bundled__": { 8 | "repo": { 9 | "properties": { 10 | "parent": { 11 | "allOf": [ 12 | { 13 | "$ref": "#/__bundled__/repo" 14 | }, 15 | { 16 | "type": "object", 17 | "properties": { 18 | "foo": { 19 | "type": "string" 20 | } 21 | } 22 | } 23 | ] 24 | } 25 | }, 26 | "type": "object" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/circular-top-level.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ref": "#/__bundled__/repo", 3 | "__bundled__": { 4 | "repo": { 5 | "properties": { 6 | "parent": { 7 | "allOf": [ 8 | { 9 | "$ref": "#/__bundled__/repo" 10 | }, 11 | { 12 | "type": "object" 13 | } 14 | ] 15 | } 16 | }, 17 | "type": "object" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "allOf": [ 3 | { 4 | "allOf": [ 5 | { 6 | "type": "object", 7 | "properties": { 8 | "foo": { 9 | "type": "object", 10 | "properties": { 11 | "user": { 12 | "$ref": "#/allOf/0/allOf/0/properties/foo/definitions/event" 13 | } 14 | }, 15 | "definitions": { 16 | "event": { 17 | "allOf": [ 18 | { 19 | "type": "object", 20 | "properties": { 21 | "names": { 22 | "items": { 23 | "$ref": "#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/name" 24 | } 25 | }, 26 | "users": { 27 | "type": "array", 28 | "items": { 29 | "type": "object", 30 | "properties": { 31 | "creation": { 32 | "$ref": "#/allOf/0/allOf/0/properties/foo" 33 | }, 34 | "foo": { 35 | "$ref": "#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts" 36 | }, 37 | "products": { 38 | "$ref": "#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | } 48 | } 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | "type": "object", 55 | "properties": { 56 | "bar": { 57 | "allOf": [ 58 | { 59 | "$ref": "#/allOf/0/allOf/0" 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/nested-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": ["limit", "order"], 3 | "type": "object", 4 | "properties": { 5 | "dimensions": { "type": "array", "items": { "type": "string" } }, 6 | "measures": { "type": "array", "items": { "type": "string" } }, 7 | "limit": { 8 | "maximum": 2000, 9 | "minimum": 0, 10 | "type": "integer", 11 | "format": "int32" 12 | }, 13 | "offset": { 14 | "minimum": 0, 15 | "type": "integer", 16 | "format": "int32", 17 | "maximum": 2147483647 18 | }, 19 | "filters": { 20 | "type": "array", 21 | "items": { "oneOf": [{ "$ref": "#/$defs/Logical" }, { "$ref": "#/$defs/Plain" }] } 22 | }, 23 | "timeDimensions": { 24 | "maxItems": 1, 25 | "minItems": 0, 26 | "type": "array", 27 | "items": { "$ref": "#/$defs/TimeDimension" } 28 | }, 29 | "order": { "type": "object", "additionalProperties": { "type": "string", "enum": ["ASC", "DESC"] } }, 30 | "nextToken": { "type": "string" } 31 | }, 32 | "$defs": { 33 | "Logical": { "type": "object", "allOf": [{ "$ref": "#/$defs/Filter" }] }, 34 | "Filter": { "type": "object", "anyOf": [{ "$ref": "#/$defs/Logical" }, { "$ref": "#/$defs/Plain" }] }, 35 | "Plain": { 36 | "required": ["member", "operator"], 37 | "type": "object", 38 | "allOf": [ 39 | { "$ref": "#/$defs/Filter" }, 40 | { 41 | "type": "object", 42 | "properties": { 43 | "member": { "type": "string" }, 44 | "operator": { "pattern": "equals|notEquals|gt|gte|lt|lte|set|notSet|inDateRange", "type": "string" }, 45 | "values": { "type": "array", "items": { "type": "object" } } 46 | } 47 | } 48 | ] 49 | }, 50 | "TimeDimension": { 51 | "required": ["dateRange", "dimension", "granularity"], 52 | "type": "object", 53 | "properties": { 54 | "dimension": { "type": "string" }, 55 | "granularity": { "pattern": "second|minute|hour|day", "type": "string" }, 56 | "dateRange": { "type": "object", "example": ["2022-04-19T16:00:00.000Z", "2022-04-19T17:00:00.000Z"] } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/todo-full-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Todo Full", 3 | "allOf": [ 4 | { 5 | "properties": { 6 | "test": { 7 | "type": "string" 8 | } 9 | } 10 | }, 11 | { 12 | "properties": { 13 | "id": { 14 | "type": "integer", 15 | "minimum": 0, 16 | "maximum": 1000000 17 | }, 18 | "completed_at": { 19 | "type": ["string", "null"], 20 | "format": "date-time" 21 | }, 22 | "created_at": { 23 | "type": "string", 24 | "format": "date-time" 25 | }, 26 | "updated_at": { 27 | "type": "string", 28 | "format": "date-time" 29 | } 30 | }, 31 | "required": ["id"] 32 | } 33 | ], 34 | "type": "object" 35 | } 36 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/todo-full.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "title": "Todo Full", 5 | "allOf": [ 6 | { 7 | "title": "Todo Partial", 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "completed": { 14 | "type": [ 15 | "boolean", 16 | "null" 17 | ] 18 | } 19 | }, 20 | "required": [ 21 | "name", 22 | "completed" 23 | ], 24 | "x-tags": [ 25 | "Todos" 26 | ] 27 | }, 28 | { 29 | "type": "object", 30 | "properties": { 31 | "id": { 32 | "type": "integer", 33 | "minimum": 0, 34 | "maximum": 1000000 35 | }, 36 | "completed_at": { 37 | "type": [ 38 | "string", 39 | "null" 40 | ], 41 | "format": "date-time" 42 | }, 43 | "created_at": { 44 | "type": "string", 45 | "format": "date-time" 46 | }, 47 | "updated_at": { 48 | "type": "string", 49 | "format": "date-time" 50 | }, 51 | "user": { 52 | "title": "User", 53 | "type": "object", 54 | "properties": { 55 | "name": { 56 | "type": "string", 57 | "description": "The user's full name." 58 | }, 59 | "age": { 60 | "type": "number", 61 | "minimum": 0, 62 | "maximum": 150 63 | } 64 | }, 65 | "required": [ 66 | "name", 67 | "age" 68 | ], 69 | "x-tags": [ 70 | "Todos" 71 | ] 72 | } 73 | }, 74 | "required": [ 75 | "id", 76 | "user" 77 | ] 78 | } 79 | ], 80 | "x-tags": [ 81 | "Todos" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/allOfs/with-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "BugExample", 3 | "description": "An example model to demonstrate a bug.", 4 | "allOf": [ 5 | { 6 | "properties": { 7 | "actionType": { 8 | "type": "string", 9 | "enum": [ 10 | "Cancel", 11 | "Confirm", 12 | "Create", 13 | "Update" 14 | ] 15 | }, 16 | "id": { 17 | "type": "string", 18 | "description": "The identifier of the existing reservation." 19 | }, 20 | "externalId": { 21 | "type": "string" 22 | }, 23 | "calculateCosts": { 24 | "type": "boolean" 25 | }, 26 | "calculateDates": { 27 | "type": "boolean" 28 | }, 29 | "items": { 30 | "type": "array", 31 | "items": { 32 | "type": "string" 33 | } 34 | } 35 | }, 36 | "required": [ 37 | "actionType", 38 | "items" 39 | ] 40 | }, 41 | { 42 | "oneOf": [ 43 | { 44 | "type": "object", 45 | "properties": { 46 | "actionType": { 47 | "type": "string", 48 | "enum": [ 49 | "Cancel", 50 | "Confirm", 51 | "Update" 52 | ] 53 | }, 54 | "id": { 55 | "type": "string" 56 | } 57 | }, 58 | "required": [ 59 | "actionType", 60 | "id" 61 | ] 62 | }, 63 | { 64 | "type": "object", 65 | "properties": { 66 | "actionType": { 67 | "type": "string", 68 | "enum": [ 69 | "Create" 70 | ] 71 | } 72 | }, 73 | "required": [ 74 | "actionType" 75 | ] 76 | } 77 | ] 78 | } 79 | ], 80 | "type": "object" 81 | } 82 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/combiners/oneof-with-array-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "oneOf": [ 3 | { 4 | "items": { 5 | "properties": { 6 | "foo": { 7 | "type": "string", 8 | "enum": ["test"] 9 | } 10 | } 11 | } 12 | }, 13 | { 14 | "items": { 15 | "properties": { 16 | "foo": { 17 | "type": "number" 18 | }, 19 | "bar": { 20 | "type": "string" 21 | } 22 | } 23 | } 24 | } 25 | ], 26 | "type": "array", 27 | "items": { 28 | "type": "object", 29 | "properties": { 30 | "foo": { 31 | "type": ["string", "number"] 32 | }, 33 | "baz": { 34 | "type": "integer" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/default-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "User", 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "description": "The user's full name. This description can be long and should truncate once it reaches the end of the row. If it's not truncating then theres and issue that needs to be fixed. Help!" 8 | }, 9 | "age": { 10 | "type": "number", 11 | "minimum": 0, 12 | "maximum": 150, 13 | "multipleOf": 10, 14 | "exclusiveMinimum": true, 15 | "exclusiveMaximum": true, 16 | "readOnly": true 17 | }, 18 | "completed_at": { 19 | "type": "string", 20 | "format": "date-time", 21 | "writeOnly": true 22 | }, 23 | "items": { 24 | "type": ["null", "array"], 25 | "items": { 26 | "type": ["string", "number"] 27 | }, 28 | "description": "This description can be long and should truncate once it reaches the end of the row. If it's not truncating then theres and issue that needs to be fixed. Help!" 29 | }, 30 | "email": { 31 | "type": "string", 32 | "format": "email", 33 | "example": "email@email.com", 34 | "deprecated": true, 35 | "minLength": 2 36 | }, 37 | "plan": { 38 | "anyOf": [ 39 | { 40 | "type": "object", 41 | "properties": { 42 | "foo": { 43 | "type": "string" 44 | }, 45 | "bar": { 46 | "type": "string" 47 | } 48 | }, 49 | "deprecated": false, 50 | "example": "hi", 51 | "required": ["foo", "bar"] 52 | }, 53 | { 54 | "type": "array", 55 | "items": { 56 | "type": "integer" 57 | } 58 | } 59 | ] 60 | }, 61 | "permissions": { 62 | "type": ["string", "object"], 63 | "properties": { 64 | "ids": { 65 | "type": "array", 66 | "items": { 67 | "type": "integer" 68 | } 69 | } 70 | } 71 | }, 72 | "ref": { 73 | "$ref": "#/properties/permissions" 74 | } 75 | }, 76 | "patternProperties": { 77 | "^id_": { "type": "number" }, 78 | "foo": { "type": "integer" }, 79 | "_name$": { "type": "string" } 80 | }, 81 | "required": ["name", "age", "completed_at"] 82 | } 83 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/formats-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "model-with-formats", 3 | "type": "object", 4 | "properties": { 5 | "date-of-birth": { 6 | "type": [ 7 | "number", 8 | "string", 9 | "array" 10 | ], 11 | "format": "date-time", 12 | "items": {} 13 | }, 14 | "name": { 15 | "type": "string" 16 | }, 17 | "id": { 18 | "type": "number", 19 | "format": "float" 20 | }, 21 | "notype": { 22 | "format": "date-time" 23 | }, 24 | "permissions": { 25 | "type": [ 26 | "string", 27 | "object" 28 | ], 29 | "format": "password", 30 | "properties": { 31 | "ids": { 32 | "type": "array", 33 | "items": { 34 | "type": "integer", 35 | "format": "int32" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/objects/additional-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/objects/additional-false.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/objects/additional-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": { 4 | "type": "object", 5 | "properties": { 6 | "baz": { 7 | "type": "number" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/objects/additional-true.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": true 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/recursive-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Thing", 3 | "allOf": [ 4 | { 5 | "$ref": "#/definitions/User" 6 | } 7 | ], 8 | "description": "baz", 9 | "definitions": { 10 | "User": { 11 | "type": "object", 12 | "description": "user", 13 | "properties": { 14 | "manager": { 15 | "$ref": "#/definitions/Boss" 16 | } 17 | } 18 | }, 19 | "Boss": { 20 | "$ref": "#/definitions/User", 21 | "description": "xyz" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/references/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "address": { 5 | "type": "object", 6 | "properties": { 7 | "street_address": { 8 | "type": "string" 9 | }, 10 | "city": { 11 | "type": "string" 12 | }, 13 | "state": { 14 | "type": "string" 15 | } 16 | }, 17 | "required": [ 18 | "street_address", 19 | "city", 20 | "state" 21 | ] 22 | } 23 | }, 24 | "type": "object", 25 | "properties": { 26 | "billing_address": { 27 | "$ref": "#/definitions/address" 28 | }, 29 | "shipping_address": { 30 | "$ref": "#/definitions/address" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/references/circular-with-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "order": { 5 | "$ref": "#/definitions/Order", 6 | "description": "My Order" 7 | } 8 | }, 9 | "definitions": { 10 | "Cart": { 11 | "type": "object", 12 | "properties": { 13 | "order": { 14 | "$ref": "#/definitions/Order", 15 | "description": "My Order" 16 | } 17 | } 18 | }, 19 | "Order": { 20 | "type": "object", 21 | "properties": { 22 | "member": { 23 | "$ref": "#/definitions/Member", 24 | "description": "Member" 25 | } 26 | } 27 | }, 28 | "Member": { 29 | "type": "object", 30 | "properties": { 31 | "referredMember": { 32 | "$ref": "#/definitions/Member", 33 | "description": "Member" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/references/nullish.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "empty-ref": { 4 | "$ref": null 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/references/with-overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "oneOf": [ 3 | { 4 | "$ref": "#/definitions/User" 5 | } 6 | ], 7 | "description": "User Model", 8 | "definitions": { 9 | "User": { 10 | "type": "object", 11 | "description": "Plain User", 12 | "properties": { 13 | "manager": { 14 | "$ref": "#/definitions/Admin" 15 | } 16 | } 17 | }, 18 | "Admin": { 19 | "$ref": "#/definitions/User", 20 | "description": "Admin User" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/tickets.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "description": "This section allows the selection of the ticketing options for all sales of the order.", 4 | "properties": { 5 | "availableTicketingOptions": { 6 | "description": "List of ticketing options of the order.", 7 | "type": "array", 8 | "items": { 9 | "$ref": "../TicketingOptionInfo/TicketingOptionInfo.v1-0.yaml" 10 | } 11 | }, 12 | "commonTicketingOptions": { 13 | "type": "array", 14 | "description": "Common ticketing options to all order items.", 15 | "items": { 16 | "type": "string" 17 | } 18 | }, 19 | "ticketingOptionChoice": { 20 | "type": "array", 21 | "description": "Ticketing option selection per order item.", 22 | "items": { 23 | "type": "object", 24 | "properties": { 25 | "state": { 26 | "description": "The status that addresses if a specific ticket option is active or not. The status active is used before ticketing or before exchange confirmation. After ticketing, the status changes in completed. This allows to store ticketing options already used at ticketing time and to clean up all non selected options after ticketing or exchanged confirmation.", 27 | "type": "string", 28 | "default": "ACTIVE", 29 | "enum": [ 30 | "COMPLETED", 31 | "ACTIVE" 32 | ] 33 | }, 34 | "orderItemBreakdown": { 35 | "description": "Structure that contains ticketing options per order item.", 36 | "type": "array", 37 | "items": { 38 | "type": "object", 39 | "properties": { 40 | "orderItemId": { 41 | "type": "string", 42 | "format": "uuid" 43 | }, 44 | "options": { 45 | "description": "Available ticketing options for a given order item.", 46 | "type": "array", 47 | "items": { 48 | "type": "object", 49 | "properties": { 50 | "title": { 51 | "description": "Ticketing option short-description.", 52 | "type": "string", 53 | "readOnly": true, 54 | "enum": [ 55 | "HOMEPRINT", 56 | "TICKETLESS", 57 | "PRINT_AT_KIOSK", 58 | "SECURE_PAPER" 59 | ] 60 | }, 61 | "selected": { 62 | "description": "Flag to specify which ticketing option is selected. Only one option is allowed to be selected.", 63 | "type": "boolean", 64 | "example": true 65 | }, 66 | "additionalRequiredInfo": { 67 | "description": "Additional passenger required info specific to the given ticketing option.", 68 | "type": "string" 69 | }, 70 | "deliveryInfo": { 71 | "description": "Data for ticket delivery.", 72 | "type": "object", 73 | "properties": { 74 | "availableDeliveryTypes": { 75 | "type": "array", 76 | "items": { 77 | "type": "string", 78 | "enum": [ 79 | "POSTAL", 80 | "PICK_UP_STATION", 81 | "E-MAIL", 82 | "LOYALTY_CARD" 83 | ] 84 | } 85 | }, 86 | "ticketRecipients": { 87 | "type": "array", 88 | "items": { 89 | "type": "string", 90 | "enum": [ 91 | "BOOKER", 92 | "CUSTOMER", 93 | "PASSENGER", 94 | "THIRD_PARTY" 95 | ] 96 | } 97 | }, 98 | "ccEmail": { 99 | "type": "string", 100 | "format": "email" 101 | }, 102 | "postalAddress": { 103 | "$ref": "../Address/Address.v0-1.yaml" 104 | }, 105 | "pickUpAtStation": { 106 | "description": "The name of the Station in case you select pick up at station as a delivery type", 107 | "type": "string" 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/tree.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SchemaTree output compound keywords given anyOf combiner placed next to allOf given allOf merging disabled, should still merge 1`] = ` 4 | "└─ # 5 | ├─ combiners 6 | │ └─ 0: anyOf 7 | └─ children 8 | ├─ 0 9 | │ └─ #/anyOf/0 10 | │ ├─ combiners 11 | │ │ └─ 0: allOf 12 | │ └─ children 13 | │ ├─ 0 14 | │ │ └─ #/anyOf/0/allOf/0 15 | │ │ ├─ types 16 | │ │ │ └─ 0: object 17 | │ │ ├─ primaryType: object 18 | │ │ └─ children 19 | │ │ ├─ 0 20 | │ │ │ └─ #/anyOf/0/allOf/0/properties/type 21 | │ │ │ ├─ types 22 | │ │ │ │ └─ 0: string 23 | │ │ │ ├─ primaryType: string 24 | │ │ │ └─ enum 25 | │ │ │ ├─ 0: admin 26 | │ │ │ └─ 1: editor 27 | │ │ └─ 1 28 | │ │ └─ #/anyOf/0/allOf/0/properties/enabled 29 | │ │ ├─ types 30 | │ │ │ └─ 0: boolean 31 | │ │ └─ primaryType: boolean 32 | │ └─ 1 33 | │ └─ #/anyOf/0/allOf/1 34 | │ ├─ types 35 | │ │ └─ 0: object 36 | │ ├─ primaryType: object 37 | │ └─ children 38 | │ ├─ 0 39 | │ │ └─ #/anyOf/0/allOf/1/properties/root 40 | │ │ ├─ types 41 | │ │ │ └─ 0: boolean 42 | │ │ └─ primaryType: boolean 43 | │ ├─ 1 44 | │ │ └─ #/anyOf/0/allOf/1/properties/group 45 | │ │ ├─ types 46 | │ │ │ └─ 0: string 47 | │ │ └─ primaryType: string 48 | │ └─ 2 49 | │ └─ #/anyOf/0/allOf/1/properties/expirationDate 50 | │ ├─ types 51 | │ │ └─ 0: string 52 | │ └─ primaryType: string 53 | └─ 1 54 | └─ #/anyOf/1 55 | ├─ combiners 56 | │ └─ 0: allOf 57 | └─ children 58 | ├─ 0 59 | │ └─ #/anyOf/1/allOf/0 60 | │ └─ mirrors: #/anyOf/0/allOf/0 61 | └─ 1 62 | └─ #/anyOf/1/allOf/1 63 | ├─ types 64 | │ └─ 0: object 65 | ├─ primaryType: object 66 | └─ children 67 | ├─ 0 68 | │ └─ #/anyOf/1/allOf/1/properties/supervisor 69 | │ ├─ types 70 | │ │ └─ 0: string 71 | │ └─ primaryType: string 72 | └─ 1 73 | └─ #/anyOf/1/allOf/1/properties/key 74 | ├─ types 75 | │ └─ 0: string 76 | └─ primaryType: string 77 | " 78 | `; 79 | 80 | exports[`SchemaTree output compound keywords given anyOf combiner placed next to allOf given allOf merging enabled, should merge contents of allOf combiners 1`] = ` 81 | "└─ # 82 | ├─ combiners 83 | │ └─ 0: anyOf 84 | └─ children 85 | ├─ 0 86 | │ └─ #/anyOf/0 87 | │ ├─ types 88 | │ │ └─ 0: object 89 | │ ├─ primaryType: object 90 | │ └─ children 91 | │ ├─ 0 92 | │ │ └─ #/anyOf/0/properties/type 93 | │ │ ├─ types 94 | │ │ │ └─ 0: string 95 | │ │ ├─ primaryType: string 96 | │ │ └─ enum 97 | │ │ ├─ 0: admin 98 | │ │ └─ 1: editor 99 | │ ├─ 1 100 | │ │ └─ #/anyOf/0/properties/enabled 101 | │ │ ├─ types 102 | │ │ │ └─ 0: boolean 103 | │ │ └─ primaryType: boolean 104 | │ ├─ 2 105 | │ │ └─ #/anyOf/0/properties/root 106 | │ │ ├─ types 107 | │ │ │ └─ 0: boolean 108 | │ │ └─ primaryType: boolean 109 | │ ├─ 3 110 | │ │ └─ #/anyOf/0/properties/group 111 | │ │ ├─ types 112 | │ │ │ └─ 0: string 113 | │ │ └─ primaryType: string 114 | │ └─ 4 115 | │ └─ #/anyOf/0/properties/expirationDate 116 | │ ├─ types 117 | │ │ └─ 0: string 118 | │ └─ primaryType: string 119 | └─ 1 120 | └─ #/anyOf/1 121 | ├─ types 122 | │ └─ 0: object 123 | ├─ primaryType: object 124 | └─ children 125 | ├─ 0 126 | │ └─ #/anyOf/1/properties/type 127 | │ ├─ types 128 | │ │ └─ 0: string 129 | │ ├─ primaryType: string 130 | │ └─ enum 131 | │ ├─ 0: admin 132 | │ └─ 1: editor 133 | ├─ 1 134 | │ └─ #/anyOf/1/properties/enabled 135 | │ ├─ types 136 | │ │ └─ 0: boolean 137 | │ └─ primaryType: boolean 138 | ├─ 2 139 | │ └─ #/anyOf/1/properties/supervisor 140 | │ ├─ types 141 | │ │ └─ 0: string 142 | │ └─ primaryType: string 143 | └─ 3 144 | └─ #/anyOf/1/properties/key 145 | ├─ types 146 | │ └─ 0: string 147 | └─ primaryType: string 148 | " 149 | `; 150 | 151 | exports[`SchemaTree output compound keywords given oneOf combiner placed next to allOf given allOf merging disabled, should still merge 1`] = ` 152 | "└─ # 153 | ├─ combiners 154 | │ └─ 0: oneOf 155 | └─ children 156 | ├─ 0 157 | │ └─ #/oneOf/0 158 | │ ├─ combiners 159 | │ │ └─ 0: allOf 160 | │ └─ children 161 | │ ├─ 0 162 | │ │ └─ #/oneOf/0/allOf/0 163 | │ │ ├─ types 164 | │ │ │ └─ 0: object 165 | │ │ ├─ primaryType: object 166 | │ │ └─ children 167 | │ │ ├─ 0 168 | │ │ │ └─ #/oneOf/0/allOf/0/properties/type 169 | │ │ │ ├─ types 170 | │ │ │ │ └─ 0: string 171 | │ │ │ ├─ primaryType: string 172 | │ │ │ └─ enum 173 | │ │ │ ├─ 0: admin 174 | │ │ │ └─ 1: editor 175 | │ │ └─ 1 176 | │ │ └─ #/oneOf/0/allOf/0/properties/enabled 177 | │ │ ├─ types 178 | │ │ │ └─ 0: boolean 179 | │ │ └─ primaryType: boolean 180 | │ └─ 1 181 | │ └─ #/oneOf/0/allOf/1 182 | │ ├─ types 183 | │ │ └─ 0: object 184 | │ ├─ primaryType: object 185 | │ └─ children 186 | │ ├─ 0 187 | │ │ └─ #/oneOf/0/allOf/1/properties/root 188 | │ │ ├─ types 189 | │ │ │ └─ 0: boolean 190 | │ │ └─ primaryType: boolean 191 | │ ├─ 1 192 | │ │ └─ #/oneOf/0/allOf/1/properties/group 193 | │ │ ├─ types 194 | │ │ │ └─ 0: string 195 | │ │ └─ primaryType: string 196 | │ └─ 2 197 | │ └─ #/oneOf/0/allOf/1/properties/expirationDate 198 | │ ├─ types 199 | │ │ └─ 0: string 200 | │ └─ primaryType: string 201 | └─ 1 202 | └─ #/oneOf/1 203 | ├─ combiners 204 | │ └─ 0: allOf 205 | └─ children 206 | ├─ 0 207 | │ └─ #/oneOf/1/allOf/0 208 | │ └─ mirrors: #/oneOf/0/allOf/0 209 | └─ 1 210 | └─ #/oneOf/1/allOf/1 211 | ├─ types 212 | │ └─ 0: object 213 | ├─ primaryType: object 214 | └─ children 215 | ├─ 0 216 | │ └─ #/oneOf/1/allOf/1/properties/supervisor 217 | │ ├─ types 218 | │ │ └─ 0: string 219 | │ └─ primaryType: string 220 | └─ 1 221 | └─ #/oneOf/1/allOf/1/properties/key 222 | ├─ types 223 | │ └─ 0: string 224 | └─ primaryType: string 225 | " 226 | `; 227 | 228 | exports[`SchemaTree output compound keywords given oneOf combiner placed next to allOf given allOf merging enabled, should merge contents of allOf combiners 1`] = ` 229 | "└─ # 230 | ├─ combiners 231 | │ └─ 0: oneOf 232 | └─ children 233 | ├─ 0 234 | │ └─ #/oneOf/0 235 | │ ├─ types 236 | │ │ └─ 0: object 237 | │ ├─ primaryType: object 238 | │ └─ children 239 | │ ├─ 0 240 | │ │ └─ #/oneOf/0/properties/type 241 | │ │ ├─ types 242 | │ │ │ └─ 0: string 243 | │ │ ├─ primaryType: string 244 | │ │ └─ enum 245 | │ │ ├─ 0: admin 246 | │ │ └─ 1: editor 247 | │ ├─ 1 248 | │ │ └─ #/oneOf/0/properties/enabled 249 | │ │ ├─ types 250 | │ │ │ └─ 0: boolean 251 | │ │ └─ primaryType: boolean 252 | │ ├─ 2 253 | │ │ └─ #/oneOf/0/properties/root 254 | │ │ ├─ types 255 | │ │ │ └─ 0: boolean 256 | │ │ └─ primaryType: boolean 257 | │ ├─ 3 258 | │ │ └─ #/oneOf/0/properties/group 259 | │ │ ├─ types 260 | │ │ │ └─ 0: string 261 | │ │ └─ primaryType: string 262 | │ └─ 4 263 | │ └─ #/oneOf/0/properties/expirationDate 264 | │ ├─ types 265 | │ │ └─ 0: string 266 | │ └─ primaryType: string 267 | └─ 1 268 | └─ #/oneOf/1 269 | ├─ types 270 | │ └─ 0: object 271 | ├─ primaryType: object 272 | └─ children 273 | ├─ 0 274 | │ └─ #/oneOf/1/properties/type 275 | │ ├─ types 276 | │ │ └─ 0: string 277 | │ ├─ primaryType: string 278 | │ └─ enum 279 | │ ├─ 0: admin 280 | │ └─ 1: editor 281 | ├─ 1 282 | │ └─ #/oneOf/1/properties/enabled 283 | │ ├─ types 284 | │ │ └─ 0: boolean 285 | │ └─ primaryType: boolean 286 | ├─ 2 287 | │ └─ #/oneOf/1/properties/supervisor 288 | │ ├─ types 289 | │ │ └─ 0: string 290 | │ └─ primaryType: string 291 | └─ 3 292 | └─ #/oneOf/1/properties/key 293 | ├─ types 294 | │ └─ 0: string 295 | └─ primaryType: string 296 | " 297 | `; 298 | 299 | exports[`SchemaTree output should generate valid tree for arrays/additional-empty.json 1`] = ` 300 | "└─ # 301 | ├─ types 302 | │ └─ 0: array 303 | ├─ primaryType: array 304 | └─ children 305 | └─ 0 306 | └─ #/additionalItems 307 | " 308 | `; 309 | 310 | exports[`SchemaTree output should generate valid tree for arrays/additional-false.json 1`] = ` 311 | "└─ # 312 | ├─ types 313 | │ └─ 0: array 314 | ├─ primaryType: array 315 | └─ children 316 | └─ 0 317 | └─ #/additionalItems 318 | └─ value: false 319 | " 320 | `; 321 | 322 | exports[`SchemaTree output should generate valid tree for arrays/additional-schema.json 1`] = ` 323 | "└─ # 324 | ├─ types 325 | │ └─ 0: array 326 | ├─ primaryType: array 327 | └─ children 328 | └─ 0 329 | └─ #/additionalItems 330 | ├─ types 331 | │ └─ 0: object 332 | ├─ primaryType: object 333 | └─ children 334 | └─ 0 335 | └─ #/additionalItems/properties/baz 336 | ├─ types 337 | │ └─ 0: number 338 | └─ primaryType: number 339 | " 340 | `; 341 | 342 | exports[`SchemaTree output should generate valid tree for arrays/additional-true.json 1`] = ` 343 | "└─ # 344 | ├─ types 345 | │ └─ 0: array 346 | ├─ primaryType: array 347 | └─ children 348 | └─ 0 349 | └─ #/additionalItems 350 | └─ value: true 351 | " 352 | `; 353 | 354 | exports[`SchemaTree output should generate valid tree for arrays/of-allofs.json 1`] = ` 355 | "└─ # 356 | ├─ types 357 | │ └─ 0: object 358 | ├─ primaryType: object 359 | └─ children 360 | └─ 0 361 | └─ #/properties/array-all-objects 362 | ├─ types 363 | │ └─ 0: array 364 | ├─ primaryType: array 365 | └─ children 366 | └─ 0 367 | └─ #/properties/array-all-objects/items 368 | ├─ types 369 | │ └─ 0: object 370 | ├─ primaryType: object 371 | └─ children 372 | ├─ 0 373 | │ └─ #/properties/array-all-objects/items/properties/foo 374 | │ ├─ types 375 | │ │ └─ 0: string 376 | │ └─ primaryType: string 377 | └─ 1 378 | └─ #/properties/array-all-objects/items/properties/bar 379 | ├─ types 380 | │ └─ 0: string 381 | └─ primaryType: string 382 | " 383 | `; 384 | 385 | exports[`SchemaTree output should generate valid tree for arrays/of-arrays.json 1`] = ` 386 | "└─ # 387 | ├─ types 388 | │ └─ 0: array 389 | ├─ primaryType: array 390 | └─ children 391 | └─ 0 392 | └─ #/items 393 | ├─ types 394 | │ └─ 0: object 395 | ├─ primaryType: object 396 | └─ children 397 | ├─ 0 398 | │ └─ #/items/properties/bar 399 | │ ├─ types 400 | │ │ └─ 0: integer 401 | │ └─ primaryType: integer 402 | └─ 1 403 | └─ #/items/properties/foo 404 | ├─ types 405 | │ └─ 0: array 406 | ├─ primaryType: array 407 | └─ children 408 | └─ 0 409 | └─ #/items/properties/foo/items 410 | ├─ types 411 | │ └─ 0: array 412 | └─ primaryType: array 413 | " 414 | `; 415 | 416 | exports[`SchemaTree output should generate valid tree for arrays/of-objects.json 1`] = ` 417 | "└─ # 418 | ├─ types 419 | │ └─ 0: object 420 | ├─ primaryType: object 421 | └─ children 422 | └─ 0 423 | └─ #/properties/propertyIsArrayOfObjects 424 | ├─ types 425 | │ └─ 0: array 426 | ├─ primaryType: array 427 | └─ children 428 | └─ 0 429 | └─ #/properties/propertyIsArrayOfObjects/items 430 | ├─ types 431 | │ └─ 0: object 432 | ├─ primaryType: object 433 | └─ children 434 | └─ 0 435 | └─ #/properties/propertyIsArrayOfObjects/items/properties/ArrayObjectProperty 436 | ├─ types 437 | │ └─ 0: string 438 | └─ primaryType: string 439 | " 440 | `; 441 | 442 | exports[`SchemaTree output should generate valid tree for arrays/of-refs.json 1`] = ` 443 | "└─ # 444 | ├─ types 445 | │ └─ 0: array 446 | ├─ primaryType: array 447 | └─ children 448 | └─ 0 449 | └─ #/items 450 | ├─ $ref: ./models/todo-full.json 451 | ├─ external: true 452 | └─ error: Cannot dereference external references 453 | " 454 | `; 455 | 456 | exports[`SchemaTree output should generate valid tree for arrays/with-multiple-arrayish-items.json 1`] = ` 457 | "└─ # 458 | ├─ types 459 | │ └─ 0: array 460 | ├─ primaryType: array 461 | └─ children 462 | ├─ 0 463 | │ └─ #/items/0 464 | │ ├─ types 465 | │ │ └─ 0: number 466 | │ └─ primaryType: number 467 | └─ 1 468 | └─ #/items/1 469 | ├─ types 470 | │ └─ 0: object 471 | ├─ primaryType: object 472 | └─ children 473 | ├─ 0 474 | │ └─ #/items/1/properties/code 475 | │ ├─ types 476 | │ │ └─ 0: number 477 | │ └─ primaryType: number 478 | ├─ 1 479 | │ └─ #/items/1/properties/msg 480 | │ ├─ types 481 | │ │ └─ 0: string 482 | │ └─ primaryType: string 483 | └─ 2 484 | └─ #/items/1/properties/ref 485 | ├─ types 486 | │ └─ 0: string 487 | └─ primaryType: string 488 | " 489 | `; 490 | 491 | exports[`SchemaTree output should generate valid tree for arrays/with-ordered-items.json 1`] = ` 492 | "└─ # 493 | ├─ types 494 | │ └─ 0: array 495 | ├─ primaryType: array 496 | └─ children 497 | ├─ 0 498 | │ └─ #/items/0 499 | │ ├─ types 500 | │ │ └─ 0: number 501 | │ └─ primaryType: number 502 | └─ 1 503 | └─ #/items/1 504 | ├─ types 505 | │ └─ 0: string 506 | └─ primaryType: string 507 | " 508 | `; 509 | 510 | exports[`SchemaTree output should generate valid tree for arrays/with-single-arrayish-items.json 1`] = ` 511 | "└─ # 512 | ├─ types 513 | │ └─ 0: array 514 | ├─ primaryType: array 515 | └─ children 516 | └─ 0 517 | └─ #/items/0 518 | ├─ types 519 | │ └─ 0: object 520 | ├─ primaryType: object 521 | └─ children 522 | ├─ 0 523 | │ └─ #/items/0/properties/code 524 | │ ├─ types 525 | │ │ └─ 0: number 526 | │ └─ primaryType: number 527 | ├─ 1 528 | │ └─ #/items/0/properties/msg 529 | │ ├─ types 530 | │ │ └─ 0: string 531 | │ └─ primaryType: string 532 | └─ 2 533 | └─ #/items/0/properties/ref 534 | ├─ types 535 | │ └─ 0: string 536 | └─ primaryType: string 537 | " 538 | `; 539 | 540 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/base.json 1`] = ` 541 | "└─ # 542 | ├─ types 543 | │ └─ 0: object 544 | ├─ primaryType: object 545 | └─ children 546 | ├─ 0 547 | │ └─ #/properties/AllOfMergeObjects 548 | │ ├─ types 549 | │ │ └─ 0: object 550 | │ ├─ primaryType: object 551 | │ └─ children 552 | │ ├─ 0 553 | │ │ └─ #/properties/AllOfMergeObjects/properties/Object1Property 554 | │ │ ├─ types 555 | │ │ │ └─ 0: string 556 | │ │ └─ primaryType: string 557 | │ └─ 1 558 | │ └─ #/properties/AllOfMergeObjects/properties/Object2Property 559 | │ ├─ types 560 | │ │ └─ 0: number 561 | │ └─ primaryType: number 562 | ├─ 1 563 | │ └─ #/properties/AllOfMergeValidations 564 | │ ├─ types 565 | │ │ └─ 0: string 566 | │ └─ primaryType: string 567 | ├─ 2 568 | │ └─ #/properties/AllOfMergeTakeMoreLogicalValidation 569 | │ ├─ types 570 | │ │ └─ 0: number 571 | │ └─ primaryType: number 572 | ├─ 3 573 | │ └─ #/properties/AllOfMergeObjectPropertyValidations 574 | │ ├─ types 575 | │ │ └─ 0: object 576 | │ ├─ primaryType: object 577 | │ └─ children 578 | │ └─ 0 579 | │ └─ #/properties/AllOfMergeObjectPropertyValidations/properties/Property 580 | │ ├─ types 581 | │ │ └─ 0: string 582 | │ └─ primaryType: string 583 | └─ 4 584 | └─ #/properties/AllOfMergeRefs 585 | ├─ types 586 | │ └─ 0: object 587 | ├─ primaryType: object 588 | └─ children 589 | ├─ 0 590 | │ └─ #/properties/AllOfMergeRefs/properties/street_address 591 | │ ├─ types 592 | │ │ └─ 0: string 593 | │ └─ primaryType: string 594 | ├─ 1 595 | │ └─ #/properties/AllOfMergeRefs/properties/city 596 | │ ├─ types 597 | │ │ └─ 0: string 598 | │ └─ primaryType: string 599 | ├─ 2 600 | │ └─ #/properties/AllOfMergeRefs/properties/state 601 | │ ├─ types 602 | │ │ └─ 0: string 603 | │ └─ primaryType: string 604 | └─ 3 605 | └─ #/properties/AllOfMergeRefs/properties/zipCode 606 | ├─ types 607 | │ └─ 0: string 608 | └─ primaryType: string 609 | " 610 | `; 611 | 612 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/circular-regular-level.json 1`] = ` 613 | "└─ # 614 | ├─ types 615 | │ └─ 0: object 616 | ├─ primaryType: object 617 | └─ children 618 | └─ 0 619 | └─ #/properties/someProp 620 | ├─ types 621 | │ └─ 0: object 622 | ├─ primaryType: object 623 | └─ children 624 | └─ 0 625 | └─ #/properties/someProp/properties/parent 626 | ├─ types 627 | │ └─ 0: object 628 | ├─ primaryType: object 629 | └─ children 630 | ├─ 0 631 | │ └─ #/properties/someProp/properties/parent/properties/parent 632 | │ └─ mirrors: #/properties/someProp/properties/parent 633 | └─ 1 634 | └─ #/properties/someProp/properties/parent/properties/foo 635 | ├─ types 636 | │ └─ 0: string 637 | └─ primaryType: string 638 | " 639 | `; 640 | 641 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/circular-top-level.json 1`] = ` 642 | "└─ # 643 | ├─ types 644 | │ └─ 0: object 645 | ├─ primaryType: object 646 | └─ children 647 | └─ 0 648 | └─ #/properties/parent 649 | ├─ types 650 | │ └─ 0: object 651 | ├─ primaryType: object 652 | └─ children 653 | └─ 0 654 | └─ #/properties/parent/properties/parent 655 | └─ mirrors: #/properties/parent 656 | " 657 | `; 658 | 659 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/complex.json 1`] = ` 660 | "└─ # 661 | ├─ types 662 | │ └─ 0: object 663 | ├─ primaryType: object 664 | └─ children 665 | ├─ 0 666 | │ └─ #/properties/foo 667 | │ ├─ types 668 | │ │ └─ 0: object 669 | │ ├─ primaryType: object 670 | │ └─ children 671 | │ └─ 0 672 | │ └─ #/properties/foo/properties/user 673 | │ ├─ combiners 674 | │ │ └─ 0: allOf 675 | │ └─ children 676 | │ └─ 0 677 | │ └─ #/properties/foo/properties/user/allOf/0 678 | │ ├─ types 679 | │ │ └─ 0: object 680 | │ ├─ primaryType: object 681 | │ └─ children 682 | │ ├─ 0 683 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/names 684 | │ │ ├─ types 685 | │ │ │ └─ 0: array 686 | │ │ ├─ primaryType: array 687 | │ │ └─ children 688 | │ │ └─ 0 689 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/names/items 690 | │ │ ├─ $ref: #/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/name 691 | │ │ ├─ external: false 692 | │ │ └─ error: Could not resolve '#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/name' 693 | │ └─ 1 694 | │ └─ #/properties/foo/properties/user/allOf/0/properties/users 695 | │ ├─ types 696 | │ │ └─ 0: array 697 | │ ├─ primaryType: array 698 | │ └─ children 699 | │ └─ 0 700 | │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items 701 | │ ├─ types 702 | │ │ └─ 0: object 703 | │ ├─ primaryType: object 704 | │ └─ children 705 | │ ├─ 0 706 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items/properties/creation 707 | │ │ ├─ types 708 | │ │ │ └─ 0: object 709 | │ │ ├─ primaryType: object 710 | │ │ └─ children 711 | │ │ └─ 0 712 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items/properties/creation/properties/user 713 | │ │ ├─ combiners 714 | │ │ │ └─ 0: allOf 715 | │ │ └─ children 716 | │ │ └─ 0 717 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items/properties/creation/properties/user/allOf/0 718 | │ │ └─ mirrors: #/properties/foo/properties/user/allOf/0 719 | │ ├─ 1 720 | │ │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items/properties/foo 721 | │ │ ├─ $ref: #/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts 722 | │ │ ├─ external: false 723 | │ │ └─ error: Could not resolve '#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts' 724 | │ └─ 2 725 | │ └─ #/properties/foo/properties/user/allOf/0/properties/users/items/properties/products 726 | │ ├─ $ref: #/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts 727 | │ ├─ external: false 728 | │ └─ error: Could not resolve '#/allOf/0/allOf/0/properties/foo/definitions/event/allOf/0/properties/contacts' 729 | └─ 1 730 | └─ #/properties/bar 731 | ├─ types 732 | │ └─ 0: object 733 | ├─ primaryType: object 734 | └─ children 735 | └─ 0 736 | └─ #/properties/bar/properties/foo 737 | ├─ types 738 | │ └─ 0: object 739 | ├─ primaryType: object 740 | └─ children 741 | └─ 0 742 | └─ #/properties/bar/properties/foo/properties/user 743 | ├─ combiners 744 | │ └─ 0: allOf 745 | └─ children 746 | └─ 0 747 | └─ #/properties/bar/properties/foo/properties/user/allOf/0 748 | └─ mirrors: #/properties/foo/properties/user/allOf/0 749 | " 750 | `; 751 | 752 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/nested-refs.json 1`] = ` 753 | "└─ # 754 | ├─ types 755 | │ └─ 0: object 756 | ├─ primaryType: object 757 | └─ children 758 | ├─ 0 759 | │ └─ #/properties/dimensions 760 | │ ├─ types 761 | │ │ └─ 0: array 762 | │ ├─ primaryType: array 763 | │ └─ children 764 | │ └─ 0 765 | │ └─ #/properties/dimensions/items 766 | │ ├─ types 767 | │ │ └─ 0: string 768 | │ └─ primaryType: string 769 | ├─ 1 770 | │ └─ #/properties/measures 771 | │ ├─ types 772 | │ │ └─ 0: array 773 | │ ├─ primaryType: array 774 | │ └─ children 775 | │ └─ 0 776 | │ └─ #/properties/measures/items 777 | │ ├─ types 778 | │ │ └─ 0: string 779 | │ └─ primaryType: string 780 | ├─ 2 781 | │ └─ #/properties/limit 782 | │ ├─ types 783 | │ │ └─ 0: integer 784 | │ └─ primaryType: integer 785 | ├─ 3 786 | │ └─ #/properties/offset 787 | │ ├─ types 788 | │ │ └─ 0: integer 789 | │ └─ primaryType: integer 790 | ├─ 4 791 | │ └─ #/properties/filters 792 | │ ├─ types 793 | │ │ └─ 0: array 794 | │ ├─ primaryType: array 795 | │ └─ children 796 | │ └─ 0 797 | │ └─ #/properties/filters/items 798 | │ ├─ combiners 799 | │ │ └─ 0: oneOf 800 | │ └─ children 801 | │ ├─ 0 802 | │ │ └─ #/properties/filters/items/oneOf/0 803 | │ │ ├─ types 804 | │ │ │ └─ 0: object 805 | │ │ ├─ primaryType: object 806 | │ │ ├─ combiners 807 | │ │ │ └─ 0: anyOf 808 | │ │ └─ children 809 | │ │ ├─ 0 810 | │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/0 811 | │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0 812 | │ │ └─ 1 813 | │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1 814 | │ │ ├─ types 815 | │ │ │ └─ 0: object 816 | │ │ ├─ primaryType: object 817 | │ │ ├─ combiners 818 | │ │ │ └─ 0: anyOf 819 | │ │ └─ children 820 | │ │ ├─ 0 821 | │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/anyOf/0 822 | │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0 823 | │ │ ├─ 1 824 | │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/anyOf/1 825 | │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0/anyOf/1 826 | │ │ ├─ 2 827 | │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/member 828 | │ │ │ ├─ types 829 | │ │ │ │ └─ 0: string 830 | │ │ │ └─ primaryType: string 831 | │ │ ├─ 3 832 | │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/operator 833 | │ │ │ ├─ types 834 | │ │ │ │ └─ 0: string 835 | │ │ │ └─ primaryType: string 836 | │ │ └─ 4 837 | │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/values 838 | │ │ ├─ types 839 | │ │ │ └─ 0: array 840 | │ │ ├─ primaryType: array 841 | │ │ └─ children 842 | │ │ └─ 0 843 | │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/values/items 844 | │ │ ├─ types 845 | │ │ │ └─ 0: object 846 | │ │ └─ primaryType: object 847 | │ └─ 1 848 | │ └─ #/properties/filters/items/oneOf/1 849 | │ └─ mirrors: #/properties/filters/items/oneOf/0/anyOf/1 850 | ├─ 5 851 | │ └─ #/properties/timeDimensions 852 | │ ├─ types 853 | │ │ └─ 0: array 854 | │ ├─ primaryType: array 855 | │ └─ children 856 | │ └─ 0 857 | │ └─ #/properties/timeDimensions/items 858 | │ ├─ types 859 | │ │ └─ 0: object 860 | │ ├─ primaryType: object 861 | │ └─ children 862 | │ ├─ 0 863 | │ │ └─ #/properties/timeDimensions/items/properties/dimension 864 | │ │ ├─ types 865 | │ │ │ └─ 0: string 866 | │ │ └─ primaryType: string 867 | │ ├─ 1 868 | │ │ └─ #/properties/timeDimensions/items/properties/granularity 869 | │ │ ├─ types 870 | │ │ │ └─ 0: string 871 | │ │ └─ primaryType: string 872 | │ └─ 2 873 | │ └─ #/properties/timeDimensions/items/properties/dateRange 874 | │ ├─ types 875 | │ │ └─ 0: object 876 | │ └─ primaryType: object 877 | ├─ 6 878 | │ └─ #/properties/order 879 | │ ├─ types 880 | │ │ └─ 0: object 881 | │ ├─ primaryType: object 882 | │ └─ children 883 | │ └─ 0 884 | │ └─ #/properties/order/additionalProperties 885 | │ ├─ types 886 | │ │ └─ 0: string 887 | │ ├─ primaryType: string 888 | │ └─ enum 889 | │ ├─ 0: ASC 890 | │ └─ 1: DESC 891 | └─ 7 892 | └─ #/properties/nextToken 893 | ├─ types 894 | │ └─ 0: string 895 | └─ primaryType: string 896 | " 897 | `; 898 | 899 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/todo-full.json 1`] = ` 900 | "└─ # 901 | ├─ types 902 | │ └─ 0: array 903 | ├─ primaryType: array 904 | └─ children 905 | └─ 0 906 | └─ #/items 907 | ├─ types 908 | │ └─ 0: object 909 | ├─ primaryType: object 910 | └─ children 911 | ├─ 0 912 | │ └─ #/items/properties/name 913 | │ ├─ types 914 | │ │ └─ 0: string 915 | │ └─ primaryType: string 916 | ├─ 1 917 | │ └─ #/items/properties/completed 918 | │ ├─ types 919 | │ │ ├─ 0: boolean 920 | │ │ └─ 1: null 921 | │ └─ primaryType: boolean 922 | ├─ 2 923 | │ └─ #/items/properties/id 924 | │ ├─ types 925 | │ │ └─ 0: integer 926 | │ └─ primaryType: integer 927 | ├─ 3 928 | │ └─ #/items/properties/completed_at 929 | │ ├─ types 930 | │ │ ├─ 0: string 931 | │ │ └─ 1: null 932 | │ └─ primaryType: string 933 | ├─ 4 934 | │ └─ #/items/properties/created_at 935 | │ ├─ types 936 | │ │ └─ 0: string 937 | │ └─ primaryType: string 938 | ├─ 5 939 | │ └─ #/items/properties/updated_at 940 | │ ├─ types 941 | │ │ └─ 0: string 942 | │ └─ primaryType: string 943 | └─ 6 944 | └─ #/items/properties/user 945 | ├─ types 946 | │ └─ 0: object 947 | ├─ primaryType: object 948 | └─ children 949 | ├─ 0 950 | │ └─ #/items/properties/user/properties/name 951 | │ ├─ types 952 | │ │ └─ 0: string 953 | │ └─ primaryType: string 954 | └─ 1 955 | └─ #/items/properties/user/properties/age 956 | ├─ types 957 | │ └─ 0: number 958 | └─ primaryType: number 959 | " 960 | `; 961 | 962 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/todo-full-2.json 1`] = ` 963 | "└─ # 964 | ├─ types 965 | │ └─ 0: object 966 | ├─ primaryType: object 967 | └─ children 968 | ├─ 0 969 | │ └─ #/properties/test 970 | │ ├─ types 971 | │ │ └─ 0: string 972 | │ └─ primaryType: string 973 | ├─ 1 974 | │ └─ #/properties/id 975 | │ ├─ types 976 | │ │ └─ 0: integer 977 | │ └─ primaryType: integer 978 | ├─ 2 979 | │ └─ #/properties/completed_at 980 | │ ├─ types 981 | │ │ ├─ 0: string 982 | │ │ └─ 1: null 983 | │ └─ primaryType: string 984 | ├─ 3 985 | │ └─ #/properties/created_at 986 | │ ├─ types 987 | │ │ └─ 0: string 988 | │ └─ primaryType: string 989 | └─ 4 990 | └─ #/properties/updated_at 991 | ├─ types 992 | │ └─ 0: string 993 | └─ primaryType: string 994 | " 995 | `; 996 | 997 | exports[`SchemaTree output should generate valid tree for combiners/allOfs/with-type.json 1`] = ` 998 | "└─ # 999 | ├─ combiners 1000 | │ └─ 0: oneOf 1001 | └─ children 1002 | ├─ 0 1003 | │ └─ #/oneOf/0 1004 | │ ├─ types 1005 | │ │ └─ 0: object 1006 | │ ├─ primaryType: object 1007 | │ └─ children 1008 | │ ├─ 0 1009 | │ │ └─ #/oneOf/0/properties/actionType 1010 | │ │ ├─ types 1011 | │ │ │ └─ 0: string 1012 | │ │ ├─ primaryType: string 1013 | │ │ └─ enum 1014 | │ │ ├─ 0: Cancel 1015 | │ │ ├─ 1: Confirm 1016 | │ │ └─ 2: Update 1017 | │ ├─ 1 1018 | │ │ └─ #/oneOf/0/properties/id 1019 | │ │ ├─ types 1020 | │ │ │ └─ 0: string 1021 | │ │ └─ primaryType: string 1022 | │ ├─ 2 1023 | │ │ └─ #/oneOf/0/properties/externalId 1024 | │ │ ├─ types 1025 | │ │ │ └─ 0: string 1026 | │ │ └─ primaryType: string 1027 | │ ├─ 3 1028 | │ │ └─ #/oneOf/0/properties/calculateCosts 1029 | │ │ ├─ types 1030 | │ │ │ └─ 0: boolean 1031 | │ │ └─ primaryType: boolean 1032 | │ ├─ 4 1033 | │ │ └─ #/oneOf/0/properties/calculateDates 1034 | │ │ ├─ types 1035 | │ │ │ └─ 0: boolean 1036 | │ │ └─ primaryType: boolean 1037 | │ └─ 5 1038 | │ └─ #/oneOf/0/properties/items 1039 | │ ├─ types 1040 | │ │ └─ 0: array 1041 | │ ├─ primaryType: array 1042 | │ └─ children 1043 | │ └─ 0 1044 | │ └─ #/oneOf/0/properties/items/items 1045 | │ ├─ types 1046 | │ │ └─ 0: string 1047 | │ └─ primaryType: string 1048 | └─ 1 1049 | └─ #/oneOf/1 1050 | ├─ types 1051 | │ └─ 0: object 1052 | ├─ primaryType: object 1053 | └─ children 1054 | ├─ 0 1055 | │ └─ #/oneOf/1/properties/actionType 1056 | │ ├─ types 1057 | │ │ └─ 0: string 1058 | │ ├─ primaryType: string 1059 | │ └─ enum 1060 | │ └─ 0: Create 1061 | ├─ 1 1062 | │ └─ #/oneOf/1/properties/id 1063 | │ ├─ types 1064 | │ │ └─ 0: string 1065 | │ └─ primaryType: string 1066 | ├─ 2 1067 | │ └─ #/oneOf/1/properties/externalId 1068 | │ ├─ types 1069 | │ │ └─ 0: string 1070 | │ └─ primaryType: string 1071 | ├─ 3 1072 | │ └─ #/oneOf/1/properties/calculateCosts 1073 | │ ├─ types 1074 | │ │ └─ 0: boolean 1075 | │ └─ primaryType: boolean 1076 | ├─ 4 1077 | │ └─ #/oneOf/1/properties/calculateDates 1078 | │ ├─ types 1079 | │ │ └─ 0: boolean 1080 | │ └─ primaryType: boolean 1081 | └─ 5 1082 | └─ #/oneOf/1/properties/items 1083 | ├─ types 1084 | │ └─ 0: array 1085 | ├─ primaryType: array 1086 | └─ children 1087 | └─ 0 1088 | └─ #/oneOf/1/properties/items/items 1089 | ├─ types 1090 | │ └─ 0: string 1091 | └─ primaryType: string 1092 | " 1093 | `; 1094 | 1095 | exports[`SchemaTree output should generate valid tree for combiners/oneof-with-array-type.json 1`] = ` 1096 | "└─ # 1097 | ├─ combiners 1098 | │ └─ 0: oneOf 1099 | └─ children 1100 | ├─ 0 1101 | │ └─ #/oneOf/0 1102 | │ ├─ types 1103 | │ │ └─ 0: array 1104 | │ ├─ primaryType: array 1105 | │ └─ children 1106 | │ └─ 0 1107 | │ └─ #/oneOf/0/items 1108 | │ ├─ types 1109 | │ │ └─ 0: object 1110 | │ ├─ primaryType: object 1111 | │ └─ children 1112 | │ ├─ 0 1113 | │ │ └─ #/oneOf/0/items/properties/foo 1114 | │ │ ├─ types 1115 | │ │ │ └─ 0: string 1116 | │ │ ├─ primaryType: string 1117 | │ │ └─ enum 1118 | │ │ └─ 0: test 1119 | │ └─ 1 1120 | │ └─ #/oneOf/0/items/properties/baz 1121 | │ ├─ types 1122 | │ │ └─ 0: integer 1123 | │ └─ primaryType: integer 1124 | └─ 1 1125 | └─ #/oneOf/1 1126 | ├─ types 1127 | │ └─ 0: array 1128 | ├─ primaryType: array 1129 | └─ children 1130 | └─ 0 1131 | └─ #/oneOf/1/items 1132 | ├─ types 1133 | │ └─ 0: object 1134 | ├─ primaryType: object 1135 | └─ children 1136 | ├─ 0 1137 | │ └─ #/oneOf/1/items/properties/foo 1138 | │ ├─ types 1139 | │ │ └─ 0: number 1140 | │ └─ primaryType: number 1141 | ├─ 1 1142 | │ └─ #/oneOf/1/items/properties/baz 1143 | │ ├─ types 1144 | │ │ └─ 0: integer 1145 | │ └─ primaryType: integer 1146 | └─ 2 1147 | └─ #/oneOf/1/items/properties/bar 1148 | ├─ types 1149 | │ └─ 0: string 1150 | └─ primaryType: string 1151 | " 1152 | `; 1153 | 1154 | exports[`SchemaTree output should generate valid tree for default-schema.json 1`] = ` 1155 | "└─ # 1156 | ├─ types 1157 | │ └─ 0: object 1158 | ├─ primaryType: object 1159 | └─ children 1160 | ├─ 0 1161 | │ └─ #/properties/name 1162 | │ ├─ types 1163 | │ │ └─ 0: string 1164 | │ └─ primaryType: string 1165 | ├─ 1 1166 | │ └─ #/properties/age 1167 | │ ├─ types 1168 | │ │ └─ 0: number 1169 | │ └─ primaryType: number 1170 | ├─ 2 1171 | │ └─ #/properties/completed_at 1172 | │ ├─ types 1173 | │ │ └─ 0: string 1174 | │ └─ primaryType: string 1175 | ├─ 3 1176 | │ └─ #/properties/items 1177 | │ ├─ types 1178 | │ │ ├─ 0: null 1179 | │ │ └─ 1: array 1180 | │ ├─ primaryType: array 1181 | │ └─ children 1182 | │ └─ 0 1183 | │ └─ #/properties/items/items 1184 | │ ├─ types 1185 | │ │ ├─ 0: string 1186 | │ │ └─ 1: number 1187 | │ └─ primaryType: string 1188 | ├─ 4 1189 | │ └─ #/properties/email 1190 | │ ├─ types 1191 | │ │ └─ 0: string 1192 | │ └─ primaryType: string 1193 | ├─ 5 1194 | │ └─ #/properties/plan 1195 | │ ├─ combiners 1196 | │ │ └─ 0: anyOf 1197 | │ └─ children 1198 | │ ├─ 0 1199 | │ │ └─ #/properties/plan/anyOf/0 1200 | │ │ ├─ types 1201 | │ │ │ └─ 0: object 1202 | │ │ ├─ primaryType: object 1203 | │ │ └─ children 1204 | │ │ ├─ 0 1205 | │ │ │ └─ #/properties/plan/anyOf/0/properties/foo 1206 | │ │ │ ├─ types 1207 | │ │ │ │ └─ 0: string 1208 | │ │ │ └─ primaryType: string 1209 | │ │ └─ 1 1210 | │ │ └─ #/properties/plan/anyOf/0/properties/bar 1211 | │ │ ├─ types 1212 | │ │ │ └─ 0: string 1213 | │ │ └─ primaryType: string 1214 | │ └─ 1 1215 | │ └─ #/properties/plan/anyOf/1 1216 | │ ├─ types 1217 | │ │ └─ 0: array 1218 | │ ├─ primaryType: array 1219 | │ └─ children 1220 | │ └─ 0 1221 | │ └─ #/properties/plan/anyOf/1/items 1222 | │ ├─ types 1223 | │ │ └─ 0: integer 1224 | │ └─ primaryType: integer 1225 | ├─ 6 1226 | │ └─ #/properties/permissions 1227 | │ ├─ types 1228 | │ │ ├─ 0: string 1229 | │ │ └─ 1: object 1230 | │ ├─ primaryType: object 1231 | │ └─ children 1232 | │ └─ 0 1233 | │ └─ #/properties/permissions/properties/ids 1234 | │ ├─ types 1235 | │ │ └─ 0: array 1236 | │ ├─ primaryType: array 1237 | │ └─ children 1238 | │ └─ 0 1239 | │ └─ #/properties/permissions/properties/ids/items 1240 | │ ├─ types 1241 | │ │ └─ 0: integer 1242 | │ └─ primaryType: integer 1243 | ├─ 7 1244 | │ └─ #/properties/ref 1245 | │ └─ mirrors: #/properties/permissions 1246 | ├─ 8 1247 | │ └─ #/patternProperties/^id_ 1248 | │ ├─ types 1249 | │ │ └─ 0: number 1250 | │ └─ primaryType: number 1251 | ├─ 9 1252 | │ └─ #/patternProperties/foo 1253 | │ ├─ types 1254 | │ │ └─ 0: integer 1255 | │ └─ primaryType: integer 1256 | └─ 10 1257 | └─ #/patternProperties/_name$ 1258 | ├─ types 1259 | │ └─ 0: string 1260 | └─ primaryType: string 1261 | " 1262 | `; 1263 | 1264 | exports[`SchemaTree output should generate valid tree for formats-schema.json 1`] = ` 1265 | "└─ # 1266 | ├─ types 1267 | │ └─ 0: object 1268 | ├─ primaryType: object 1269 | └─ children 1270 | ├─ 0 1271 | │ └─ #/properties/date-of-birth 1272 | │ ├─ types 1273 | │ │ ├─ 0: number 1274 | │ │ ├─ 1: string 1275 | │ │ └─ 2: array 1276 | │ ├─ primaryType: array 1277 | │ └─ children 1278 | │ └─ 0 1279 | │ └─ #/properties/date-of-birth/items 1280 | ├─ 1 1281 | │ └─ #/properties/name 1282 | │ ├─ types 1283 | │ │ └─ 0: string 1284 | │ └─ primaryType: string 1285 | ├─ 2 1286 | │ └─ #/properties/id 1287 | │ ├─ types 1288 | │ │ └─ 0: number 1289 | │ └─ primaryType: number 1290 | ├─ 3 1291 | │ └─ #/properties/notype 1292 | └─ 4 1293 | └─ #/properties/permissions 1294 | ├─ types 1295 | │ ├─ 0: string 1296 | │ └─ 1: object 1297 | ├─ primaryType: object 1298 | └─ children 1299 | └─ 0 1300 | └─ #/properties/permissions/properties/ids 1301 | ├─ types 1302 | │ └─ 0: array 1303 | ├─ primaryType: array 1304 | └─ children 1305 | └─ 0 1306 | └─ #/properties/permissions/properties/ids/items 1307 | ├─ types 1308 | │ └─ 0: integer 1309 | └─ primaryType: integer 1310 | " 1311 | `; 1312 | 1313 | exports[`SchemaTree output should generate valid tree for objects/additional-empty.json 1`] = ` 1314 | "└─ # 1315 | ├─ types 1316 | │ └─ 0: object 1317 | ├─ primaryType: object 1318 | └─ children 1319 | └─ 0 1320 | └─ #/additionalProperties 1321 | " 1322 | `; 1323 | 1324 | exports[`SchemaTree output should generate valid tree for objects/additional-false.json 1`] = ` 1325 | "└─ # 1326 | ├─ types 1327 | │ └─ 0: object 1328 | ├─ primaryType: object 1329 | └─ children 1330 | └─ 0 1331 | └─ #/additionalProperties 1332 | └─ value: false 1333 | " 1334 | `; 1335 | 1336 | exports[`SchemaTree output should generate valid tree for objects/additional-schema.json 1`] = ` 1337 | "└─ # 1338 | ├─ types 1339 | │ └─ 0: object 1340 | ├─ primaryType: object 1341 | └─ children 1342 | └─ 0 1343 | └─ #/additionalProperties 1344 | ├─ types 1345 | │ └─ 0: object 1346 | ├─ primaryType: object 1347 | └─ children 1348 | └─ 0 1349 | └─ #/additionalProperties/properties/baz 1350 | ├─ types 1351 | │ └─ 0: number 1352 | └─ primaryType: number 1353 | " 1354 | `; 1355 | 1356 | exports[`SchemaTree output should generate valid tree for objects/additional-true.json 1`] = ` 1357 | "└─ # 1358 | ├─ types 1359 | │ └─ 0: object 1360 | ├─ primaryType: object 1361 | └─ children 1362 | └─ 0 1363 | └─ #/additionalProperties 1364 | └─ value: true 1365 | " 1366 | `; 1367 | 1368 | exports[`SchemaTree output should generate valid tree for references/base.json 1`] = ` 1369 | "└─ # 1370 | ├─ types 1371 | │ └─ 0: object 1372 | ├─ primaryType: object 1373 | └─ children 1374 | ├─ 0 1375 | │ └─ #/properties/billing_address 1376 | │ ├─ types 1377 | │ │ └─ 0: object 1378 | │ ├─ primaryType: object 1379 | │ └─ children 1380 | │ ├─ 0 1381 | │ │ └─ #/properties/billing_address/properties/street_address 1382 | │ │ ├─ types 1383 | │ │ │ └─ 0: string 1384 | │ │ └─ primaryType: string 1385 | │ ├─ 1 1386 | │ │ └─ #/properties/billing_address/properties/city 1387 | │ │ ├─ types 1388 | │ │ │ └─ 0: string 1389 | │ │ └─ primaryType: string 1390 | │ └─ 2 1391 | │ └─ #/properties/billing_address/properties/state 1392 | │ ├─ types 1393 | │ │ └─ 0: string 1394 | │ └─ primaryType: string 1395 | └─ 1 1396 | └─ #/properties/shipping_address 1397 | └─ mirrors: #/properties/billing_address 1398 | " 1399 | `; 1400 | 1401 | exports[`SchemaTree output should generate valid tree for references/circular-with-overrides.json 1`] = ` 1402 | "└─ # 1403 | ├─ types 1404 | │ └─ 0: object 1405 | ├─ primaryType: object 1406 | └─ children 1407 | └─ 0 1408 | └─ #/properties/order 1409 | ├─ types 1410 | │ └─ 0: object 1411 | ├─ primaryType: object 1412 | └─ children 1413 | └─ 0 1414 | └─ #/properties/order/properties/member 1415 | ├─ types 1416 | │ └─ 0: object 1417 | ├─ primaryType: object 1418 | └─ children 1419 | └─ 0 1420 | └─ #/properties/order/properties/member/properties/referredMember 1421 | ├─ types 1422 | │ └─ 0: object 1423 | ├─ primaryType: object 1424 | └─ children 1425 | └─ 0 1426 | └─ #/properties/order/properties/member/properties/referredMember/properties/referredMember 1427 | └─ mirrors: #/properties/order/properties/member/properties/referredMember 1428 | " 1429 | `; 1430 | 1431 | exports[`SchemaTree output should generate valid tree for references/nullish.json 1`] = ` 1432 | "└─ # 1433 | ├─ types 1434 | │ └─ 0: object 1435 | ├─ primaryType: object 1436 | └─ children 1437 | └─ 0 1438 | └─ #/properties/empty-ref 1439 | ├─ $ref 1440 | ├─ external: false 1441 | └─ error: $ref is not a string 1442 | " 1443 | `; 1444 | 1445 | exports[`SchemaTree output should generate valid tree for references/with-overrides.json 1`] = ` 1446 | "└─ # 1447 | ├─ combiners 1448 | │ └─ 0: oneOf 1449 | └─ children 1450 | └─ 0 1451 | └─ #/oneOf/0 1452 | ├─ types 1453 | │ └─ 0: object 1454 | ├─ primaryType: object 1455 | └─ children 1456 | └─ 0 1457 | └─ #/oneOf/0/properties/manager 1458 | ├─ types 1459 | │ └─ 0: object 1460 | ├─ primaryType: object 1461 | └─ children 1462 | └─ 0 1463 | └─ #/oneOf/0/properties/manager/properties/manager 1464 | └─ mirrors: #/oneOf/0/properties/manager 1465 | " 1466 | `; 1467 | 1468 | exports[`SchemaTree output should generate valid tree for tickets.schema.json 1`] = ` 1469 | "└─ # 1470 | ├─ types 1471 | │ └─ 0: object 1472 | ├─ primaryType: object 1473 | └─ children 1474 | ├─ 0 1475 | │ └─ #/properties/availableTicketingOptions 1476 | │ ├─ types 1477 | │ │ └─ 0: array 1478 | │ ├─ primaryType: array 1479 | │ └─ children 1480 | │ └─ 0 1481 | │ └─ #/properties/availableTicketingOptions/items 1482 | │ ├─ $ref: ../TicketingOptionInfo/TicketingOptionInfo.v1-0.yaml 1483 | │ ├─ external: true 1484 | │ └─ error: Cannot dereference external references 1485 | ├─ 1 1486 | │ └─ #/properties/commonTicketingOptions 1487 | │ ├─ types 1488 | │ │ └─ 0: array 1489 | │ ├─ primaryType: array 1490 | │ └─ children 1491 | │ └─ 0 1492 | │ └─ #/properties/commonTicketingOptions/items 1493 | │ ├─ types 1494 | │ │ └─ 0: string 1495 | │ └─ primaryType: string 1496 | └─ 2 1497 | └─ #/properties/ticketingOptionChoice 1498 | ├─ types 1499 | │ └─ 0: array 1500 | ├─ primaryType: array 1501 | └─ children 1502 | └─ 0 1503 | └─ #/properties/ticketingOptionChoice/items 1504 | ├─ types 1505 | │ └─ 0: object 1506 | ├─ primaryType: object 1507 | └─ children 1508 | ├─ 0 1509 | │ └─ #/properties/ticketingOptionChoice/items/properties/state 1510 | │ ├─ types 1511 | │ │ └─ 0: string 1512 | │ ├─ primaryType: string 1513 | │ └─ enum 1514 | │ ├─ 0: COMPLETED 1515 | │ └─ 1: ACTIVE 1516 | └─ 1 1517 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown 1518 | ├─ types 1519 | │ └─ 0: array 1520 | ├─ primaryType: array 1521 | └─ children 1522 | └─ 0 1523 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items 1524 | ├─ types 1525 | │ └─ 0: object 1526 | ├─ primaryType: object 1527 | └─ children 1528 | ├─ 0 1529 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/orderItemId 1530 | │ ├─ types 1531 | │ │ └─ 0: string 1532 | │ └─ primaryType: string 1533 | └─ 1 1534 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options 1535 | ├─ types 1536 | │ └─ 0: array 1537 | ├─ primaryType: array 1538 | └─ children 1539 | └─ 0 1540 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items 1541 | ├─ types 1542 | │ └─ 0: object 1543 | ├─ primaryType: object 1544 | └─ children 1545 | ├─ 0 1546 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/title 1547 | │ ├─ types 1548 | │ │ └─ 0: string 1549 | │ ├─ primaryType: string 1550 | │ └─ enum 1551 | │ ├─ 0: HOMEPRINT 1552 | │ ├─ 1: TICKETLESS 1553 | │ ├─ 2: PRINT_AT_KIOSK 1554 | │ └─ 3: SECURE_PAPER 1555 | ├─ 1 1556 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/selected 1557 | │ ├─ types 1558 | │ │ └─ 0: boolean 1559 | │ └─ primaryType: boolean 1560 | ├─ 2 1561 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/additionalRequiredInfo 1562 | │ ├─ types 1563 | │ │ └─ 0: string 1564 | │ └─ primaryType: string 1565 | └─ 3 1566 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo 1567 | ├─ types 1568 | │ └─ 0: object 1569 | ├─ primaryType: object 1570 | └─ children 1571 | ├─ 0 1572 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/availableDeliveryTypes 1573 | │ ├─ types 1574 | │ │ └─ 0: array 1575 | │ ├─ primaryType: array 1576 | │ └─ children 1577 | │ └─ 0 1578 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/availableDeliveryTypes/items 1579 | │ ├─ types 1580 | │ │ └─ 0: string 1581 | │ ├─ primaryType: string 1582 | │ └─ enum 1583 | │ ├─ 0: POSTAL 1584 | │ ├─ 1: PICK_UP_STATION 1585 | │ ├─ 2: E-MAIL 1586 | │ └─ 3: LOYALTY_CARD 1587 | ├─ 1 1588 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/ticketRecipients 1589 | │ ├─ types 1590 | │ │ └─ 0: array 1591 | │ ├─ primaryType: array 1592 | │ └─ children 1593 | │ └─ 0 1594 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/ticketRecipients/items 1595 | │ ├─ types 1596 | │ │ └─ 0: string 1597 | │ ├─ primaryType: string 1598 | │ └─ enum 1599 | │ ├─ 0: BOOKER 1600 | │ ├─ 1: CUSTOMER 1601 | │ ├─ 2: PASSENGER 1602 | │ └─ 3: THIRD_PARTY 1603 | ├─ 2 1604 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/ccEmail 1605 | │ ├─ types 1606 | │ │ └─ 0: string 1607 | │ └─ primaryType: string 1608 | ├─ 3 1609 | │ └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/postalAddress 1610 | │ ├─ $ref: ../Address/Address.v0-1.yaml 1611 | │ ├─ external: true 1612 | │ └─ error: Cannot dereference external references 1613 | └─ 4 1614 | └─ #/properties/ticketingOptionChoice/items/properties/orderItemBreakdown/items/properties/options/items/properties/deliveryInfo/properties/pickUpAtStation 1615 | ├─ types 1616 | │ └─ 0: string 1617 | └─ primaryType: string 1618 | " 1619 | `; 1620 | -------------------------------------------------------------------------------- /src/__tests__/tree.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fastGlob from 'fast-glob'; 2 | import * as fs from 'fs'; 3 | import type { JSONSchema4 } from 'json-schema'; 4 | import * as path from 'path'; 5 | 6 | import { isRegularNode } from '../guards'; 7 | import type { RegularNode } from '../nodes'; 8 | import { SchemaTree } from '../tree'; 9 | import { printTree } from './utils/printTree'; 10 | 11 | describe('SchemaTree', () => { 12 | describe('output', () => { 13 | it.each( 14 | fastGlob.sync('**/*.json', { 15 | cwd: path.join(__dirname, '__fixtures__'), 16 | ignore: ['stress-schema.json', 'recursive-schema.json'], 17 | }), 18 | )('should generate valid tree for %s', async filename => { 19 | const schema = JSON.parse(await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', filename), 'utf8')); 20 | expect(printTree(schema)).toMatchSnapshot(); 21 | }); 22 | 23 | describe('compound keywords', () => { 24 | describe.each(['anyOf', 'oneOf'])('given %s combiner placed next to allOf', combiner => { 25 | let schema: JSONSchema4; 26 | 27 | beforeEach(() => { 28 | schema = { 29 | type: 'object', 30 | title: 'Account', 31 | allOf: [ 32 | { 33 | type: 'object', 34 | properties: { 35 | type: { 36 | type: 'string', 37 | enum: ['admin', 'editor'], 38 | }, 39 | enabled: { 40 | type: 'boolean', 41 | description: 'Is this account enabled', 42 | }, 43 | }, 44 | required: ['type'], 45 | }, 46 | ], 47 | [combiner]: [ 48 | { 49 | type: 'object', 50 | title: 'Admin', 51 | properties: { 52 | root: { 53 | type: 'boolean', 54 | }, 55 | group: { 56 | type: 'string', 57 | }, 58 | expirationDate: { 59 | type: 'string', 60 | }, 61 | }, 62 | }, 63 | { 64 | type: 'object', 65 | title: 'Editor', 66 | properties: { 67 | supervisor: { 68 | type: 'string', 69 | }, 70 | key: { 71 | type: 'string', 72 | }, 73 | }, 74 | }, 75 | ], 76 | }; 77 | }); 78 | 79 | it('given allOf merging disabled, should still merge', () => { 80 | expect(printTree(schema, { mergeAllOf: false })).toMatchSnapshot(); 81 | }); 82 | 83 | it('given allOf merging enabled, should merge contents of allOf combiners', () => { 84 | expect(printTree(schema)).toMatchSnapshot(); 85 | }); 86 | }); 87 | 88 | it('given array with oneOf containing items, should merge it correctly', () => { 89 | const schema: JSONSchema4 = { 90 | oneOf: [ 91 | { 92 | items: { 93 | type: 'string', 94 | }, 95 | }, 96 | { 97 | items: { 98 | type: 'number', 99 | }, 100 | }, 101 | ], 102 | type: 'array', 103 | }; 104 | 105 | expect(printTree(schema)).toMatchInlineSnapshot(` 106 | "└─ # 107 | ├─ combiners 108 | │ └─ 0: oneOf 109 | └─ children 110 | ├─ 0 111 | │ └─ #/oneOf/0 112 | │ ├─ types 113 | │ │ └─ 0: array 114 | │ ├─ primaryType: array 115 | │ └─ children 116 | │ └─ 0 117 | │ └─ #/oneOf/0/items 118 | │ ├─ types 119 | │ │ └─ 0: string 120 | │ └─ primaryType: string 121 | └─ 1 122 | └─ #/oneOf/1 123 | ├─ types 124 | │ └─ 0: array 125 | ├─ primaryType: array 126 | └─ children 127 | └─ 0 128 | └─ #/oneOf/1/items 129 | ├─ types 130 | │ └─ 0: number 131 | └─ primaryType: number 132 | " 133 | `); 134 | }); 135 | 136 | describe('allOf handling', () => { 137 | it('given incompatible values, should bail out and display unmerged allOf', () => { 138 | const schema: JSONSchema4 = { 139 | allOf: [ 140 | { 141 | type: 'string', 142 | }, 143 | { 144 | type: 'number', 145 | }, 146 | { 147 | type: 'object', 148 | properties: { 149 | name: { 150 | type: 'string', 151 | }, 152 | }, 153 | }, 154 | ], 155 | }; 156 | 157 | expect(printTree(schema)).toMatchInlineSnapshot(` 158 | "└─ # 159 | ├─ combiners 160 | │ └─ 0: allOf 161 | └─ children 162 | ├─ 0 163 | │ └─ #/allOf/0 164 | │ ├─ types 165 | │ │ └─ 0: string 166 | │ └─ primaryType: string 167 | ├─ 1 168 | │ └─ #/allOf/1 169 | │ ├─ types 170 | │ │ └─ 0: number 171 | │ └─ primaryType: number 172 | └─ 2 173 | └─ #/allOf/2 174 | ├─ types 175 | │ └─ 0: object 176 | ├─ primaryType: object 177 | └─ children 178 | └─ 0 179 | └─ #/allOf/2/properties/name 180 | ├─ types 181 | │ └─ 0: string 182 | └─ primaryType: string 183 | " 184 | `); 185 | }); 186 | 187 | it('should handle simple circular $refs', () => { 188 | const schema = { 189 | title: 'Sign Request', 190 | type: 'object', 191 | allOf: [ 192 | { 193 | $ref: '#/definitions/SignRequestCreateRequest', 194 | }, 195 | { 196 | properties: { 197 | signers: { 198 | type: 'array', 199 | items: { 200 | $ref: '#/definitions/SignRequestSigner', 201 | }, 202 | }, 203 | }, 204 | }, 205 | ], 206 | definitions: { 207 | SignRequestCreateRequest: { 208 | title: 'Create a sign request', 209 | type: 'object', 210 | required: ['signers', 'source_files', 'parent_folder'], 211 | properties: { 212 | signers: { 213 | type: 'array', 214 | items: { 215 | $ref: '#/definitions/SignRequestCreateSigner', 216 | }, 217 | }, 218 | }, 219 | }, 220 | SignRequestCreateSigner: { 221 | title: 'Signer fields for Create Sign Request', 222 | type: 'object', 223 | required: ['email'], 224 | properties: { 225 | email: { 226 | type: 'string', 227 | }, 228 | }, 229 | }, 230 | SignRequestSigner: { 231 | title: 'Signer fields for GET Sign Request response', 232 | type: 'object', 233 | required: ['email'], 234 | allOf: [ 235 | { 236 | $ref: '#/definitions/SignRequestCreateSigner', 237 | }, 238 | { 239 | properties: { 240 | has_viewed_document: { 241 | type: 'boolean', 242 | }, 243 | }, 244 | }, 245 | ], 246 | }, 247 | }, 248 | }; 249 | 250 | expect(printTree(schema)).toMatchInlineSnapshot(` 251 | "└─ # 252 | ├─ types 253 | │ └─ 0: object 254 | ├─ primaryType: object 255 | └─ children 256 | └─ 0 257 | └─ #/properties/signers 258 | ├─ types 259 | │ └─ 0: array 260 | ├─ primaryType: array 261 | └─ children 262 | └─ 0 263 | └─ #/properties/signers/items 264 | ├─ types 265 | │ └─ 0: object 266 | ├─ primaryType: object 267 | └─ children 268 | ├─ 0 269 | │ └─ #/properties/signers/items/properties/email 270 | │ ├─ types 271 | │ │ └─ 0: string 272 | │ └─ primaryType: string 273 | └─ 1 274 | └─ #/properties/signers/items/properties/has_viewed_document 275 | ├─ types 276 | │ └─ 0: boolean 277 | └─ primaryType: boolean 278 | " 279 | `); 280 | }); 281 | 282 | it('given allOf having a $ref pointing at another allOf, should merge it', () => { 283 | const schema: JSONSchema4 = { 284 | type: 'object', 285 | allOf: [ 286 | { 287 | $ref: '#/definitions/Item', 288 | }, 289 | { 290 | properties: { 291 | summary: { 292 | type: 'string', 293 | }, 294 | }, 295 | }, 296 | ], 297 | definitions: { 298 | Item: { 299 | allOf: [ 300 | { 301 | properties: { 302 | id: { 303 | type: 'string', 304 | }, 305 | }, 306 | }, 307 | { 308 | properties: { 309 | description: { 310 | type: 'string', 311 | }, 312 | }, 313 | }, 314 | ], 315 | }, 316 | }, 317 | }; 318 | 319 | const tree = new SchemaTree(schema); 320 | tree.populate(); 321 | 322 | expect(printTree(schema)).toMatchInlineSnapshot(` 323 | "└─ # 324 | ├─ types 325 | │ └─ 0: object 326 | ├─ primaryType: object 327 | └─ children 328 | ├─ 0 329 | │ └─ #/properties/summary 330 | │ ├─ types 331 | │ │ └─ 0: string 332 | │ └─ primaryType: string 333 | ├─ 1 334 | │ └─ #/properties/id 335 | │ ├─ types 336 | │ │ └─ 0: string 337 | │ └─ primaryType: string 338 | └─ 2 339 | └─ #/properties/description 340 | ├─ types 341 | │ └─ 0: string 342 | └─ primaryType: string 343 | " 344 | `); 345 | }); 346 | }); 347 | }); 348 | 349 | describe('eager $ref resolving', () => { 350 | it('given a plain object with properties, should resolve', () => { 351 | const schema: JSONSchema4 = { 352 | type: 'object', 353 | properties: { 354 | foo: { 355 | $ref: '#/properties/bar', 356 | }, 357 | bar: { 358 | type: 'boolean', 359 | }, 360 | }, 361 | }; 362 | 363 | expect(printTree(schema)).toMatchInlineSnapshot(` 364 | "└─ # 365 | ├─ types 366 | │ └─ 0: object 367 | ├─ primaryType: object 368 | └─ children 369 | ├─ 0 370 | │ └─ #/properties/foo 371 | │ ├─ types 372 | │ │ └─ 0: boolean 373 | │ └─ primaryType: boolean 374 | └─ 1 375 | └─ #/properties/bar 376 | └─ mirrors: #/properties/foo 377 | " 378 | `); 379 | }); 380 | 381 | it('preserves the original $ref info', () => { 382 | const schema: JSONSchema4 = { 383 | type: 'object', 384 | properties: { 385 | foo: { 386 | $ref: '#/properties/bar', 387 | }, 388 | bar: { 389 | type: 'boolean', 390 | }, 391 | }, 392 | }; 393 | 394 | const tree = new SchemaTree(schema); 395 | tree.populate(); 396 | 397 | const topLevelObject = tree.root.children[0] as RegularNode; 398 | const fooObj = topLevelObject.children!.find(child => child.path[child.path.length - 1] === 'foo')!; 399 | expect(isRegularNode(fooObj) && fooObj.originalFragment.$ref).toBe('#/properties/bar'); 400 | }); 401 | 402 | it('given an array with $reffed items, should resolve', () => { 403 | const schema: JSONSchema4 = { 404 | type: 'object', 405 | properties: { 406 | foo: { 407 | type: 'array', 408 | items: { 409 | $ref: '#/properties/bar', 410 | }, 411 | }, 412 | bar: { 413 | type: 'boolean', 414 | }, 415 | }, 416 | }; 417 | 418 | expect(printTree(schema)).toMatchInlineSnapshot(` 419 | "└─ # 420 | ├─ types 421 | │ └─ 0: object 422 | ├─ primaryType: object 423 | └─ children 424 | ├─ 0 425 | │ └─ #/properties/foo 426 | │ ├─ types 427 | │ │ └─ 0: array 428 | │ ├─ primaryType: array 429 | │ └─ children 430 | │ └─ 0 431 | │ └─ #/properties/foo/items 432 | │ ├─ types 433 | │ │ └─ 0: boolean 434 | │ └─ primaryType: boolean 435 | └─ 1 436 | └─ #/properties/bar 437 | └─ mirrors: #/properties/foo/items 438 | " 439 | `); 440 | }); 441 | 442 | it('should leave broken $refs', () => { 443 | const schema: JSONSchema4 = { 444 | type: 'object', 445 | properties: { 446 | foo: { 447 | type: 'array', 448 | items: { 449 | $ref: '#/properties/baz', 450 | }, 451 | }, 452 | bar: { 453 | $ref: '#/properties/bazinga', 454 | }, 455 | }, 456 | }; 457 | 458 | expect(printTree(schema)).toMatchInlineSnapshot(` 459 | "└─ # 460 | ├─ types 461 | │ └─ 0: object 462 | ├─ primaryType: object 463 | └─ children 464 | ├─ 0 465 | │ └─ #/properties/foo 466 | │ ├─ types 467 | │ │ └─ 0: array 468 | │ ├─ primaryType: array 469 | │ └─ children 470 | │ └─ 0 471 | │ └─ #/properties/foo/items 472 | │ ├─ $ref: #/properties/baz 473 | │ ├─ external: false 474 | │ └─ error: Could not resolve '#/properties/baz' 475 | └─ 1 476 | └─ #/properties/bar 477 | ├─ $ref: #/properties/bazinga 478 | ├─ external: false 479 | └─ error: Could not resolve '#/properties/bazinga' 480 | " 481 | `); 482 | }); 483 | 484 | it('should handle circular references', () => { 485 | const schema: JSONSchema4 = { 486 | type: 'object', 487 | properties: { 488 | foo: { 489 | type: 'array', 490 | items: { 491 | type: 'object', 492 | properties: { 493 | user: { 494 | $ref: '#/properties/bar', 495 | }, 496 | }, 497 | }, 498 | }, 499 | bar: { 500 | $ref: '#/properties/baz', 501 | }, 502 | baz: { 503 | $ref: '#/properties/foo', 504 | }, 505 | }, 506 | }; 507 | 508 | expect(printTree(schema)).toMatchInlineSnapshot(` 509 | "└─ # 510 | ├─ types 511 | │ └─ 0: object 512 | ├─ primaryType: object 513 | └─ children 514 | ├─ 0 515 | │ └─ #/properties/foo 516 | │ ├─ types 517 | │ │ └─ 0: array 518 | │ ├─ primaryType: array 519 | │ └─ children 520 | │ └─ 0 521 | │ └─ #/properties/foo/items 522 | │ ├─ types 523 | │ │ └─ 0: object 524 | │ ├─ primaryType: object 525 | │ └─ children 526 | │ └─ 0 527 | │ └─ #/properties/foo/items/properties/user 528 | │ └─ mirrors: #/properties/foo 529 | ├─ 1 530 | │ └─ #/properties/bar 531 | │ └─ mirrors: #/properties/foo 532 | └─ 2 533 | └─ #/properties/baz 534 | └─ mirrors: #/properties/foo 535 | " 536 | `); 537 | }); 538 | 539 | it('should handle circular references pointing at parents', () => { 540 | const schema: JSONSchema4 = { 541 | properties: { 542 | bar: { 543 | properties: { 544 | foo: { 545 | type: 'string', 546 | }, 547 | baz: { 548 | $ref: '#/properties/bar', 549 | }, 550 | }, 551 | }, 552 | }, 553 | }; 554 | 555 | expect(printTree(schema)).toMatchInlineSnapshot(` 556 | "└─ # 557 | ├─ types 558 | │ └─ 0: object 559 | ├─ primaryType: object 560 | └─ children 561 | └─ 0 562 | └─ #/properties/bar 563 | ├─ types 564 | │ └─ 0: object 565 | ├─ primaryType: object 566 | └─ children 567 | ├─ 0 568 | │ └─ #/properties/bar/properties/foo 569 | │ ├─ types 570 | │ │ └─ 0: string 571 | │ └─ primaryType: string 572 | └─ 1 573 | └─ #/properties/bar/properties/baz 574 | └─ mirrors: #/properties/bar 575 | " 576 | `); 577 | }); 578 | 579 | it('should handle circular references pointing at document', () => { 580 | const schema: JSONSchema4 = { 581 | title: 'root', 582 | properties: { 583 | bar: { 584 | properties: { 585 | baz: { 586 | $ref: '#', 587 | }, 588 | }, 589 | }, 590 | }, 591 | }; 592 | 593 | expect(printTree(schema)).toMatchInlineSnapshot(` 594 | "└─ # 595 | ├─ types 596 | │ └─ 0: object 597 | ├─ primaryType: object 598 | └─ children 599 | └─ 0 600 | └─ #/properties/bar 601 | ├─ types 602 | │ └─ 0: object 603 | ├─ primaryType: object 604 | └─ children 605 | └─ 0 606 | └─ #/properties/bar/properties/baz 607 | └─ mirrors: # 608 | " 609 | `); 610 | }); 611 | 612 | // it('should handle resolving errors', () => { 613 | // const schema: JSONSchema4 = { 614 | // type: 'object', 615 | // properties: { 616 | // foo: { 617 | // type: 'string', 618 | // }, 619 | // bar: { 620 | // $ref: 'http://localhost:8080/some/not/existing/path', 621 | // }, 622 | // }, 623 | // }; 624 | // 625 | // const tree = new SchemaTree(schema, new SchemaTreeState(), { 626 | // expandedDepth: Infinity, 627 | // mergeAllOf: true, 628 | // resolveRef: () => { 629 | // throw new Error('resolving error'); 630 | // }, 631 | // shouldResolveEagerly: true, 632 | // onPopulate: void 0, 633 | // }); 634 | // 635 | // tree.populate(); 636 | // 637 | // expect(tree.count).toEqual(4); 638 | // expect(getNodeMetadata(tree.itemAt(3)!)).toHaveProperty('error', 'resolving error'); 639 | // }); 640 | }); 641 | 642 | it('given multiple object and string type, should process properties', () => { 643 | const schema: JSONSchema4 = { 644 | type: ['string', 'object'], 645 | properties: { 646 | ids: { 647 | type: 'array', 648 | items: { 649 | type: 'integer', 650 | }, 651 | }, 652 | }, 653 | }; 654 | 655 | expect(printTree(schema)).toMatchInlineSnapshot(` 656 | "└─ # 657 | ├─ types 658 | │ ├─ 0: string 659 | │ └─ 1: object 660 | ├─ primaryType: object 661 | └─ children 662 | └─ 0 663 | └─ #/properties/ids 664 | ├─ types 665 | │ └─ 0: array 666 | ├─ primaryType: array 667 | └─ children 668 | └─ 0 669 | └─ #/properties/ids/items 670 | ├─ types 671 | │ └─ 0: integer 672 | └─ primaryType: integer 673 | " 674 | `); 675 | }); 676 | 677 | it('given empty schema, should output empty tree', () => { 678 | expect(printTree({})).toEqual(''); 679 | }); 680 | 681 | it('should override description', () => { 682 | const schema = { 683 | type: 'object', 684 | properties: { 685 | caves: { 686 | type: 'array', 687 | items: { 688 | summary: 'Bear cave', 689 | $ref: '#/$defs/Cave', 690 | description: 'Apparently Tom likes bears', 691 | }, 692 | }, 693 | greatestBear: { 694 | $ref: '#/$defs/Bear', 695 | description: 'The greatest bear!', 696 | }, 697 | bestBear: { 698 | $ref: '#/$defs/Bear', 699 | summary: 'The best bear!', 700 | }, 701 | }, 702 | $defs: { 703 | Bear: { 704 | type: 'string', 705 | summary: "Tom's favorite bear", 706 | }, 707 | Cave: { 708 | type: 'string', 709 | summary: 'A cave', 710 | description: '_Everyone_ ~hates~ loves caves', 711 | }, 712 | }, 713 | }; 714 | 715 | const tree = new SchemaTree(schema, {}); 716 | tree.populate(); 717 | 718 | expect(tree.root).toEqual( 719 | expect.objectContaining({ 720 | children: [ 721 | expect.objectContaining({ 722 | primaryType: 'object', 723 | types: ['object'], 724 | children: [ 725 | expect.objectContaining({ 726 | primaryType: 'array', 727 | subpath: ['properties', 'caves'], 728 | types: ['array'], 729 | children: [ 730 | expect.objectContaining({ 731 | primaryType: 'string', 732 | types: ['string'], 733 | subpath: ['items'], 734 | annotations: { 735 | description: 'Apparently Tom likes bears', 736 | }, 737 | }), 738 | ], 739 | }), 740 | expect.objectContaining({ 741 | primaryType: 'string', 742 | types: ['string'], 743 | subpath: ['properties', 'greatestBear'], 744 | annotations: { 745 | description: 'The greatest bear!', 746 | }, 747 | }), 748 | expect.objectContaining({ 749 | primaryType: 'string', 750 | types: ['string'], 751 | subpath: ['properties', 'bestBear'], 752 | annotations: {}, 753 | }), 754 | ], 755 | }), 756 | ], 757 | }), 758 | ); 759 | }); 760 | 761 | it('node of type array should adopt description of referenced node', () => { 762 | const schema = { 763 | definitions: { 764 | Cave: { 765 | type: 'string', 766 | summary: 'A cave', 767 | description: '_Everyone_ ~hates~ loves caves', 768 | }, 769 | }, 770 | type: 'object', 771 | properties: { 772 | caves: { 773 | type: 'array', 774 | items: { 775 | $ref: '#/definitions/Cave', 776 | }, 777 | }, 778 | }, 779 | }; 780 | 781 | const tree = new SchemaTree(schema); 782 | tree.populate(); 783 | 784 | expect( 785 | // @ts-ignore 786 | tree.root.children[0].children[0].annotations.description, 787 | ).toEqual('_Everyone_ ~hates~ loves caves'); 788 | }); 789 | 790 | it('should not override description reference siblings', () => { 791 | const schema = { 792 | $schema: 'http://json-schema.org/draft-07/schema#', 793 | type: 'object', 794 | properties: { 795 | AAAAA: { 796 | allOf: [{ description: 'AAAAA', type: 'string' }, { examples: ['AAAAA'] }], 797 | }, 798 | BBBBB: { 799 | allOf: [ 800 | { 801 | $ref: '#/properties/AAAAA/allOf/0', 802 | description: 'BBBBB', 803 | }, 804 | { examples: ['BBBBB'] }, 805 | ], 806 | }, 807 | }, 808 | }; 809 | 810 | const tree = new SchemaTree(schema, {}); 811 | tree.populate(); 812 | 813 | expect(tree.root).toEqual( 814 | expect.objectContaining({ 815 | children: [ 816 | expect.objectContaining({ 817 | primaryType: 'object', 818 | types: ['object'], 819 | children: [ 820 | expect.objectContaining({ 821 | primaryType: 'string', 822 | subpath: ['properties', 'AAAAA'], 823 | types: ['string'], 824 | annotations: { 825 | description: 'AAAAA', 826 | examples: ['AAAAA'], 827 | }, 828 | }), 829 | expect.objectContaining({ 830 | primaryType: 'string', 831 | subpath: ['properties', 'BBBBB'], 832 | types: ['string'], 833 | annotations: { 834 | description: 'BBBBB', 835 | examples: ['BBBBB'], 836 | }, 837 | }), 838 | ], 839 | }), 840 | ], 841 | }), 842 | ); 843 | }); 844 | 845 | it('node of type array should keep its own description even when referenced node has a description', () => { 846 | const schema = { 847 | definitions: { 848 | Cave: { 849 | type: 'string', 850 | summary: 'A cave', 851 | description: '_Everyone_ ~hates~ loves caves', 852 | }, 853 | }, 854 | type: 'object', 855 | properties: { 856 | caves: { 857 | type: 'array', 858 | description: 'I have my own description', 859 | items: { 860 | $ref: '#/definitions/Cave', 861 | }, 862 | }, 863 | }, 864 | }; 865 | 866 | const tree = new SchemaTree(schema); 867 | tree.populate(); 868 | 869 | expect( 870 | // @ts-ignore 871 | tree.root.children[0].children[0].annotations.description, 872 | ).toEqual('I have my own description'); 873 | }); 874 | 875 | it('referenced node description should appear for all properties with that ref', () => { 876 | const schema = { 877 | definitions: { 878 | Cave: { 879 | type: 'string', 880 | summary: 'A cave', 881 | description: '_Everyone_ ~hates~ loves caves', 882 | }, 883 | }, 884 | type: 'object', 885 | properties: { 886 | caves: { 887 | type: 'array', 888 | items: { 889 | $ref: '#/definitions/Cave', 890 | }, 891 | }, 892 | bear: { 893 | $ref: '#/definitions/Cave', 894 | }, 895 | }, 896 | }; 897 | 898 | const tree = new SchemaTree(schema); 899 | tree.populate(); 900 | 901 | expect( 902 | // @ts-ignore 903 | tree.root.children[0].children[0].annotations.description, 904 | ).toEqual('_Everyone_ ~hates~ loves caves'); 905 | expect( 906 | // @ts-ignore 907 | tree.root.children[0].children[1].annotations.description, 908 | ).toEqual('_Everyone_ ~hates~ loves caves'); 909 | }); 910 | 911 | it('should render true/false schemas', () => { 912 | const schema = { 913 | type: 'object', 914 | properties: { 915 | bear: true, 916 | cave: false, 917 | }, 918 | }; 919 | 920 | const tree = new SchemaTree(schema); 921 | tree.populate(); 922 | 923 | expect(printTree(schema)).toMatchInlineSnapshot(` 924 | "└─ # 925 | ├─ types 926 | │ └─ 0: object 927 | ├─ primaryType: object 928 | └─ children 929 | ├─ 0 930 | │ └─ #/properties/bear 931 | │ └─ value: true 932 | └─ 1 933 | └─ #/properties/cave 934 | └─ value: false 935 | " 936 | `); 937 | }); 938 | }); 939 | 940 | describe('position', () => { 941 | let schema: JSONSchema4; 942 | 943 | beforeEach(() => { 944 | schema = { 945 | type: ['string', 'object'], 946 | properties: { 947 | ids: { 948 | type: 'array', 949 | items: { 950 | type: 'integer', 951 | }, 952 | }, 953 | tag: { 954 | type: 'string', 955 | }, 956 | uuid: { 957 | type: 'string', 958 | }, 959 | }, 960 | }; 961 | }); 962 | 963 | it('given node being the only child, should have correct position info', () => { 964 | const tree = new SchemaTree(schema); 965 | tree.populate(); 966 | 967 | const node = tree.root.children[0]; 968 | 969 | expect(node.isFirst).toBe(true); 970 | expect(node.isLast).toBe(true); 971 | expect(node.pos).toEqual(0); 972 | }); 973 | 974 | it('given node being the first child among other children, should have correct position info', () => { 975 | const tree = new SchemaTree(schema); 976 | tree.populate(); 977 | 978 | const node = (tree.root.children[0] as RegularNode).children![0]; 979 | 980 | expect(node.isFirst).toBe(true); 981 | expect(node.isLast).toBe(false); 982 | expect(node.pos).toEqual(0); 983 | }); 984 | 985 | it('given node being the last child among other children, should have correct position info', () => { 986 | const tree = new SchemaTree(schema); 987 | tree.populate(); 988 | 989 | const node = (tree.root.children[0] as RegularNode).children![2]; 990 | 991 | expect(node.isFirst).toBe(false); 992 | expect(node.isLast).toBe(true); 993 | expect(node.pos).toEqual(2); 994 | }); 995 | 996 | it('given node not being the first nor the child among other children, should have correct position info', () => { 997 | const tree = new SchemaTree(schema); 998 | tree.populate(); 999 | 1000 | const node = (tree.root.children[0] as RegularNode).children![1]; 1001 | 1002 | expect(node.isFirst).toBe(false); 1003 | expect(node.isLast).toBe(false); 1004 | expect(node.pos).toEqual(1); 1005 | }); 1006 | }); 1007 | 1008 | describe('mirroring', () => { 1009 | describe('circular references', () => { 1010 | it('should self expand', () => { 1011 | const schema: JSONSchema4 = { 1012 | type: 'object', 1013 | properties: { 1014 | foo: { 1015 | type: 'array', 1016 | items: { 1017 | type: 'object', 1018 | properties: { 1019 | user: { 1020 | $ref: '#/properties/bar', 1021 | }, 1022 | }, 1023 | }, 1024 | }, 1025 | bar: { 1026 | $ref: '#/properties/baz', 1027 | }, 1028 | baz: { 1029 | $ref: '#/properties/foo', 1030 | }, 1031 | }, 1032 | }; 1033 | 1034 | const tree = new SchemaTree(schema); 1035 | tree.populate(); 1036 | 1037 | expect( 1038 | // @ts-ignore 1039 | tree.root.children[0].children[2].children[0].children[0].children[0].parent === 1040 | // @ts-ignore 1041 | tree.root.children[0].children[2].children[0].children[0], 1042 | ).toBe(true); 1043 | 1044 | // @ts-ignore 1045 | expect(tree.root.children[0].children[0].children[0].children[0].children[0].children[0].path).toEqual([ 1046 | 'properties', 1047 | 'foo', 1048 | 'items', 1049 | 'properties', 1050 | 'user', 1051 | 'items', 1052 | 'properties', 1053 | 'user', 1054 | ]); 1055 | // @ts-ignore 1056 | expect(tree.root.children[0].children[2].children[0].children[0].children[0].children[0].path).toEqual([ 1057 | 'properties', 1058 | 'baz', 1059 | 'items', 1060 | 'properties', 1061 | 'user', 1062 | 'items', 1063 | 'properties', 1064 | 'user', 1065 | ]); 1066 | 1067 | // todo: add some more assertions here 1068 | }); 1069 | }); 1070 | }); 1071 | 1072 | describe('recursive walking', () => { 1073 | it('should load with a max depth', async () => { 1074 | const schema = JSON.parse( 1075 | await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', 'recursive-schema.json'), 'utf8'), 1076 | ); 1077 | 1078 | const w = new SchemaTree(schema, { 1079 | maxRefDepth: 1000, 1080 | }); 1081 | w.populate(); 1082 | }); 1083 | }); 1084 | }); 1085 | -------------------------------------------------------------------------------- /src/__tests__/utils/printTree.ts: -------------------------------------------------------------------------------- 1 | import { pathToPointer } from '@stoplight/json'; 2 | import type { Dictionary } from '@stoplight/types'; 3 | import * as treeify from 'treeify'; 4 | 5 | import { isBooleanishNode, isMirroredNode, isReferenceNode, isRegularNode, isRootNode } from '../../guards'; 6 | import type { MirroredSchemaNode, ReferenceNode, RegularNode, SchemaNode } from '../../nodes'; 7 | import type { BooleanishNode } from '../../nodes/BooleanishNode'; 8 | import type { SchemaTreeOptions } from '../../tree'; 9 | import { SchemaTree } from '../../tree'; 10 | import type { SchemaFragment } from '../../types'; 11 | import { isNonNullable } from '../../utils'; 12 | 13 | export function printTree(schema: SchemaFragment, opts?: Partial) { 14 | const tree = new SchemaTree(schema, opts); 15 | tree.populate(); 16 | 17 | const root: unknown = 18 | tree.root.children.length > 1 19 | ? tree.root.children.map(child => prepareTree(child)) 20 | : tree.root.children.length === 1 21 | ? prepareTree(tree.root.children[0]) 22 | : {}; 23 | 24 | return treeify.asTree(root as treeify.TreeObject, true, true); 25 | } 26 | 27 | function printRegularNode(node: RegularNode): Dictionary { 28 | return { 29 | ...(node.types !== null ? { types: node.types } : null), 30 | ...(node.primaryType !== null ? { primaryType: node.primaryType } : null), 31 | ...(node.combiners !== null ? { combiners: node.combiners } : null), 32 | ...(node.enum !== null ? { enum: node.enum } : null), 33 | ...(isNonNullable(node.children) ? { children: node.children.map(prepareTree) } : null), 34 | }; 35 | } 36 | 37 | function printReferenceNode(node: ReferenceNode) { 38 | return { 39 | $ref: node.value, 40 | external: node.external, 41 | ...(node.error !== null ? { error: node.error } : null), 42 | }; 43 | } 44 | 45 | function printBooleanishNode(node: BooleanishNode) { 46 | return { 47 | value: node.fragment, 48 | }; 49 | } 50 | 51 | function printMirrorNode(node: MirroredSchemaNode): any { 52 | return { 53 | mirrors: pathToPointer(node.mirroredNode.path as string[]), 54 | }; 55 | } 56 | 57 | function printNode(node: SchemaNode) { 58 | if (isMirroredNode(node)) { 59 | return printMirrorNode(node); 60 | } else if (isRegularNode(node)) { 61 | return printRegularNode(node); 62 | } else if (isReferenceNode(node)) { 63 | return printReferenceNode(node); 64 | } else if (isBooleanishNode(node)) { 65 | return printBooleanishNode(node); 66 | } else if (isRootNode(node)) { 67 | return {}; 68 | } 69 | } 70 | 71 | function prepareTree(node: SchemaNode) { 72 | return { 73 | [pathToPointer(node.path as string[])]: printNode(node), 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/accessors/__tests__/getAnnotations.spec.ts: -------------------------------------------------------------------------------- 1 | import { getAnnotations } from '../getAnnotations'; 2 | 3 | describe('getAnnotations util', () => { 4 | it('should treat example as examples', () => { 5 | expect( 6 | getAnnotations({ 7 | example: 'foo', 8 | }), 9 | ).toStrictEqual({ 10 | examples: ['foo'], 11 | }); 12 | }); 13 | 14 | it('should prefer examples over example', () => { 15 | expect( 16 | getAnnotations({ 17 | examples: ['bar', 'baz'], 18 | example: 'foo', 19 | }), 20 | ).toStrictEqual({ 21 | examples: ['bar', 'baz'], 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/accessors/__tests__/getValidations.spec.ts: -------------------------------------------------------------------------------- 1 | import { SchemaNodeKind } from '../../nodes/types'; 2 | import { getValidations } from '../getValidations'; 3 | 4 | describe('getValidations util', () => { 5 | it('should support integer type', () => { 6 | expect( 7 | getValidations( 8 | { 9 | minimum: 2, 10 | exclusiveMaximum: true, 11 | maximum: 20, 12 | multipleOf: 2, 13 | }, 14 | [SchemaNodeKind.Integer], 15 | ), 16 | ).toStrictEqual({ 17 | exclusiveMaximum: true, 18 | maximum: 20, 19 | minimum: 2, 20 | multipleOf: 2, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/accessors/getAnnotations.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaFragment } from '../types'; 2 | import { pick } from '../utils/pick'; 3 | 4 | const ANNOTATIONS = ['description', 'default', 'examples'] as const; 5 | 6 | export type SchemaAnnotations = typeof ANNOTATIONS[number]; 7 | 8 | export function getAnnotations(fragment: SchemaFragment) { 9 | const annotations = pick(fragment, ANNOTATIONS); 10 | if ('example' in fragment && !Array.isArray(annotations.examples)) { 11 | // example is more OAS-ish, but it's common enough to be worth supporting 12 | annotations.examples = [fragment.example]; 13 | } 14 | 15 | return annotations; 16 | } 17 | -------------------------------------------------------------------------------- /src/accessors/getCombiners.ts: -------------------------------------------------------------------------------- 1 | import { SchemaCombinerName } from '../nodes/types'; 2 | import type { SchemaFragment } from '../types'; 3 | 4 | export function getCombiners(fragment: SchemaFragment): SchemaCombinerName[] | null { 5 | let combiners: SchemaCombinerName[] | null = null; 6 | 7 | if (SchemaCombinerName.AnyOf in fragment) { 8 | combiners ??= []; 9 | combiners.push(SchemaCombinerName.AnyOf); 10 | } 11 | 12 | if (SchemaCombinerName.OneOf in fragment) { 13 | combiners ??= []; 14 | combiners.push(SchemaCombinerName.OneOf); 15 | } 16 | 17 | if (SchemaCombinerName.AllOf in fragment) { 18 | combiners ??= []; 19 | combiners.push(SchemaCombinerName.AllOf); 20 | } 21 | 22 | return combiners; 23 | } 24 | -------------------------------------------------------------------------------- /src/accessors/getPrimaryType.ts: -------------------------------------------------------------------------------- 1 | import { SchemaNodeKind } from '../nodes/types'; 2 | import type { SchemaFragment } from '../types'; 3 | 4 | export function getPrimaryType(fragment: SchemaFragment, types: SchemaNodeKind[] | null) { 5 | if (types !== null) { 6 | if (types.includes(SchemaNodeKind.Object)) { 7 | return SchemaNodeKind.Object; 8 | } 9 | 10 | if (types.includes(SchemaNodeKind.Array)) { 11 | return SchemaNodeKind.Array; 12 | } 13 | 14 | if (types.length > 0) { 15 | return types[0]; 16 | } 17 | 18 | return null; 19 | } 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /src/accessors/getRequired.ts: -------------------------------------------------------------------------------- 1 | import { isStringOrNumber } from '../utils/guards'; 2 | 3 | export function getRequired(required: unknown): string[] | null { 4 | if (!Array.isArray(required)) return null; 5 | return required.filter(isStringOrNumber).map(String); 6 | } 7 | -------------------------------------------------------------------------------- /src/accessors/getTypes.ts: -------------------------------------------------------------------------------- 1 | import { SchemaNodeKind } from '../nodes/types'; 2 | import type { SchemaFragment } from '../types'; 3 | import { isValidType } from './guards/isValidType'; 4 | import { inferType } from './inferType'; 5 | 6 | export function getTypes(fragment: SchemaFragment): SchemaNodeKind[] | null { 7 | const types: SchemaNodeKind[] = []; 8 | let isNullable = false; 9 | 10 | if ('nullable' in fragment) { 11 | if (fragment.nullable === true) { 12 | isNullable = true; 13 | } 14 | } 15 | if ('type' in fragment) { 16 | if (Array.isArray(fragment.type)) { 17 | types.push(...fragment.type.filter(isValidType)); 18 | } else if (isValidType(fragment.type)) { 19 | types.push(fragment.type); 20 | } 21 | if (isNullable && !types.includes(SchemaNodeKind.Null)) { 22 | types.push(SchemaNodeKind.Null); 23 | } 24 | return types; 25 | } 26 | 27 | const inferredType = inferType(fragment); 28 | if (inferredType !== null) { 29 | types.push(inferredType); 30 | if (isNullable && !types.includes(SchemaNodeKind.Null)) { 31 | types.push(SchemaNodeKind.Null); 32 | } 33 | return types; 34 | } 35 | 36 | return null; 37 | } 38 | -------------------------------------------------------------------------------- /src/accessors/getValidations.ts: -------------------------------------------------------------------------------- 1 | import type { Dictionary } from '@stoplight/types'; 2 | 3 | import type { SchemaNodeKind } from '../nodes/types'; 4 | import type { SchemaFragment } from '../types'; 5 | import { pick } from '../utils/pick'; 6 | 7 | export const COMMON_VALIDATION_TYPES: string[] = ['readOnly', 'writeOnly', 'style']; 8 | 9 | const VALIDATION_TYPES: Partial> = { 10 | string: ['minLength', 'maxLength', 'pattern'], 11 | number: ['multipleOf', 'minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum'], 12 | get integer() { 13 | return this.number; 14 | }, 15 | object: ['minProperties', 'maxProperties'], 16 | array: ['minItems', 'maxItems', 'uniqueItems'], 17 | }; 18 | 19 | function getTypeValidations(types: SchemaNodeKind[]): (keyof SchemaFragment)[] | null { 20 | let extraValidations: (keyof SchemaFragment)[] | null = null; 21 | 22 | for (const type of types) { 23 | const value = VALIDATION_TYPES[type]; 24 | if (value !== void 0) { 25 | extraValidations ??= []; 26 | extraValidations.push(...value); 27 | } 28 | } 29 | 30 | return extraValidations; 31 | } 32 | 33 | export function getValidations(fragment: SchemaFragment, types: SchemaNodeKind[] | null): Dictionary { 34 | const extraValidations = types === null ? null : getTypeValidations(types); 35 | 36 | return { 37 | ...pick(fragment, COMMON_VALIDATION_TYPES), 38 | ...(extraValidations !== null ? pick(fragment, extraValidations) : null), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/accessors/guards/isValidType.ts: -------------------------------------------------------------------------------- 1 | import { SchemaNodeKind } from '../../nodes/types'; 2 | 3 | const VALID_TYPES = Object.values(SchemaNodeKind); 4 | 5 | export const isValidType = (maybeType: unknown): maybeType is SchemaNodeKind => 6 | typeof maybeType === 'string' && VALID_TYPES.includes(maybeType as SchemaNodeKind); 7 | -------------------------------------------------------------------------------- /src/accessors/inferType.ts: -------------------------------------------------------------------------------- 1 | import { SchemaNodeKind } from '../nodes/types'; 2 | import type { SchemaFragment } from '../types'; 3 | 4 | export function inferType(fragment: SchemaFragment): SchemaNodeKind | null { 5 | if ('properties' in fragment || 'additionalProperties' in fragment || 'patternProperties' in fragment) { 6 | return SchemaNodeKind.Object; 7 | } 8 | 9 | if ('items' in fragment || 'additionalItems' in fragment) { 10 | return SchemaNodeKind.Array; 11 | } 12 | 13 | return null; 14 | } 15 | -------------------------------------------------------------------------------- /src/accessors/isDeprecated.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaFragment } from '../types'; 2 | 3 | export function isDeprecated(fragment: SchemaFragment): boolean { 4 | if ('x-deprecated' in fragment) { 5 | return fragment['x-deprecated'] === true; 6 | } 7 | 8 | if ('deprecated' in fragment) { 9 | return fragment.deprecated === true; 10 | } 11 | 12 | return false; 13 | } 14 | -------------------------------------------------------------------------------- /src/accessors/unwrap.ts: -------------------------------------------------------------------------------- 1 | export function unwrapStringOrNull(value: unknown): string | null { 2 | return typeof value === 'string' ? value : null; 3 | } 4 | 5 | export function unwrapArrayOrNull(value: unknown): unknown[] | null { 6 | return Array.isArray(value) ? value : null; 7 | } 8 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ResolvingError extends ReferenceError { 2 | public readonly name = 'ResolvingError'; 3 | } 4 | 5 | export class MergingError extends Error { 6 | public readonly name = 'MergingError'; 7 | } 8 | -------------------------------------------------------------------------------- /src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nodes'; 2 | -------------------------------------------------------------------------------- /src/guards/nodes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MirroredReferenceNode, 3 | MirroredRegularNode, 4 | MirroredSchemaNode, 5 | ReferenceNode, 6 | RegularNode, 7 | RootNode, 8 | SchemaNode, 9 | } from '../nodes'; 10 | import type { BooleanishNode } from '../nodes/BooleanishNode'; 11 | 12 | export function isSchemaNode(node: unknown): node is SchemaNode { 13 | const name = Object.getPrototypeOf(node).constructor.name; 14 | return ( 15 | name === RootNode.name || 16 | name === RegularNode.name || 17 | name === MirroredRegularNode.name || 18 | name === ReferenceNode.name || 19 | name === MirroredReferenceNode.name 20 | ); 21 | } 22 | 23 | export function isRootNode(node: SchemaNode): node is RootNode { 24 | return Object.getPrototypeOf(node).constructor.name === 'RootNode'; 25 | } 26 | 27 | export function isRegularNode(node: SchemaNode): node is RegularNode { 28 | return 'types' in node && 'primaryType' in node && 'combiners' in node; 29 | } 30 | 31 | export function isMirroredNode(node: SchemaNode): node is MirroredSchemaNode { 32 | return 'mirroredNode' in node; 33 | } 34 | 35 | export function isReferenceNode(node: SchemaNode): node is ReferenceNode { 36 | return 'external' in node && 'value' in node; 37 | } 38 | 39 | export function isBooleanishNode(node: SchemaNode): node is BooleanishNode { 40 | return typeof node.fragment === 'boolean'; 41 | } 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards'; 2 | export * from './nodes'; 3 | export * from './tree'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/mergers/mergeAllOf.ts: -------------------------------------------------------------------------------- 1 | import { pathToPointer } from '@stoplight/json'; 2 | 3 | import { ResolvingError } from '../errors'; 4 | import type { SchemaFragment } from '../types'; 5 | import type { WalkerRefResolver, WalkingOptions } from '../walker/types'; 6 | 7 | const resolveAllOf = require('@stoplight/json-schema-merge-allof'); 8 | 9 | const store = new WeakMap>(); 10 | 11 | function _mergeAllOf( 12 | fragment: SchemaFragment, 13 | path: string[], 14 | resolveRef: WalkerRefResolver | null, 15 | seen: WeakMap, 16 | ): SchemaFragment { 17 | const cached = seen.get(fragment); 18 | if (cached !== void 0) { 19 | return cached; 20 | } 21 | 22 | const merged = resolveAllOf(fragment, { 23 | deep: false, 24 | resolvers: resolveAllOf.stoplightResolvers, 25 | ...(resolveRef !== null 26 | ? { 27 | $refResolver($ref: unknown) { 28 | if (typeof $ref !== 'string') { 29 | return {}; 30 | } 31 | 32 | if (pathToPointer(path).startsWith($ref)) { 33 | throw new ResolvingError('Circular reference detected'); 34 | } 35 | 36 | const allRefs = store.get(resolveRef)!; 37 | let schemaRefs = allRefs.get(fragment); 38 | 39 | if (schemaRefs === void 0) { 40 | schemaRefs = [$ref]; 41 | allRefs.set(fragment, schemaRefs); 42 | } else if (schemaRefs.includes($ref)) { 43 | const resolved = resolveRef(null, $ref); 44 | return 'allOf' in resolved ? _mergeAllOf(resolved, path, resolveRef, seen) : resolved; 45 | } else { 46 | schemaRefs.push($ref); 47 | } 48 | 49 | const resolved = resolveRef(null, $ref); 50 | 51 | if (Array.isArray(resolved.allOf)) { 52 | for (const member of resolved.allOf) { 53 | const index = schemaRefs.indexOf(member.$ref); 54 | if (typeof member.$ref === 'string' && index !== -1 && index !== schemaRefs.lastIndexOf(member.$ref)) { 55 | throw new ResolvingError('Circular reference detected'); 56 | } 57 | } 58 | } 59 | 60 | return resolved; 61 | }, 62 | } 63 | : null), 64 | }); 65 | 66 | seen.set(fragment, merged); 67 | return merged; 68 | } 69 | 70 | export function mergeAllOf( 71 | fragment: SchemaFragment, 72 | path: string[], 73 | walkingOptions: WalkingOptions, 74 | seen: WeakMap, 75 | ) { 76 | if (walkingOptions.resolveRef !== null && !store.has(walkingOptions.resolveRef)) { 77 | store.set(walkingOptions.resolveRef, new WeakMap()); 78 | } 79 | 80 | let merged = fragment; 81 | do { 82 | merged = _mergeAllOf(merged, path, walkingOptions.resolveRef, seen); 83 | } while ('allOf' in merged); 84 | 85 | return merged; 86 | } 87 | -------------------------------------------------------------------------------- /src/mergers/mergeOneOrAnyOf.ts: -------------------------------------------------------------------------------- 1 | import { SchemaCombinerName } from '../nodes/types'; 2 | import type { SchemaFragment } from '../types'; 3 | import type { WalkingOptions } from '../walker/types'; 4 | import { mergeAllOf } from './mergeAllOf'; 5 | 6 | export function mergeOneOrAnyOf( 7 | fragment: SchemaFragment, 8 | path: string[], 9 | walkingOptions: WalkingOptions, 10 | mergedAllOfs: WeakMap, 11 | ): SchemaFragment[] { 12 | const combiner = SchemaCombinerName.OneOf in fragment ? SchemaCombinerName.OneOf : SchemaCombinerName.AnyOf; 13 | const items = fragment[combiner]; 14 | 15 | if (!Array.isArray(items)) return []; // just in case 16 | 17 | const merged: SchemaFragment[] = []; 18 | 19 | if (Array.isArray(fragment.allOf)) { 20 | for (const item of items) { 21 | merged.push({ 22 | allOf: [...fragment.allOf, item], 23 | }); 24 | } 25 | 26 | return merged; 27 | } else { 28 | const prunedSchema = { ...fragment }; 29 | delete prunedSchema[combiner]; 30 | 31 | for (const item of items) { 32 | if (Object.keys(prunedSchema).length === 0) { 33 | merged.push(item); 34 | } else { 35 | merged.push( 36 | mergeAllOf( 37 | { 38 | allOf: [prunedSchema, item], 39 | }, 40 | path, 41 | walkingOptions, 42 | mergedAllOfs, 43 | ), 44 | ); 45 | } 46 | } 47 | } 48 | 49 | return merged; 50 | } 51 | -------------------------------------------------------------------------------- /src/nodes/BaseNode.ts: -------------------------------------------------------------------------------- 1 | import type { MirroredRegularNode } from './mirrored'; 2 | import type { RegularNode } from './RegularNode'; 3 | import type { RootNode } from './RootNode'; 4 | 5 | let SEED = BigInt(0); // cannot use literal, cause TS. 6 | 7 | export abstract class BaseNode { 8 | public readonly id: string; 9 | 10 | public parent: RegularNode | RootNode | MirroredRegularNode | null = null; 11 | public subpath: string[]; 12 | 13 | public get path(): ReadonlyArray { 14 | return this.parent === null ? this.subpath : [...this.parent.path, ...this.subpath]; 15 | } 16 | 17 | public get depth(): number { 18 | return this.parent === null ? 0 : this.parent.depth + 1; 19 | } 20 | 21 | private get parentChildren(): BaseNode[] { 22 | return (this.parent?.children ?? []) as BaseNode[]; 23 | } 24 | 25 | public get pos(): number { 26 | return Math.max(0, this.parentChildren.indexOf(this)); 27 | } 28 | 29 | public get isFirst(): boolean { 30 | return this.pos === 0; 31 | } 32 | 33 | public get isLast(): boolean { 34 | return this.pos === this.parentChildren.length - 1; 35 | } 36 | 37 | protected constructor() { 38 | this.id = String(SEED++); 39 | this.subpath = []; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/nodes/BooleanishNode.ts: -------------------------------------------------------------------------------- 1 | import { BaseNode } from './BaseNode'; 2 | 3 | export class BooleanishNode extends BaseNode { 4 | constructor(public readonly fragment: boolean) { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/nodes/ReferenceNode.ts: -------------------------------------------------------------------------------- 1 | import { isLocalRef } from '@stoplight/json'; 2 | 3 | import { unwrapStringOrNull } from '../accessors/unwrap'; 4 | import type { SchemaFragment } from '../types'; 5 | import { BaseNode } from './BaseNode'; 6 | 7 | export class ReferenceNode extends BaseNode { 8 | public readonly value: string | null; 9 | 10 | constructor(public readonly fragment: SchemaFragment, public readonly error: string | null) { 11 | super(); 12 | 13 | this.value = unwrapStringOrNull(fragment.$ref); 14 | } 15 | 16 | public get external() { 17 | return this.value !== null && !isLocalRef(this.value); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/nodes/RegularNode.ts: -------------------------------------------------------------------------------- 1 | import type { Dictionary } from '@stoplight/types'; 2 | 3 | import { getAnnotations } from '../accessors/getAnnotations'; 4 | import { getCombiners } from '../accessors/getCombiners'; 5 | import { getPrimaryType } from '../accessors/getPrimaryType'; 6 | import { getRequired } from '../accessors/getRequired'; 7 | import { getTypes } from '../accessors/getTypes'; 8 | import { getValidations } from '../accessors/getValidations'; 9 | import { isDeprecated } from '../accessors/isDeprecated'; 10 | import { unwrapArrayOrNull, unwrapStringOrNull } from '../accessors/unwrap'; 11 | import type { SchemaFragment } from '../types'; 12 | import { BaseNode } from './BaseNode'; 13 | import type { BooleanishNode } from './BooleanishNode'; 14 | import type { ReferenceNode } from './ReferenceNode'; 15 | import { MirroredSchemaNode, SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from './types'; 16 | 17 | export class RegularNode extends BaseNode { 18 | public readonly $id: string | null; 19 | public readonly types: SchemaNodeKind[] | null; 20 | public readonly primaryType: SchemaNodeKind | null; // object (first choice) or array (second option), primitive last 21 | public readonly combiners: SchemaCombinerName[] | null; 22 | 23 | public readonly required: string[] | null; 24 | public readonly enum: unknown[] | null; // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1 25 | public readonly format: string | null; // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7 26 | public readonly title: string | null; 27 | public readonly deprecated: boolean; 28 | 29 | public children: (RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined; 30 | 31 | public readonly annotations: Readonly>>; 32 | public readonly validations: Readonly>; 33 | public readonly originalFragment: SchemaFragment; 34 | 35 | constructor(public readonly fragment: SchemaFragment, context?: { originalFragment?: SchemaFragment }) { 36 | super(); 37 | 38 | this.$id = unwrapStringOrNull('id' in fragment ? fragment.id : fragment.$id); 39 | this.types = getTypes(fragment); 40 | this.primaryType = getPrimaryType(fragment, this.types); 41 | this.combiners = getCombiners(fragment); 42 | 43 | this.deprecated = isDeprecated(fragment); 44 | this.enum = 'const' in fragment ? [fragment.const] : unwrapArrayOrNull(fragment.enum); 45 | this.required = getRequired(fragment.required); 46 | this.format = unwrapStringOrNull(fragment.format); 47 | this.title = unwrapStringOrNull(fragment.title); 48 | 49 | this.annotations = getAnnotations(fragment); 50 | this.validations = getValidations(fragment, this.types); 51 | this.originalFragment = context?.originalFragment ?? fragment; 52 | 53 | this.children = void 0; 54 | } 55 | 56 | public get simple() { 57 | return ( 58 | this.primaryType !== SchemaNodeKind.Array && this.primaryType !== SchemaNodeKind.Object && this.combiners === null 59 | ); 60 | } 61 | 62 | public get unknown() { 63 | return ( 64 | this.types === null && 65 | this.combiners === null && 66 | this.format === null && 67 | this.enum === null && 68 | Object.keys(this.annotations).length + Object.keys(this.validations).length === 0 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/nodes/RootNode.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaFragment } from '../types'; 2 | import { BaseNode } from './BaseNode'; 3 | import type { SchemaNode } from './types'; 4 | 5 | export class RootNode extends BaseNode { 6 | public readonly parent = null; 7 | public readonly children: SchemaNode[]; 8 | 9 | constructor(public readonly fragment: SchemaFragment) { 10 | super(); 11 | this.children = []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseNode } from './BaseNode'; 2 | export { BooleanishNode } from './BooleanishNode'; 3 | export * from './mirrored'; 4 | export { ReferenceNode } from './ReferenceNode'; 5 | export { RegularNode } from './RegularNode'; 6 | export { RootNode } from './RootNode'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /src/nodes/mirrored/MirroredReferenceNode.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaFragment } from '../../types'; 2 | import { BaseNode } from '../BaseNode'; 3 | import type { ReferenceNode } from '../ReferenceNode'; 4 | 5 | export class MirroredReferenceNode extends BaseNode implements ReferenceNode { 6 | public readonly fragment: SchemaFragment; 7 | 8 | constructor(public readonly mirroredNode: ReferenceNode) { 9 | super(); 10 | this.fragment = mirroredNode.fragment; 11 | } 12 | 13 | get error() { 14 | return this.mirroredNode.error; 15 | } 16 | 17 | get value() { 18 | return this.mirroredNode.value; 19 | } 20 | 21 | public get external() { 22 | return this.mirroredNode.external; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/nodes/mirrored/MirroredRegularNode.ts: -------------------------------------------------------------------------------- 1 | import type { Dictionary } from '@stoplight/types'; 2 | 3 | import { isReferenceNode, isRegularNode } from '../../guards'; 4 | import type { SchemaFragment } from '../../types'; 5 | import { isNonNullable } from '../../utils'; 6 | import { BaseNode } from '../BaseNode'; 7 | import { BooleanishNode } from '../BooleanishNode'; 8 | import type { ReferenceNode } from '../ReferenceNode'; 9 | import type { RegularNode } from '../RegularNode'; 10 | import type { SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from '../types'; 11 | import { MirroredReferenceNode } from './MirroredReferenceNode'; 12 | 13 | export class MirroredRegularNode extends BaseNode implements RegularNode { 14 | public readonly fragment: SchemaFragment; 15 | public readonly $id!: string | null; 16 | public readonly types!: SchemaNodeKind[] | null; 17 | public readonly primaryType!: SchemaNodeKind | null; 18 | public readonly combiners!: SchemaCombinerName[] | null; 19 | 20 | public readonly required!: string[] | null; 21 | public readonly enum!: unknown[] | null; 22 | public readonly format!: string | null; 23 | public readonly title!: string | null; 24 | public readonly deprecated!: boolean; 25 | 26 | public readonly annotations!: Readonly>>; 27 | public readonly validations!: Readonly>; 28 | public readonly originalFragment!: SchemaFragment; 29 | 30 | public readonly simple!: boolean; 31 | public readonly unknown!: boolean; 32 | 33 | private readonly cache: WeakMap< 34 | RegularNode | BooleanishNode | ReferenceNode, 35 | MirroredRegularNode | BooleanishNode | MirroredReferenceNode 36 | >; 37 | 38 | constructor(public readonly mirroredNode: RegularNode, context?: { originalFragment?: SchemaFragment }) { 39 | super(); 40 | this.fragment = mirroredNode.fragment; 41 | this.originalFragment = context?.originalFragment ?? mirroredNode.originalFragment; 42 | 43 | this.cache = new WeakMap(); 44 | 45 | this._this = new Proxy(this, { 46 | get(target, key) { 47 | if (key in target) { 48 | return target[key]; 49 | } 50 | 51 | if (key in mirroredNode) { 52 | return Reflect.get(mirroredNode, key, mirroredNode); 53 | } 54 | 55 | return; 56 | }, 57 | 58 | has(target, key) { 59 | return key in target || key in mirroredNode; 60 | }, 61 | }); 62 | 63 | return this._this; 64 | } 65 | 66 | private readonly _this: MirroredRegularNode; 67 | 68 | private _children?: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[]; 69 | 70 | public get children(): (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] | null | undefined { 71 | const referencedChildren = this.mirroredNode.children; 72 | 73 | if (!isNonNullable(referencedChildren)) { 74 | return referencedChildren; 75 | } 76 | 77 | if (this._children === void 0) { 78 | this._children = []; 79 | } else { 80 | this._children.length = 0; 81 | } 82 | 83 | const children: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] = this._children; 84 | for (const child of referencedChildren) { 85 | // this is to avoid pointing at nested mirroring 86 | const cached = this.cache.get(child); 87 | 88 | if (cached !== void 0) { 89 | children.push(cached); 90 | continue; 91 | } 92 | 93 | const mirroredChild = isRegularNode(child) 94 | ? new MirroredRegularNode(child) 95 | : isReferenceNode(child) 96 | ? new MirroredReferenceNode(child) 97 | : new BooleanishNode(child.fragment); 98 | 99 | mirroredChild.parent = this._this; 100 | mirroredChild.subpath = child.subpath; 101 | this.cache.set(child, mirroredChild); 102 | children.push(mirroredChild); 103 | } 104 | 105 | return children; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/nodes/mirrored/index.ts: -------------------------------------------------------------------------------- 1 | export { MirroredReferenceNode } from './MirroredReferenceNode'; 2 | export { MirroredRegularNode } from './MirroredRegularNode'; 3 | -------------------------------------------------------------------------------- /src/nodes/types.ts: -------------------------------------------------------------------------------- 1 | import type { BooleanishNode } from './BooleanishNode'; 2 | import type { MirroredReferenceNode } from './mirrored/MirroredReferenceNode'; 3 | import type { MirroredRegularNode } from './mirrored/MirroredRegularNode'; 4 | import type { ReferenceNode } from './ReferenceNode'; 5 | import type { RegularNode } from './RegularNode'; 6 | import type { RootNode } from './RootNode'; 7 | 8 | export type MirroredSchemaNode = MirroredRegularNode | MirroredReferenceNode; 9 | 10 | export type SchemaNode = RootNode | RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode; 11 | 12 | export enum SchemaNodeKind { 13 | Any = 'any', 14 | String = 'string', 15 | Number = 'number', 16 | Integer = 'integer', 17 | Boolean = 'boolean', 18 | Null = 'null', 19 | Array = 'array', 20 | Object = 'object', 21 | } 22 | 23 | export enum SchemaCombinerName { 24 | AllOf = 'allOf', 25 | AnyOf = 'anyOf', 26 | OneOf = 'oneOf', 27 | } 28 | 29 | export { SchemaAnnotations } from '../accessors/getAnnotations'; 30 | -------------------------------------------------------------------------------- /src/tree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tree'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/tree/tree.ts: -------------------------------------------------------------------------------- 1 | import { extractPointerFromRef, extractSourceFromRef, resolveInlineRef } from '@stoplight/json'; 2 | 3 | import { ResolvingError } from '../errors'; 4 | import { RootNode } from '../nodes/RootNode'; 5 | import type { SchemaFragment } from '../types'; 6 | import { isObjectLiteral } from '../utils'; 7 | import { Walker } from '../walker'; 8 | import type { WalkerRefResolver } from '../walker/types'; 9 | import type { SchemaTreeOptions } from './types'; 10 | 11 | export class SchemaTree { 12 | public walker: Walker; 13 | public root: RootNode; 14 | private readonly resolvedRefs = new Map(); 15 | 16 | constructor(public schema: SchemaFragment, protected readonly opts?: Partial) { 17 | this.root = new RootNode(schema); 18 | this.resolvedRefs = new Map(); 19 | this.walker = new Walker(this.root, { 20 | mergeAllOf: this.opts?.mergeAllOf !== false, 21 | resolveRef: opts?.refResolver === null ? null : this.resolveRef, 22 | maxRefDepth: opts?.maxRefDepth, 23 | }); 24 | } 25 | 26 | public destroy() { 27 | this.root.children.length = 0; 28 | this.walker.destroy(); 29 | this.resolvedRefs.clear(); 30 | } 31 | 32 | public populate() { 33 | this.invokeWalker(this.walker); 34 | } 35 | 36 | public invokeWalker(walker: Walker) { 37 | walker.walk(); 38 | } 39 | 40 | protected resolveRef: WalkerRefResolver = (path, $ref) => { 41 | if (this.resolvedRefs.has($ref)) { 42 | return this.resolvedRefs.get($ref); 43 | } 44 | 45 | const seenRefs: string[] = []; 46 | let cur$ref: unknown = $ref; 47 | let resolvedValue!: SchemaFragment; 48 | 49 | while (typeof cur$ref === 'string') { 50 | if (seenRefs.includes(cur$ref)) { 51 | break; 52 | } 53 | 54 | seenRefs.push(cur$ref); 55 | resolvedValue = this._resolveRef(path, cur$ref); 56 | cur$ref = resolvedValue.$ref; 57 | } 58 | 59 | this.resolvedRefs.set($ref, resolvedValue); 60 | return resolvedValue; 61 | }; 62 | 63 | private _resolveRef: WalkerRefResolver = (path, $ref) => { 64 | const source = extractSourceFromRef($ref); 65 | const pointer = extractPointerFromRef($ref); 66 | const refResolver = this.opts?.refResolver; 67 | 68 | if (typeof refResolver === 'function') { 69 | return refResolver({ source, pointer }, path, this.schema); 70 | } else if (source !== null) { 71 | throw new ResolvingError('Cannot dereference external references'); 72 | } else if (pointer === null) { 73 | throw new ResolvingError('The pointer is empty'); 74 | } else if (isObjectLiteral(this.schema)) { 75 | const value = resolveInlineRef(this.schema, pointer); 76 | if (!isObjectLiteral(value)) { 77 | throw new ResolvingError('Invalid value'); 78 | } 79 | 80 | return value; 81 | } else { 82 | throw new ResolvingError('Unexpected input'); 83 | } 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/tree/types.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaFragment } from '../types'; 2 | 3 | export type SchemaTreeOptions = { 4 | mergeAllOf: boolean; 5 | /** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */ 6 | refResolver: SchemaTreeRefDereferenceFn | null; 7 | /** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */ 8 | maxRefDepth?: number | null; 9 | }; 10 | 11 | export type SchemaTreeRefInfo = { 12 | source: string | null; 13 | pointer: string | null; 14 | }; 15 | 16 | export type SchemaTreeRefDereferenceFn = ( 17 | ref: SchemaTreeRefInfo, 18 | propertyPath: string[] | null, 19 | schema: SchemaFragment, 20 | ) => SchemaFragment; 21 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; 2 | 3 | export type ViewMode = 'read' | 'write' | 'standalone'; 4 | 5 | export type SchemaFragment = Record | JSONSchema4 | JSONSchema6 | JSONSchema7; 6 | -------------------------------------------------------------------------------- /src/utils/guards.ts: -------------------------------------------------------------------------------- 1 | import type { Dictionary } from '@stoplight/types'; 2 | 3 | import type { SchemaFragment } from '../types'; 4 | 5 | export function isStringOrNumber(value: unknown): value is number | string { 6 | return typeof value === 'string' || typeof value === 'number'; 7 | } 8 | 9 | export function isObject(maybeObj: unknown): maybeObj is object { 10 | return maybeObj !== void 0 && maybeObj !== null && typeof maybeObj === 'object'; 11 | } 12 | 13 | export function isPrimitive( 14 | maybePrimitive: unknown, 15 | ): maybePrimitive is string | number | boolean | undefined | null | symbol | bigint { 16 | return typeof maybePrimitive !== 'function' && !isObject(maybePrimitive); 17 | } 18 | 19 | export function isObjectLiteral(maybeObj: unknown): maybeObj is Dictionary { 20 | if (isPrimitive(maybeObj) === true) return false; 21 | const proto = Object.getPrototypeOf(maybeObj); 22 | return proto === null || proto === Object.prototype; 23 | } 24 | 25 | export function isNonNullable(maybeNullable: T): maybeNullable is NonNullable { 26 | return maybeNullable !== void 0 && maybeNullable !== null; 27 | } 28 | 29 | export function isValidSchemaFragment(maybeSchemaFragment: unknown): maybeSchemaFragment is SchemaFragment { 30 | return typeof maybeSchemaFragment === 'boolean' || isObjectLiteral(maybeSchemaFragment); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards'; 2 | export * from './pick'; 3 | -------------------------------------------------------------------------------- /src/utils/pick.ts: -------------------------------------------------------------------------------- 1 | import type { Dictionary } from '@stoplight/types'; 2 | 3 | export function pick(target: object, keys: readonly (string | number)[]) { 4 | const source: Dictionary = {}; 5 | 6 | for (const key of keys) { 7 | if (key in target) { 8 | source[key] = target[key]; 9 | } 10 | } 11 | 12 | return source; 13 | } 14 | -------------------------------------------------------------------------------- /src/walker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './walker'; 3 | -------------------------------------------------------------------------------- /src/walker/types.ts: -------------------------------------------------------------------------------- 1 | import type { RegularNode, SchemaNode } from '../nodes'; 2 | import type { RootNode } from '../nodes/RootNode'; 3 | import type { SchemaFragment } from '../types'; 4 | 5 | export type WalkerRefResolver = (path: string[] | null, $ref: string) => SchemaFragment; 6 | 7 | export type WalkingOptions = { 8 | mergeAllOf: boolean; 9 | /** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */ 10 | resolveRef: WalkerRefResolver | null; 11 | /** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */ 12 | maxRefDepth?: number | null; 13 | }; 14 | 15 | export type WalkerSnapshot = { 16 | readonly fragment: SchemaFragment | boolean; 17 | readonly depth: number; 18 | readonly schemaNode: RegularNode | RootNode; 19 | readonly path: string[]; 20 | }; 21 | 22 | export type WalkerHookAction = 'filter' | 'stepIn'; 23 | export type WalkerHookHandler = (node: SchemaNode) => boolean; 24 | 25 | export type WalkerNodeEventHandler = (node: SchemaNode) => void; 26 | export type WalkerFragmentEventHandler = (node: SchemaFragment | boolean) => void; 27 | export type WalkerErrorEventHandler = (ex: Error) => void; 28 | 29 | export type WalkerEmitter = { 30 | enterNode: WalkerNodeEventHandler; 31 | exitNode: WalkerNodeEventHandler; 32 | 33 | includeNode: WalkerNodeEventHandler; 34 | skipNode: WalkerNodeEventHandler; 35 | 36 | stepInNode: WalkerNodeEventHandler; 37 | stepOverNode: WalkerNodeEventHandler; 38 | stepOutNode: WalkerNodeEventHandler; 39 | 40 | enterFragment: WalkerFragmentEventHandler; 41 | exitFragment: WalkerFragmentEventHandler; 42 | 43 | error: WalkerErrorEventHandler; 44 | }; 45 | -------------------------------------------------------------------------------- /src/walker/walker.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@stoplight/lifecycle'; 2 | import type { Dictionary } from '@stoplight/types'; 3 | import createMagicError from 'magic-error'; 4 | 5 | import { MergingError } from '../errors'; 6 | import { isMirroredNode, isReferenceNode, isRegularNode, isRootNode } from '../guards'; 7 | import { mergeAllOf } from '../mergers/mergeAllOf'; 8 | import { mergeOneOrAnyOf } from '../mergers/mergeOneOrAnyOf'; 9 | import { MirroredReferenceNode, MirroredRegularNode, MirroredSchemaNode, ReferenceNode, RegularNode } from '../nodes'; 10 | import { BooleanishNode } from '../nodes/BooleanishNode'; 11 | import type { RootNode } from '../nodes/RootNode'; 12 | import { SchemaCombinerName, SchemaNode, SchemaNodeKind } from '../nodes/types'; 13 | import type { SchemaFragment } from '../types'; 14 | import { isNonNullable, isObjectLiteral, isValidSchemaFragment } from '../utils/guards'; 15 | import type { WalkerEmitter, WalkerHookAction, WalkerHookHandler, WalkerSnapshot, WalkingOptions } from './types'; 16 | 17 | type InternalWalkerState = { 18 | depth: number; 19 | pathLength: number; 20 | schemaNode: RegularNode | RootNode; 21 | }; 22 | 23 | type ProcessedFragment = SchemaFragment | SchemaFragment[]; 24 | 25 | export class Walker extends EventEmitter { 26 | public readonly path: string[]; 27 | public depth: number; 28 | 29 | protected fragment: SchemaFragment | boolean; 30 | protected schemaNode: RegularNode | RootNode; 31 | 32 | private mergedAllOfs: WeakMap; 33 | private processedFragments: WeakMap; 34 | 35 | private readonly hooks: Partial>; 36 | 37 | constructor(protected readonly root: RootNode, protected readonly walkingOptions: WalkingOptions) { 38 | super(); 39 | 40 | let maxRefDepth = walkingOptions.maxRefDepth ?? null; 41 | if (typeof maxRefDepth === 'number') { 42 | if (maxRefDepth < 1) { 43 | maxRefDepth = null; 44 | } else if (maxRefDepth > 1000) { 45 | // experimented with 1500 and the recursion limit is still lower than that 46 | maxRefDepth = 1000; 47 | } 48 | } 49 | walkingOptions.maxRefDepth = maxRefDepth; 50 | 51 | this.path = []; 52 | this.depth = -1; 53 | this.fragment = root.fragment; 54 | this.schemaNode = root; 55 | this.processedFragments = new WeakMap(); 56 | this.mergedAllOfs = new WeakMap(); 57 | 58 | this.hooks = {}; 59 | } 60 | 61 | public destroy() { 62 | this.path.length = 0; 63 | this.depth = -1; 64 | this.fragment = this.root.fragment; 65 | this.schemaNode = this.root; 66 | this.processedFragments = new WeakMap(); 67 | this.mergedAllOfs = new WeakMap(); 68 | } 69 | 70 | public loadSnapshot(snapshot: WalkerSnapshot) { 71 | this.path.splice(0, this.path.length, ...snapshot.path); 72 | this.depth = snapshot.depth; 73 | this.fragment = snapshot.fragment; 74 | this.schemaNode = snapshot.schemaNode; 75 | } 76 | 77 | public saveSnapshot(): WalkerSnapshot { 78 | return { 79 | depth: this.depth, 80 | fragment: this.fragment, 81 | schemaNode: this.schemaNode, 82 | path: this.path.slice(), 83 | }; 84 | } 85 | 86 | public hookInto(action: WalkerHookAction, handler: WalkerHookHandler) { 87 | this.hooks[action] = handler; 88 | } 89 | 90 | public restoreWalkerAtNode(node: RegularNode) { 91 | this.processedFragments.delete(node.fragment); 92 | this.path.splice(0, this.path.length, ...node.path); 93 | this.depth = node.depth; 94 | this.fragment = node.fragment; 95 | this.schemaNode = node; 96 | } 97 | 98 | public walk(): void { 99 | const { depth: initialDepth, fragment } = this; 100 | let { schemaNode: initialSchemaNode } = this; 101 | 102 | if (initialDepth === -1 && Object.keys(fragment).length === 0) { 103 | // empty schema, nothing to do 104 | return; 105 | } 106 | 107 | while (isMirroredNode(initialSchemaNode)) { 108 | if (!isRegularNode(initialSchemaNode.mirroredNode)) { 109 | return; 110 | } 111 | 112 | if (initialSchemaNode.mirroredNode.children === void 0) { 113 | this.restoreWalkerAtNode(initialSchemaNode.mirroredNode); 114 | initialSchemaNode = this.schemaNode; 115 | this.depth = initialDepth; 116 | } else { 117 | return; 118 | } 119 | } 120 | 121 | const state = this.dumpInternalWalkerState(); 122 | 123 | super.emit('enterFragment', fragment); 124 | const [schemaNode, initialFragment] = this.processFragment(); 125 | super.emit('enterNode', schemaNode); 126 | 127 | const actualNode = isMirroredNode(schemaNode) ? schemaNode.mirroredNode : schemaNode; 128 | if (typeof schemaNode.fragment !== 'boolean' && initialFragment !== null) { 129 | this.processedFragments.set(schemaNode.fragment, actualNode); 130 | this.processedFragments.set(initialFragment, actualNode); 131 | } 132 | 133 | this.fragment = schemaNode.fragment; 134 | this.depth = initialDepth + 1; 135 | 136 | if (!isRootNode(schemaNode)) { 137 | schemaNode.parent = initialSchemaNode; 138 | schemaNode.subpath = this.path.slice(initialSchemaNode.path.length); 139 | } 140 | 141 | const isIncluded = this.hooks.filter?.(schemaNode); 142 | 143 | if (isIncluded === false) { 144 | super.emit('skipNode', schemaNode); 145 | return; 146 | } 147 | 148 | if ('children' in initialSchemaNode && !isRootNode(schemaNode)) { 149 | if (initialSchemaNode.children === void 0) { 150 | (initialSchemaNode as RegularNode).children = [schemaNode]; 151 | } else { 152 | initialSchemaNode.children!.push(schemaNode); 153 | } 154 | } 155 | 156 | super.emit('includeNode', schemaNode); 157 | 158 | if (isRegularNode(schemaNode)) { 159 | this.schemaNode = schemaNode; 160 | 161 | if (this.hooks.stepIn?.(schemaNode) !== false) { 162 | super.emit('stepInNode', schemaNode); 163 | this.walkNodeChildren(); 164 | super.emit('stepOutNode', schemaNode); 165 | } else { 166 | super.emit('stepOverNode', schemaNode); 167 | } 168 | } 169 | 170 | super.emit('exitNode', schemaNode); 171 | this.restoreInternalWalkerState(state); 172 | super.emit('exitFragment', fragment); 173 | } 174 | 175 | protected dumpInternalWalkerState(): InternalWalkerState { 176 | return { 177 | depth: this.depth, 178 | pathLength: this.path.length, 179 | schemaNode: this.schemaNode, 180 | }; 181 | } 182 | 183 | protected restoreInternalWalkerState({ depth, pathLength, schemaNode }: InternalWalkerState) { 184 | this.depth = depth; 185 | this.path.length = pathLength; 186 | this.schemaNode = schemaNode; 187 | } 188 | 189 | protected walkNodeChildren(): void { 190 | const { fragment, schemaNode } = this; 191 | 192 | if (!isRegularNode(schemaNode) || typeof fragment === 'boolean') return; 193 | 194 | const state = this.dumpInternalWalkerState(); 195 | 196 | if (schemaNode.combiners !== null) { 197 | for (const combiner of schemaNode.combiners) { 198 | const items = fragment[combiner]; 199 | if (!Array.isArray(items)) continue; 200 | 201 | let i = -1; 202 | for (const item of items) { 203 | i++; 204 | if (!isObjectLiteral(item)) continue; 205 | this.fragment = item; 206 | this.restoreInternalWalkerState(state); 207 | this.path.push(combiner, String(i)); 208 | this.walk(); 209 | } 210 | } 211 | } 212 | 213 | switch (schemaNode.primaryType) { 214 | case SchemaNodeKind.Array: 215 | if (Array.isArray(fragment.items)) { 216 | let i = -1; 217 | for (const item of fragment.items) { 218 | i++; 219 | if (!isValidSchemaFragment(item)) continue; 220 | this.fragment = item; 221 | this.restoreInternalWalkerState(state); 222 | this.path.push('items', String(i)); 223 | this.walk(); 224 | } 225 | } else { 226 | if (isObjectLiteral(fragment.items)) { 227 | this.fragment = fragment.items; 228 | this.restoreInternalWalkerState(state); 229 | this.path.push('items'); 230 | this.walk(); 231 | } 232 | 233 | if (isValidSchemaFragment(fragment.additionalItems)) { 234 | this.fragment = fragment.additionalItems; 235 | this.restoreInternalWalkerState(state); 236 | this.path.push('additionalItems'); 237 | this.walk(); 238 | } 239 | } 240 | 241 | break; 242 | case SchemaNodeKind.Object: 243 | if (isObjectLiteral(fragment.properties)) { 244 | for (const key of Object.keys(fragment.properties)) { 245 | const value = fragment.properties[key]; 246 | if (!isValidSchemaFragment(value)) continue; 247 | this.fragment = value; 248 | this.restoreInternalWalkerState(state); 249 | this.path.push('properties', key); 250 | this.walk(); 251 | } 252 | } 253 | 254 | if (isObjectLiteral(fragment.patternProperties)) { 255 | for (const key of Object.keys(fragment.patternProperties)) { 256 | const value = fragment.patternProperties[key]; 257 | if (!isValidSchemaFragment(value)) continue; 258 | this.fragment = value; 259 | this.restoreInternalWalkerState(state); 260 | this.path.push('patternProperties', key); 261 | this.walk(); 262 | } 263 | } 264 | 265 | if (isValidSchemaFragment(fragment.additionalProperties)) { 266 | this.fragment = fragment.additionalProperties; 267 | this.restoreInternalWalkerState(state); 268 | this.path.push('additionalProperties'); 269 | this.walk(); 270 | } 271 | 272 | break; 273 | } 274 | 275 | this.schemaNode = schemaNode; 276 | } 277 | 278 | protected retrieveFromFragment( 279 | fragment: ProcessedFragment, 280 | originalFragment: SchemaFragment, 281 | ): [MirroredSchemaNode, ProcessedFragment] | void { 282 | const processedSchemaNode = this.processedFragments.get(fragment); 283 | if (processedSchemaNode !== void 0) { 284 | if (isRegularNode(processedSchemaNode)) { 285 | return [new MirroredRegularNode(processedSchemaNode, { originalFragment }), fragment]; 286 | } 287 | 288 | if (isReferenceNode(processedSchemaNode)) { 289 | return [new MirroredReferenceNode(processedSchemaNode), fragment]; 290 | } 291 | 292 | // whoops, we don't know what to do with it 293 | throw new TypeError('Cannot mirror the node'); 294 | } 295 | } 296 | 297 | protected processFragment(): [SchemaNode, ProcessedFragment | null] { 298 | const { walkingOptions, path, fragment: originalFragment, depth } = this; 299 | let { fragment } = this; 300 | 301 | if (typeof fragment === 'boolean') { 302 | return [new BooleanishNode(fragment), null]; 303 | } 304 | 305 | if (typeof originalFragment === 'boolean') { 306 | throw new TypeError('Original fragment cannot be a boolean'); 307 | } 308 | 309 | let retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(fragment, fragment) : null; 310 | 311 | if (retrieved) { 312 | return retrieved; 313 | } 314 | 315 | let initialFragment: ProcessedFragment = fragment; 316 | 317 | if ('$ref' in fragment) { 318 | if (typeof walkingOptions.maxRefDepth === 'number' && walkingOptions.maxRefDepth < depth) { 319 | return [new ReferenceNode(fragment, `max $ref depth limit reached`), fragment]; 320 | } else if (typeof fragment.$ref !== 'string') { 321 | return [new ReferenceNode(fragment, '$ref is not a string'), fragment]; 322 | } else if (walkingOptions.resolveRef !== null) { 323 | try { 324 | let newFragment = walkingOptions.resolveRef(path, fragment.$ref); 325 | 326 | if (typeof fragment.description === 'string') { 327 | newFragment = { ...newFragment }; 328 | Object.assign(newFragment, { description: fragment.description }); 329 | } else { 330 | retrieved = this.retrieveFromFragment(newFragment, originalFragment); 331 | if (retrieved) { 332 | return retrieved; 333 | } 334 | } 335 | 336 | fragment = newFragment; 337 | } catch (ex) { 338 | super.emit('error', createMagicError(ex)); 339 | return [new ReferenceNode(fragment, ex?.message ?? 'Unknown resolving error'), fragment]; 340 | } 341 | } else { 342 | return [new ReferenceNode(fragment, null), fragment]; 343 | } 344 | } 345 | //fragment with type 'array' and no description should adopt description of $ref if it exists. 346 | if (fragment.type === 'array' && fragment.description === void 0) { 347 | if (fragment.items !== void 0 && isObjectLiteral(fragment.items)) { 348 | for (const key of Object.keys(fragment.items)) { 349 | if (key === '$ref') { 350 | const refToResolve = fragment.items[key]; 351 | if (typeof refToResolve !== 'string') { 352 | return [new ReferenceNode(fragment, '$ref is not a string'), fragment]; 353 | } else if (walkingOptions.resolveRef !== null) { 354 | try { 355 | let newFragment = walkingOptions.resolveRef(path, refToResolve); 356 | if (newFragment.description !== void 0) { 357 | newFragment = { ...newFragment }; 358 | Object.assign(fragment, { description: newFragment.description }); 359 | } 360 | } catch (ex) { 361 | super.emit('error', createMagicError(ex)); 362 | } 363 | } 364 | } 365 | } 366 | } 367 | } 368 | if (walkingOptions.mergeAllOf && SchemaCombinerName.AllOf in fragment) { 369 | try { 370 | if (Array.isArray(fragment.allOf)) { 371 | initialFragment = fragment.allOf; 372 | } 373 | 374 | fragment = mergeAllOf(fragment, path, walkingOptions, this.mergedAllOfs); 375 | } catch (ex) { 376 | initialFragment = fragment; 377 | super.emit('error', createMagicError(new MergingError(ex?.message ?? 'Unknown merging error'))); 378 | // no the end of the world - we will render raw unprocessed fragment 379 | } 380 | } 381 | 382 | if (SchemaCombinerName.OneOf in fragment || SchemaCombinerName.AnyOf in fragment) { 383 | try { 384 | const merged = mergeOneOrAnyOf(fragment, path, walkingOptions, this.mergedAllOfs); 385 | if (merged.length === 1) { 386 | return [new RegularNode(merged[0], { originalFragment }), initialFragment]; 387 | } else { 388 | const combiner = SchemaCombinerName.OneOf in fragment ? SchemaCombinerName.OneOf : SchemaCombinerName.AnyOf; 389 | return [new RegularNode({ [combiner]: merged }, { originalFragment }), initialFragment]; 390 | } 391 | } catch (ex) { 392 | super.emit('error', createMagicError(new MergingError(ex?.message ?? 'Unknown merging error'))); 393 | // no the end of the world - we will render raw unprocessed fragment 394 | } 395 | } 396 | 397 | retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(initialFragment, originalFragment) : null; 398 | 399 | if (retrieved) { 400 | return retrieved; 401 | } 402 | 403 | return [new RegularNode(fragment, { originalFragment }), initialFragment]; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | // This config is used by `sl-scripts build` and `sl-scripts build.docs` 2 | { 3 | "extends": "./tsconfig.json", 4 | 5 | // NOTE: must only include one element, otherwise build process into dist folder ends up with incorrect structure 6 | "include": ["src"], 7 | 8 | // Ignore dev folders like __tests__ and __stories__ when building for distribution 9 | "exclude": ["**/__*__/**"], 10 | 11 | "compilerOptions": { 12 | "outDir": "dist", 13 | "target": "ES2018", 14 | "moduleResolution": "node" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@stoplight/scripts/tsconfig.json", 3 | // target all ts files 4 | "include": ["."], 5 | "exclude": ["**/__mocks__/**", "dist/"], 6 | "compilerOptions": { 7 | "lib": ["dom", "ES2018"], 8 | "target": "ES2020", 9 | "importsNotUsedAsValues": "error", 10 | "esModuleInterop": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------