├── .editorconfig ├── .eslintrc.yml ├── .gitattributes ├── .github └── workflows │ └── CI-CD.yaml ├── .gitignore ├── .mocharc.yml ├── .nycrc.yml ├── .vscode ├── launch.json └── tasks.json ├── 404.md ├── LICENSE ├── README.md ├── _config.yml ├── docs ├── anchor.md ├── error-handling.md ├── file.md ├── json-schema.md ├── options.md ├── parse-file.md ├── pointer.md ├── read-file.md ├── reference.md ├── resolution.md ├── resource.md ├── schema-error.md ├── schema-structure.afphoto ├── schema-structure.md └── schema-structure.svg ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── helpers.ts ├── hooks │ ├── determine-file-type.ts │ ├── index-file.ts │ ├── parse-file.ts │ └── read-file.ts ├── index.ts ├── isomorphic.browser.ts ├── isomorphic.node.ts ├── multi-error.ts ├── options.ts ├── process-file.ts ├── read-json-schema.ts └── url-utils.ts ├── test ├── specs │ ├── circular-refs │ │ ├── ancestor │ │ │ ├── schema.json │ │ │ └── spec.js │ │ ├── cross-reference │ │ │ ├── child.json │ │ │ ├── parent.json │ │ │ ├── schema.json │ │ │ └── spec.js │ │ └── direct │ │ │ ├── schema.json │ │ │ └── spec.js │ ├── exports.spec.js │ ├── multi-file │ │ ├── address.json │ │ ├── person.json │ │ ├── schema.json │ │ └── spec.js │ ├── one-file │ │ ├── schema.json │ │ └── spec.js │ ├── options │ │ ├── continue-on-error │ │ │ ├── address.json │ │ │ ├── company.xyz │ │ │ ├── person.json │ │ │ ├── schema.json │ │ │ └── spec.js │ │ ├── determine-file-type │ │ │ ├── schema.json │ │ │ └── spec.js │ │ ├── index-file │ │ │ ├── schema.json │ │ │ └── spec.js │ │ ├── parse-file │ │ │ ├── schema.json │ │ │ └── spec.js │ │ └── read-file │ │ │ ├── person.json │ │ │ ├── schema.json │ │ │ └── spec.js │ └── real-world │ │ ├── download.js │ │ ├── known-errors.js │ │ └── real-world.spec.js └── utils │ ├── assert.js │ └── path.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config 2 | # http://EditorConfig.org 3 | 4 | # This EditorConfig overrides any parent EditorConfigs 5 | root = true 6 | 7 | # Default rules applied to all file types 8 | [*] 9 | 10 | # No trailing spaces, newline at EOF 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | end_of_line = lf 15 | 16 | # 2 space indentation 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # JavaScript-specific settings 21 | [*.{js,ts}] 22 | quote_type = double 23 | continuation_indent_size = 2 24 | curly_bracket_next_line = false 25 | indent_brace_style = BSD 26 | spaces_around_operators = true 27 | spaces_around_brackets = none 28 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # ESLint config 2 | # http://eslint.org/docs/user-guide/configuring 3 | # https://jstools.dev/eslint-config/ 4 | 5 | root: true 6 | extends: "@jsdevtools" 7 | globals: 8 | TextEncoder: false 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git attributes 2 | # https://git-scm.com/docs/gitattributes 3 | # https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes 4 | 5 | # Normalize line endings for all files that git determines to be text. 6 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto 7 | * text=auto 8 | 9 | # Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows. 10 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf 11 | *.txt text eol=lf 12 | *.html text eol=lf 13 | *.md text eol=lf 14 | *.css text eol=lf 15 | *.scss text eol=lf 16 | *.map text eol=lf 17 | *.js text eol=lf 18 | *.jsx text eol=lf 19 | *.ts text eol=lf 20 | *.tsx text eol=lf 21 | *.json text eol=lf 22 | *.yml text eol=lf 23 | *.yaml text eol=lf 24 | *.xml text eol=lf 25 | *.svg text eol=lf 26 | -------------------------------------------------------------------------------- /.github/workflows/CI-CD.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions 3 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 4 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions 5 | 6 | name: CI-CD 7 | 8 | on: 9 | push: 10 | branches: 11 | - "*" 12 | tags-ignore: 13 | - "*" 14 | 15 | schedule: 16 | - cron: "0 0 1 * *" 17 | 18 | jobs: 19 | node_tests: 20 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 21 | runs-on: ${{ matrix.os }} 22 | timeout-minutes: 10 23 | strategy: 24 | fail-fast: true 25 | matrix: 26 | os: 27 | - ubuntu-latest 28 | - macos-latest 29 | - windows-latest 30 | node: 31 | - 10 32 | - 12 33 | - 14 34 | 35 | steps: 36 | - name: Checkout source 37 | uses: actions/checkout@v2 38 | 39 | - name: Install Node ${{ matrix.node }} 40 | uses: actions/setup-node@v1 41 | with: 42 | node-version: ${{ matrix.node }} 43 | 44 | - name: Install dependencies 45 | run: npm ci 46 | 47 | - name: Run linter 48 | run: npm run lint 49 | 50 | - name: Build the code 51 | run: npm run build 52 | 53 | - name: Run tests 54 | run: npm run coverage:node 55 | 56 | - name: Send code coverage results to Coveralls 57 | uses: coverallsapp/github-action@v1.1.0 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | parallel: true 61 | 62 | browser_tests: 63 | name: Browser Tests 64 | runs-on: ${{ matrix.os }} 65 | timeout-minutes: 10 66 | strategy: 67 | fail-fast: true 68 | matrix: 69 | os: 70 | - ubuntu-latest # Chrome, Firefox, Safari (via SauceLabs), Edge (via SauceLabs) 71 | - windows-latest # Chrome on Windows 72 | 73 | steps: 74 | - name: Checkout source 75 | uses: actions/checkout@v2 76 | 77 | - name: Install Node 78 | uses: actions/setup-node@v1 79 | with: 80 | node-version: 12 81 | 82 | - name: Install dependencies 83 | run: npm ci 84 | 85 | - name: Build the code 86 | run: npm run build 87 | 88 | - name: Run tests 89 | run: npm run coverage:browser 90 | env: 91 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 92 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 93 | 94 | - name: Combine code coverage data into a single file 95 | shell: bash 96 | run: | 97 | ls -Rlh coverage/*/lcov.info 98 | cat coverage/*/lcov.info > ./coverage/lcov.info 99 | 100 | - name: Send code coverage results to Coveralls 101 | uses: coverallsapp/github-action@v1.1.0 102 | with: 103 | github-token: ${{ secrets.GITHUB_TOKEN }} 104 | parallel: true 105 | 106 | coverage: 107 | name: Code Coverage 108 | runs-on: ubuntu-latest 109 | timeout-minutes: 10 110 | needs: 111 | - node_tests 112 | - browser_tests 113 | steps: 114 | - name: Let Coveralls know that all tests have finished 115 | uses: coverallsapp/github-action@v1.1.0 116 | with: 117 | github-token: ${{ secrets.GITHUB_TOKEN }} 118 | parallel-finished: true 119 | 120 | deploy: 121 | name: Publish to NPM 122 | if: github.ref == 'refs/heads/master' 123 | runs-on: ubuntu-latest 124 | timeout-minutes: 10 125 | needs: 126 | - node_tests 127 | - browser_tests 128 | 129 | steps: 130 | - name: Checkout source 131 | uses: actions/checkout@v2 132 | 133 | - name: Install Node 134 | uses: actions/setup-node@v1 135 | 136 | - name: Install dependencies 137 | run: npm ci 138 | 139 | - name: Build the code 140 | run: npm run build 141 | 142 | - name: Publish to NPM 143 | uses: JS-DevTools/npm-publish@v1 144 | with: 145 | token: ${{ secrets.NPM_TOKEN }} 146 | access: public 147 | tag: alpha 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git ignore 2 | # https://git-scm.com/docs/gitignore 3 | 4 | # Private files 5 | .env 6 | 7 | # Miscellaneous 8 | *~ 9 | *# 10 | .DS_STORE 11 | Thumbs.db 12 | .netbeans 13 | nbproject 14 | .node_history 15 | 16 | # IDEs & Text Editors 17 | .idea 18 | .sublime-* 19 | .vscode/settings.json 20 | .netbeans 21 | nbproject 22 | 23 | # Temporary files 24 | .tmp 25 | .temp 26 | .grunt 27 | .lock-wscript 28 | 29 | # Logs 30 | /logs 31 | *.log 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | 38 | # Dependencies 39 | node_modules 40 | 41 | # Build output 42 | /cjs 43 | /esm 44 | 45 | # Test output 46 | /.nyc_output 47 | /coverage 48 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | # Mocha options 2 | # https://mochajs.org/#configuring-mocha-nodejs 3 | # https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml 4 | 5 | spec: 6 | - test/specs/*spec.js # Run the tests in the root folder first 7 | - test/specs/**/*spec.js # ...then run the tests in subfolders 8 | bail: true 9 | recursive: true 10 | require: source-map-support/register 11 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | # NYC config 2 | # https://github.com/istanbuljs/nyc#configuration-files 3 | 4 | extension: 5 | - .js 6 | - .ts 7 | 8 | reporter: 9 | - text 10 | - lcov 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // VSCode Launch Configuration 2 | // https://code.visualstudio.com/docs/editor/debugging#_launch-configurations 3 | 4 | // Available variables which can be used inside of strings. 5 | // ${workspaceRoot}: the root folder of the team 6 | // ${file}: the current opened file 7 | // ${fileBasename}: the current opened file's basename 8 | // ${fileDirname}: the current opened file's dirname 9 | // ${fileExtname}: the current opened file's extension 10 | // ${cwd}: the current working directory of the spawned process 11 | 12 | { 13 | "version": "0.2.0", 14 | "configurations": [ 15 | { 16 | "name": "Run Mocha", 17 | "type": "node", 18 | "runtimeArgs": [ 19 | "--nolazy" 20 | ], 21 | "request": "launch", 22 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 23 | "stopOnEntry": false, 24 | "args": [ 25 | "--quick-test", 26 | "--timeout=600000", 27 | "--retries=0", 28 | ], 29 | "env": { 30 | "NODE_ENV": "test" 31 | }, 32 | "cwd": "${workspaceRoot}", 33 | "console": "internalConsole", 34 | "sourceMaps": true, 35 | "outFiles": [ 36 | "${workspaceRoot}/cjs/**/*.js", 37 | ], 38 | "smartStep": true, 39 | "skipFiles": [ 40 | "/**/*.js" 41 | ], 42 | }, 43 | 44 | { 45 | "name": "Run Karma", 46 | "type": "node", 47 | "runtimeArgs": [ 48 | "--nolazy" 49 | ], 50 | "request": "launch", 51 | "program": "${workspaceRoot}/node_modules/karma/bin/karma", 52 | "stopOnEntry": false, 53 | "args": [ 54 | "start", 55 | "--single-run" 56 | ], 57 | "env": { 58 | "NODE_ENV": "test" 59 | }, 60 | "cwd": "${workspaceRoot}", 61 | "console": "internalConsole", 62 | "outputCapture": "std", 63 | "sourceMaps": false, 64 | "outFiles": [ 65 | "${workspaceRoot}/esm/**/*.js", 66 | ], 67 | "smartStep": true, 68 | "skipFiles": [ 69 | "/**/*.js" 70 | ], 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // VSCode Tasks 2 | // https://code.visualstudio.com/docs/editor/tasks 3 | 4 | // Available variables which can be used inside of strings. 5 | // ${workspaceRoot}: the root folder of the team 6 | // ${file}: the current opened file 7 | // ${fileBasename}: the current opened file's basename 8 | // ${fileDirname}: the current opened file's dirname 9 | // ${fileExtname}: the current opened file's extension 10 | // ${cwd}: the current working directory of the spawned process 11 | 12 | { 13 | "version": "2.0.0", 14 | "command": "npm", 15 | "tasks": [ 16 | { 17 | "type": "npm", 18 | "script": "build", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "problemMatcher": "$tsc" 24 | }, 25 | 26 | 27 | { 28 | "type": "npm", 29 | "script": "test:node", 30 | "group": { 31 | "kind": "test", 32 | "isDefault": true 33 | }, 34 | }, 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: 404 3 | --- 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 James Messinger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON Schema Reader 2 | ============================================== 3 | ### Reads multi-file JSON Schemas from files, URLs, and other sources 4 | 5 | [![npm](https://img.shields.io/npm/v/@apidevtools/json-schema-reader.svg)](https://www.npmjs.com/package/@apidevtools/json-schema-reader) 6 | [![License](https://img.shields.io/npm/l/@apidevtools/json-schema-reader.svg)](LICENSE) 7 | [![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://plant.treeware.earth/APIDevTools/json-schema-reader) 8 | 9 | [![Build Status](https://github.com/APIDevTools/json-schema-reader/workflows/CI-CD/badge.svg)](https://github.com/APIDevTools/json-schema-reader/actions) 10 | [![Coverage Status](https://coveralls.io/repos/github/APIDevTools/json-schema-reader/badge.svg?branch=master)](https://coveralls.io/github/APIDevTools/json-schema-reader) 11 | [![Dependencies](https://david-dm.org/APIDevTools/json-schema-reader.svg)](https://david-dm.org/APIDevTools/json-schema-reader) 12 | 13 | [![OS and Browser Compatibility](https://apitools.dev/img/badges/ci-badges.svg)](https://github.com/APIDevTools/json-schema-reader/actions) 14 | 15 | 16 | 17 | Features 18 | -------------------------- 19 | - 🔱 **Read files from anywhere**
20 | Reads JSON Schemas from local files, network files, and web URLs by default. Can be extended to read from a database, FTP server, CMS, or anything else! 21 | 22 | - 🗃 **Multi-file schemas**
23 | Split your schemas into as many files as you want, and use `$ref` to link between them. 24 | 25 | - 📃 **Broad compatibility**
26 | Supports multiple versions of the JSON Schema spec, including the latest **2019-09 draft**, and can even auto-detect the correct version. 27 | 28 | - 🏷**Supports any file type**
29 | Supports JSON, plain-text, and binary files by default, but can be extended to support YAML, TOML, XML, or any other file type. 30 | 31 | - 🧩 **Fully customizable**
32 | Allows you to extend/override any part of the process. Add support for additional file locations, file types, syntaxes, schema versions, etc. 33 | 34 | - 🧪 **Thoroughly tested**
35 | Tested on **[over 1,500 real-world schemas](https://apis.guru/browse-apis/)** from Google, Microsoft, Facebook, Spotify, etc. 36 | 37 | 38 | 39 | Example 40 | -------------------------- 41 | This example demonstrates reading a multi-file JSON Schema. The root file (`schema.json`) contains two `$ref`s that link to `address.json`. One of the `$ref`s use a JSON Pointer path (`#/$defs/Name`), and the other a named anchor (`#address`). 42 | 43 | **schema.json** 44 | ```json 45 | { 46 | "$schema": "https://json-schema.org/draft/2019-09/schema", 47 | "title": "Person", 48 | "properties": { 49 | "name": { "$ref": "types.json#/$defs/Name" }, 50 | "address": { "$ref": "types.json#address" } 51 | } 52 | } 53 | ``` 54 | 55 | **types.json** 56 | ```json 57 | { 58 | "$defs": { 59 | "Name": { 60 | "title": "Name", 61 | "properties": { 62 | "first": { "type": "string" }, 63 | "last": { "type": "string" } 64 | } 65 | }, 66 | "Address": { 67 | "$anchor": "address", 68 | "title": "Address", 69 | "properties": { 70 | "street": { "type": "string" }, 71 | "city": { "type": "string" }, 72 | "postalCode": { "type": "string" } 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | **script.js** 80 | 81 | ```javascript 82 | const { readJsonSchema } = require("@apidevtools/json-schema-reader"); 83 | 84 | (async () => { 85 | // Read the schema, and all referenced files 86 | let schema = await readJsonSchema("schema.json"); 87 | 88 | // List all the files in the schema 89 | console.log(schema.files.length); // 2 90 | console.log(schema.rootFile.path); // schema.json 91 | console.log(schema.files[1].path); // types.json 92 | 93 | // Inspect the $refs in schema.json 94 | let refs = [...schema.rootFile.references]; 95 | 96 | console.log(refs[0].locationInFile.path); // /properties/name 97 | console.log(refs[0].value); // types.json#/$defs/Name 98 | console.log(refs[0].targetURI.href); // /path/to/types.json#/$defs/Name 99 | console.log(refs[0].resolve().data); // { title: "Name", properties: {...}} 100 | 101 | console.log(refs[1].locationInFile.path); // /properties/address 102 | console.log(refs[1].value); // types.json#address 103 | console.log(refs[1].targetURI.href); // /path/to/types.json#address 104 | console.log(refs[1].resolve().data); // { title: "Address", properties: {...}} 105 | })(); 106 | ``` 107 | 108 | 109 | 110 | Installation 111 | -------------------------- 112 | You can install JSON Schema Reader via [npm](https://docs.npmjs.com/about-npm/). 113 | 114 | ```bash 115 | npm install @apidevtools/json-schema-reader 116 | ``` 117 | 118 | 119 | 120 | Usage 121 | -------------------------- 122 | When using JSON Schema Reader in Node.js apps, you'll probably want to use **CommonJS** syntax: 123 | 124 | ```javascript 125 | const { readJsonSchema } = require("@apidevtools/json-schema-reader"); 126 | ``` 127 | 128 | When using a transpiler such as [Babel](https://babeljs.io/) or [TypeScript](https://www.typescriptlang.org/), or a bundler such as [Webpack](https://webpack.js.org/) or [Rollup](https://rollupjs.org/), you can use **ECMAScript modules** syntax instead: 129 | 130 | ```javascript 131 | import { readJsonSchema } from "@apidevtools/json-schema-reader"; 132 | ``` 133 | 134 | 135 | ### `readJsonSchema(location, [options])` 136 | This is an async function that reads your JSON Schema from a file path or URL. If the schema contains any `$ref`s to other files and/or URLs, then they are automatically read/downloaded as well. 137 | 138 | #### `location` (string or [URL object](https://developer.mozilla.org/en-US/docs/Web/API/URL)) 139 | This is the location of the root file of your JSON Schema. When running in Node.js, it can be an absolute or relative filesystem path, or a URL. When running in a web browser, it can be an absolute or relative URL. 140 | 141 | #### `options` (_optional_, [`Options` object](docs/options.md)) 142 | You can pass an [`Options` object](doc/options) as the second parameter to customize the behavior of the `readJsonSchema()` function. 143 | 144 | #### Return value ([`JsonSchema` object](docs/json-schema.md)) 145 | This is an async function, so the return value is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), but the Promise resolves to a [`JsonSchema` object](docs/json-schema.md), which contains details about all the files, resources, anchors, and references in the schema. 146 | 147 | 148 | ### Documentation 149 | - [Understanding the structure of a schema](docs/schema-structure.md) 150 | - [JsonSchema](docs/json-schema.md) 151 | - [File](docs/file.md) 152 | - [Resource](docs/resource.md) 153 | - [Anchor](docs/anchor.md) 154 | - [Reference](docs/reference.md) 155 | - [Pointer](docs/pointer.md) 156 | - [Options / Customization](docs/options.md) 157 | - [Reading files from other sources](docs/read-file.md) 158 | - [Supporting other file types](docs/parse-file.md) 159 | - [Error handling](docs/error-handling.md) 160 | 161 | 162 | 163 | Browser support 164 | -------------------------- 165 | JSON Schema Reader supports recent versions of every major web browser. Older browsers may require [Babel](https://babeljs.io/) and/or [polyfills](https://babeljs.io/docs/en/next/babel-polyfill). 166 | 167 | To use JSON Schema Reader in a browser, you'll need to use a bundling tool such as [Webpack](https://webpack.js.org/), [Rollup](https://rollupjs.org/), [Parcel](https://parceljs.org/), or [Browserify](http://browserify.org/). Some bundlers may require a bit of configuration, such as setting `browser: true` in [rollup-plugin-resolve](https://github.com/rollup/rollup-plugin-node-resolve). 168 | 169 | 170 | 171 | Contributing 172 | -------------------------- 173 | Contributions, enhancements, and bug-fixes are welcome! [Open an issue](https://github.com/APIDevTools/json-schema-reader/issues) on GitHub and [submit a pull request](https://github.com/APIDevTools/json-schema-reader/pulls). 174 | 175 | #### Building 176 | To build the project locally on your computer: 177 | 178 | 1. __Clone this repo__
179 | `git clone https://github.com/APIDevTools/json-schema-reader.git` 180 | 181 | 2. __Install dependencies__
182 | `npm install` 183 | 184 | 3. __Build the code__
185 | `npm run build` 186 | 187 | 4. __Run the tests__
188 | `npm test` 189 | 190 | 191 | 192 | License 193 | -------------------------- 194 | JSON Schema Reader is 100% free and open-source, under the [MIT license](LICENSE). Use it however you want. 195 | 196 | This package is [Treeware](http://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/APIDevTools/json-schema-reader) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats. 197 | 198 | 199 | 200 | Big Thanks To 201 | -------------------------- 202 | Thanks to these awesome companies for their support of Open Source developers ❤ 203 | 204 | [![GitHub](https://apitools.dev/img/badges/github.svg)](https://github.com/open-source) 205 | [![NPM](https://apitools.dev/img/badges/npm.svg)](https://www.npmjs.com/) 206 | [![Coveralls](https://apitools.dev/img/badges/coveralls.svg)](https://coveralls.io) 207 | [![Travis CI](https://apitools.dev/img/badges/travis-ci.svg)](https://travis-ci.com) 208 | [![SauceLabs](https://apitools.dev/img/badges/sauce-labs.svg)](https://saucelabs.com) 209 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: APIDevTools/gh-pages-theme 2 | 3 | title: JSON Schema Reader 4 | logo: https://apitools.dev/img/logos/logo.png 5 | 6 | author: 7 | twitter: APIDevTools 8 | 9 | google_analytics: UA-68102273-2 10 | 11 | twitter: 12 | username: APIDevTools 13 | card: summary 14 | 15 | defaults: 16 | - scope: 17 | path: "" 18 | values: 19 | image: https://apitools.dev/img/logos/card.png 20 | - scope: 21 | path: "test/**/*" 22 | values: 23 | sitemap: false 24 | 25 | plugins: 26 | - jekyll-sitemap 27 | -------------------------------------------------------------------------------- /docs/anchor.md: -------------------------------------------------------------------------------- 1 | `Anchor` Object 2 | =========================== 3 | An `Anchor` object represents a [JSON Schema anchor](http://json-schema.org/draft/2019-09/json-schema-core.html#anchor) (i.e. `$anchor`) in a [resource](resource.md). 4 | 5 | > **TIP:** Read [Understanding Schema Structure](schema-structure.md) to learn how all the different objects in our object model are related. 6 | 7 | 8 | Source Code 9 | ---------------- 10 | You can view the source code for the `JsonSchema` object [here](https://github.com/APIDevTools/json-schema/blob/master/src/anchor.ts). 11 | 12 | 13 | 14 | Properties 15 | ---------------------- 16 | 17 | ### `schema` ([`JsonSchema` object](json-schema.md)) 18 | This is the `JsonSchema` object that contains this anchor. 19 | 20 | ```javascript 21 | if (anchor.schema.rootResource === anchor.resource) { 22 | console.log("This anchor is in the root resource of the schema"); 23 | } 24 | ``` 25 | 26 | 27 | ### `file` ([`File` object](file.md)) 28 | This is the `File` object that contains this anchor. 29 | 30 | ```javascript 31 | console.log(`This anchor is in ${anchor.file.path}`); 32 | ``` 33 | 34 | 35 | ### `resource` ([`Resource` object](resource.md)) 36 | This is the `Resource` object that contains this anchor. 37 | 38 | ```javascript 39 | console.log(`This anchor is in ${anchor.resource.uri.href}`); 40 | ``` 41 | 42 | 43 | 44 | > ### ⚠ INCOMPLETE DOCS ⚠ 45 | > There's more to this object, but the rest of this page has not yet been written. 46 | -------------------------------------------------------------------------------- /docs/error-handling.md: -------------------------------------------------------------------------------- 1 | Error Handling 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/file.md: -------------------------------------------------------------------------------- 1 | `File` Object 2 | =========================== 3 | The [`JsonSchema` object](json-schema.md) contains an array of `File` objects — one for each physical file on disk or URL that was downloaded. 4 | 5 | > **TIP:** Read [Understanding Schema Structure](schema-structure.md) to learn how all the different objects in our object model are related. 6 | 7 | 8 | Source Code 9 | ---------------- 10 | You can view the source code for the `JsonSchema` object [here](https://github.com/APIDevTools/json-schema/blob/master/src/file.ts). 11 | 12 | 13 | 14 | Properties 15 | ---------------------- 16 | 17 | ### `schema` ([`JsonSchema` object](json-schema.md)) 18 | This is the `JsonSchema` object to which the file belongs. 19 | 20 | ```javascript 21 | if (file.schema.rootFile === file) { 22 | console.log("This is the root file of the schema"); 23 | } 24 | ``` 25 | 26 | 27 | ### `url` ([`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL)) 28 | The absolute URL of the file, parsed into its constituent parts. For filesystem files, this will be a `file://` URL. 29 | 30 | ```javascript 31 | console.log(file.url.href); 32 | ``` 33 | 34 | 35 | ### `path` (string) 36 | The absolute or relative path of the file, based on the root path or URL that was provided when you called the `readJsonSchema()` function. The intent is for this to be a shorter, more user-friendly path that can be used in user-facing messages. 37 | 38 | ```javascript 39 | const { readJsonSchema } = require("@apidevtools/json-schema-reader"); 40 | 41 | (async () => { 42 | // Notice that we're using a relative path when calling readJsonSchema(). 43 | // All file paths will be based on this. 44 | let schema = await readJsonSchema("schemas/my-schema.json"); 45 | 46 | // The full, absolute URLs 47 | console.log(schema.rootFile.url.href); // file://absolute/path/to/schemas/my-schema.json 48 | console.log(schema.files[1].url.href); // file://absolute/path/to/schemas/some-referenced-file.json 49 | 50 | // The nice, short, user-friendly paths 51 | console.log(schema.rootFile.path); // schemas/my-schema.json 52 | console.log(schema.files[1].path); // schemas/some-referenced-file.json 53 | 54 | })(); 55 | ``` 56 | 57 | 58 | ### `mediaType` (string) 59 | The [IANA media type](https://www.iana.org/assignments/media-types/media-types.xhtml) of the file (e.g. `application/json`, `text/yaml`, etc.). This is used to determine how to parse the file's data. 60 | 61 | JSON Schema Reader sets this property based on the `Content-Type` header of downloaded files, and based on the file extension of local files. 62 | 63 | ```javascript 64 | if (file.mediaType === "text/yaml") { 65 | console.log("This is a YAML file"); 66 | } 67 | ``` 68 | 69 | 70 | ### `metadata` (object) 71 | This object contains miscellaneous metadata about the file. JSON Schema Reader populates this object differently, depending on where the file was raed from. For example, local filesystem files will contain all the properties of [`FS.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats), whereas files that were downloaded will contain HTTP headers. 72 | 73 | To simplify things, JSON Schema Reader adds some metadata in the form of HTTP headers, even for filesystem files: 74 | 75 | | Metadata property name | Value 76 | |------------------------|---------------------------------------------- 77 | | `content-location` | The absolute file path 78 | | `content-type` | The [IANA media type](https://www.iana.org/assignments/media-types/media-types.xhtml) of the file, based on its file extension 79 | | `content-length` | The file size (in bytes) 80 | | `last-modified` | The date/time that the file was last modified, in [UTC String format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString) 81 | 82 | You can also add your own metadata to this object. Some JSON Schema Reader plugins may add their own data as well. 83 | 84 | ```javascript 85 | for (let [key, value] of Object.entries(file.metadata)) { 86 | console.log(`${key} = ${value}`); 87 | } 88 | ``` 89 | 90 | 91 | ### `resources` (array of [`Resource` objects](resource.md)) 92 | This is an array of all the [JSON Schema resources](http://json-schema.org/draft/2019-09/json-schema-core.html#id-keyword) in the file. See [Understanding Schema Structure](schema-structure.md) to learn more about resources. 93 | 94 | ```javascript 95 | for (let resource of file.resources) { 96 | console.log(resource.uri.href); 97 | } 98 | ``` 99 | 100 | 101 | ### `rootResource` ([`Resource` object](resource.md)) 102 | The [root resource](http://json-schema.org/draft/2019-09/json-schema-core.html#root) of the file. Every file has a root resource, and some files _only_ have a root resource. 103 | 104 | ```javascript 105 | console.log(file.rootResource.uri.href); 106 | ``` 107 | 108 | 109 | ### `errors` (array of [`SchemaError` objects](schema-error.md)) 110 | The errors that were encountered in this file, if any. 111 | 112 | ```javascript 113 | for (let error of file.errors) { 114 | console.error(error.message); 115 | } 116 | ``` 117 | 118 | 119 | ### `data` (anything) 120 | The file data. This can be _any_ JavaScript value, but will usually be one of the following: 121 | 122 | - `object`
123 | If the file is a JSON Schema document that has already been parsed. 124 | 125 | - `string`
126 | If the file is in a text-based file that has not yet been parsed. 127 | This includes JSON, YAML, HTML, SVG, CSV, plain-text, etc. 128 | 129 | - `ArrayBuffer`
130 | If the file contains binary data, such as an image. 131 | 132 | > **NOTE:** This is actually just a convenience property that points to the `data` property of the root [`Resource` object](resource.md) 133 | 134 | ```javascript 135 | file.data = { 136 | $id: "person", 137 | title: "Person", 138 | properties: { 139 | name: { type: "string" }, 140 | age: { type: "integer" }, 141 | } 142 | }; 143 | ``` 144 | 145 | 146 | ### `anchors` (iterable of [`Anchor` objects](anchor.md)) 147 | Iterates over all the [JSON Schema anchors](http://json-schema.org/draft/2019-09/json-schema-core.html#anchor) (i.e. `$anchor`) in every resource in the file. 148 | 149 | ```javascript 150 | for (let anchor of file.anchors) { 151 | console.log(anchor.uri.href); 152 | } 153 | ``` 154 | 155 | 156 | ### `references` (iterable of [`Reference` objects](reference.md)) 157 | Iterates over all the [JSON Schema references](http://json-schema.org/draft/2019-09/json-schema-core.html#ref) (i.e. `$ref`) in every resource in the file. 158 | 159 | ```javascript 160 | for (let ref of file.references) { 161 | console.log(`${ref.locationInFile.path} points to ${ref.targetURI.href}`); 162 | } 163 | ``` 164 | 165 | 166 | 167 | Methods 168 | ---------------------- 169 | 170 | ### `hasResource(uri)` 171 | Determines whether the specified [resource](schema-structure.md#resources) exists in the file. 172 | 173 | - `uri` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
174 | The absolute, canonical URI of the resource to check for. 175 | 176 | - Return value (boolean) 177 | 178 | ```javascript 179 | if (file.hasResource("http://example.com/schemas/person")) { 180 | console.log("Found the person resource"); 181 | } 182 | ``` 183 | 184 | 185 | ### `getResource(uri)` 186 | Returns the specified [resource](schema-structure.md#resources) in the file. 187 | 188 | - `uri` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
189 | The absolute, canonical URI of the resource to return. 190 | 191 | - Return value (boolean) 192 | 193 | ```javascript 194 | let person = file.getResource("http://example.com/schemas/person"); 195 | 196 | if (person) { 197 | console.log(person.data); 198 | } 199 | ``` 200 | 201 | 202 | ### `index([versionNumber])` 203 | Re-indexes the file's contents. That is, it re-populates the `resources`, `anchors`, and `references` in the file. 204 | 205 | This method is useful if you edit the file's contents (e.g. the `file.rootResource.data` property) in a way that changes the resources, anchors, or references in it. 206 | 207 | - `versionNumber` (optional string)
208 | The [JSON Schema version number](http://json-schema.org/specification-links.html#table-of-all-versions-of-everything) to use (e.g. `draft-04`, `2019-09`, `latest`, `auto`). The version number determines which keywords are supported, URI resolution rules, and other aspects of indexing. The default is `auto`, which attempts to automatically determine the version number via the `$schema` keyword. An error will be thrown if there is no `$schema` keyword. 209 | 210 | - Return value (`File` object)
211 | The `index()` method updates the `File` object and all of its resources, anchors, and references in-place. The same `File` instance is returned to allow for chaining. 212 | 213 | ```javascript 214 | // Index using auto-detected version 215 | file.index(); 216 | 217 | // Index using a specific version 218 | file.index("2019-09"); 219 | ``` 220 | 221 | 222 | ### `isFile(value)` 223 | This is a static method of the `File` class. It determines whether the given value is a `File` instance. Simply using `instanceof File` is insufficient, since there may be multiple versions of the `@apidevtools/json-schema` package in the `node_modules` folder and thus multiple `File` classes in memory. 224 | 225 | - `value` (any value)
226 | The thing that you suspect might be a `File` object 227 | 228 | - Return value (boolean)
229 | Returns `true` if `value instanceof File` or if `value` is an object with the same structure as a `File` object (i.e. ["duck typing"](https://en.wikipedia.org/wiki/Duck_typing)). 230 | 231 | ```javascript 232 | if (File.isFile(something)) { 233 | // It's IS a File object, so it's safe to use 234 | console.log(something.rootResource.uri); 235 | } 236 | ``` 237 | -------------------------------------------------------------------------------- /docs/json-schema.md: -------------------------------------------------------------------------------- 1 | `JsonSchema` Object 2 | =========================== 3 | JSON Schema Reader returns a `JsonSchema` object, which contains details about all the files, resources, anchors, and references in the schema. 4 | 5 | > **TIP:** Read [Understanding Schema Structure](schema-structure.md) to learn how all the different objects in our object model are related. 6 | 7 | 8 | Source Code 9 | ---------------- 10 | You can view the source code for the `JsonSchema` object [here](https://github.com/APIDevTools/json-schema/blob/master/src/json-schema.ts). 11 | 12 | 13 | 14 | Properties 15 | ---------------------- 16 | 17 | ### `files` (array of [`File` objects](file.md)) 18 | This is an array of all the files in the schema, including the root file and any files referenced by `$ref` pointers. 19 | 20 | ```javascript 21 | for (let file of schema.files) { 22 | console.log(file.path); 23 | } 24 | ``` 25 | 26 | 27 | ### `rootFile` ([`File` object](file.md)) 28 | The root file of the schema. This is the first file that was read. All other files are referenced either directly or indirectly by this file. 29 | 30 | ```javascript 31 | console.log(schema.rootFile.path); 32 | ``` 33 | 34 | 35 | ### `rootResource` ([`Resource` object](resource.md)) 36 | The [root resource](http://json-schema.org/draft/2019-09/json-schema-core.html#root) of the root file. This is the easiest way to access the top-level schema data. 37 | 38 | ```javascript 39 | console.log(schema.rootResoure.uri); 40 | ``` 41 | 42 | 43 | ### `resources` (iterable of [`Resource` objects](resource.md)) 44 | Iterates over every resource in every file of the schema. 45 | 46 | ```javascript 47 | for (let resource of schema.resources) { 48 | console.log(resource.uri.href); 49 | } 50 | ``` 51 | 52 | 53 | ### `hasErrors` (boolean) 54 | Indicates whether there are any errors in any file in the schema. 55 | 56 | ```javascript 57 | if (schema.hasErrors) { 58 | console.warn("There were errors!"); 59 | } 60 | ``` 61 | 62 | 63 | ### `errors` (iterable of [`SchemaError` objects](schema-error.md)) 64 | Iterates over every error in every file of the schema. 65 | 66 | ```javascript 67 | for (let error of schema.errors) { 68 | console.error(error.message); 69 | } 70 | ``` 71 | 72 | 73 | Methods 74 | ---------------------- 75 | 76 | ### `hasFile(location)` 77 | Determines whether the specified file is in the schema. 78 | 79 | - `location` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
80 | The absolute or relative path or URL of the file to check for. 81 | 82 | - Return value (boolean) 83 | 84 | ```javascript 85 | if (schema.hasFile("subdir/some-file.json")) { 86 | console.log("Found it!"); 87 | } 88 | ``` 89 | 90 | 91 | ### `getFile(location)` 92 | Returns the specified file in the schema. 93 | 94 | - `location` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
95 | The absolute or relative path or URL of the file to return. 96 | 97 | - Return value ([`File` object](file.md) or `undefined`) 98 | 99 | ```javascript 100 | let file = schema.getFile("subdir/some-file.json"); 101 | 102 | if (file) { 103 | console.log(file.path); 104 | } 105 | ``` 106 | 107 | 108 | ### `hasResource(uri)` 109 | Determines whether the specified [resource](schema-structure.md#resources) exists in the schema. 110 | 111 | - `uri` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
112 | The absolute, canonical URI of the resource to check for. Note that whereas the `hasFile()` method accepts relative paths, filesystem paths, or URLs, the `hasResource()` method _only_ accepts the full canonical resource URI. 113 | 114 | - Return value (boolean) 115 | 116 | ```javascript 117 | if (schema.hasResource("http://example.com/schemas/person")) { 118 | console.log("Found the person resource"); 119 | } 120 | ``` 121 | 122 | 123 | ### `getResource(uri)` 124 | Returns the specified [resource](schema-structure.md#resources) in the schema. 125 | 126 | - `uri` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
127 | The absolute, canonical URI of the resource to return. Note that whereas the `getFile()` method accepts relative paths, filesystem paths, or URLs, the `getResource()` method _only_ accepts the full canonical resource URI. 128 | 129 | - Return value (boolean) 130 | 131 | ```javascript 132 | let person = schema.getResource("http://example.com/schemas/person"); 133 | 134 | if (person) { 135 | console.log(person.data); 136 | } 137 | ``` 138 | 139 | 140 | ### `index([versionNumber])` 141 | Re-indexes the schema's contents. That is, it scans all files and records all JSON Schema resources, anchors, and references in them. 142 | 143 | This method is useful if you edit the schema's contents (e.g. the `schema.rootResource.data` property) in a way that changes the resources, anchors, or references in it. 144 | 145 | - `versionNumber` (optional string)
146 | The [JSON Schema version number](http://json-schema.org/specification-links.html#table-of-all-versions-of-everything) to use (e.g. `draft-04`, `2019-09`, `latest`, `auto`). The version number determines which keywords are supported, URI resolution rules, and other aspects of indexing. The default is `auto`, which attempts to automatically determine the version number via the `$schema` keyword. An error will be thrown if there is no `$schema` keyword. 147 | 148 | - Return value (`JsonSchema` object)
149 | The `index()` method updates the `JsonSchema` object and all of its files, resources, anchors, and references in-place. The same `JsonSchema` instance is returned to allow for chaining. 150 | 151 | ```javascript 152 | // Index using auto-detected version 153 | schema.index(); 154 | 155 | // Index using a specific version 156 | schema.index("2019-09"); 157 | ``` 158 | 159 | 160 | ### `resolve(uri)` 161 | Resolves a URI to a value in the schema. This method can return any value at any location in any file in the schema. It works the same way as the `$ref` keyword. 162 | 163 | - `uri` (string or [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL))
164 | An absolute or relative URI (relative to the root of the schema). The URI can be a file URL, a resource URI, an anchor URI, or a URI with a JSON Pointer fragment. That is, it can be anything that would be valid in a `$ref`. See [Schema identification examples](http://json-schema.org/draft/2019-09/json-schema-core.html#idExamples) for examples. 165 | 166 | - Return value ([`Resolution` object](resolution.md))
167 | The returned object contains the resolved value, information about the value's location, and details about how the value was resolved, including every `$ref` that was followed along the way. 168 | 169 | ```javascript 170 | // Find a file in the schema via its URL 171 | let file = schema.resolve("some-file.json"); 172 | 173 | // Find a resource in the schema via its URI 174 | let person = schema.resolve("person"); 175 | 176 | // Find a sub-schema using an $anchor 177 | let address = schema.resolve("#address"); 178 | 179 | // Get a specific value in the schema using a JSON Pointer 180 | let value = schema.resolve("#/$defs/address/properties/city"); 181 | 182 | // Show the resolved value 183 | console.log(value.data); 184 | 185 | // Show where it was found 186 | console.log(value.file.path, value.locationInFile.path); 187 | ``` 188 | 189 | 190 | ### `isJsonSchema(value)` 191 | This is a static method of the `JsonSchema` class. It determines whether the given value is a `JsonSchema` instance. Simply using `instanceof JsonSchema` is insufficient, since there may be multiple versions of the `@apidevtools/json-schema` package in the `node_modules` folder and thus multiple `JsonSchema` classes in memory. 192 | 193 | - `value` (any value)
194 | The thing that you suspect might be a `JsonSchema` object 195 | 196 | - Return value (boolean)
197 | Returns `true` if `value instanceof JsonSchema` or if `value` is an object with the same structure as a `JsonSchema` object (i.e. ["duck typing"](https://en.wikipedia.org/wiki/Duck_typing)). 198 | 199 | ```javascript 200 | if (JsonSchema.isJsonSchema(something)) { 201 | // It's IS a JsonSchema object, so it's safe to use 202 | console.log(something.rootResource.uri); 203 | } 204 | ``` 205 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | Options 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/parse-file.md: -------------------------------------------------------------------------------- 1 | Parsing Files 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/pointer.md: -------------------------------------------------------------------------------- 1 | `Pointer` Object 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/read-file.md: -------------------------------------------------------------------------------- 1 | Reading Files 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | `Reference` Objecct 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/resolution.md: -------------------------------------------------------------------------------- 1 | Resolving Resources 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/resource.md: -------------------------------------------------------------------------------- 1 | `Resource` Object 2 | =========================== 3 | Every [`File`](file.md) in a [`JsonSchema` object](json-schema.md) contains an array of `Resource` objects. Each `Resource` object represents a [JSON Schema Resource](http://json-schema.org/draft/2019-09/json-schema-core.html#id-keyword) in the file. Every file has at least one resource. 4 | 5 | > **TIP:** Read [Understanding Schema Structure](schema-structure.md) to learn how all the different objects in our object model are related. 6 | 7 | 8 | Source Code 9 | ---------------- 10 | You can view the source code for the `JsonSchema` object [here](https://github.com/APIDevTools/json-schema/blob/master/src/resource.ts). 11 | 12 | 13 | 14 | Properties 15 | ---------------------- 16 | 17 | ### `schema` ([`JsonSchema` object](json-schema.md)) 18 | This is the `JsonSchema` object that contains this resource. 19 | 20 | ```javascript 21 | if (resource.schema.rootResource === resource) { 22 | console.log("This is the root resource of the schema"); 23 | } 24 | ``` 25 | 26 | 27 | ### `file` ([`File` object](file.md)) 28 | This is the `File` object that contains this resource. 29 | 30 | ```javascript 31 | console.log(`This resource is in ${resource.file.path}`); 32 | ``` 33 | 34 | 35 | ### `uri` ([`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL)) 36 | The absolute, canonical [UR**I** (Uniform Resource Identifier)](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) of the resource. Note that this does _not_ necessarily correspond to a physical file on disk, or a [UR**L** (Uniform Resource Locator)](https://en.wikipedia.org/wiki/URL) that can be downloaded. 37 | 38 | ```javascript 39 | console.log(resource.uri.href); 40 | ``` 41 | 42 | 43 | ### `locationInFile` ([`Pointer` object](pointer.md)) 44 | A [JSON Pointer](https://tools.ietf.org/html/rfc6901) that indicates the resource's location in the file. 45 | 46 | ```javascript 47 | console.log(`This resource is at ${resource.locationInFile.path} in ${resource.file}`); 48 | ``` 49 | 50 | 51 | ### `data` (anything) 52 | The resource data. This can be _any_ JavaScript value, but will usually be one of the following: 53 | 54 | - `object`
55 | If the resource is a JSON Schema document that has already been parsed. 56 | 57 | - `string`
58 | If the resource is in text-based data that has not yet been parsed. 59 | This includes JSON, YAML, HTML, SVG, CSV, plain-text, etc. 60 | 61 | - `ArrayBuffer`
62 | If the resource contains binary data, such as an image. 63 | 64 | ```javascript 65 | resource.data = { 66 | $id: "person", 67 | title: "Person", 68 | properties: { 69 | name: { type: "string" }, 70 | age: { type: "integer" }, 71 | } 72 | }; 73 | ``` 74 | 75 | 76 | ### `anchors` (array of [`Anchor` objects](anchor.md)) 77 | An array of all the [JSON Schema anchors](http://json-schema.org/draft/2019-09/json-schema-core.html#anchor) (i.e. `$anchor`) in the resource. 78 | 79 | ```javascript 80 | for (let anchor of resource.anchors) { 81 | console.log(anchor.uri.href); 82 | } 83 | ``` 84 | 85 | 86 | ### `references` (iterable of [`Reference` objects](reference.md)) 87 | An array of all the [JSON Schema references](http://json-schema.org/draft/2019-09/json-schema-core.html#ref) (i.e. `$ref`) in the resource. 88 | 89 | ```javascript 90 | for (let ref of resource.references) { 91 | console.log(`${ref.locationInFile.path} points to ${ref.targetURI.href}`); 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/schema-error.md: -------------------------------------------------------------------------------- 1 | `SchemaError` Object 2 | ====================== 3 | 4 | > ### ⚠ INCOMPLETE DOCS ⚠ 5 | > This documentation has not been written yet 6 | -------------------------------------------------------------------------------- /docs/schema-structure.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIDevTools/json-schema-reader/45b31d2d4304f3255ab98ab7a452453bc0409850/docs/schema-structure.afphoto -------------------------------------------------------------------------------- /docs/schema-structure.md: -------------------------------------------------------------------------------- 1 | Understanding Schema Structure 2 | ======================================== 3 | The [`JsonSchema` object](json-schema.md) is an object model representation of your entire JSON Schema. It includes detailed information about each file in your schema, and provides data structures that make it easy to inspect important parts of each file. This page explains the concepts of the object model. 4 | 5 | 6 | Example 7 | ------------------------ 8 | Consider the following JSON Schema that is composed of 2 files. The file contents are shown on the right. On the left, you see a visual representation of the corresponding `JsonSchema` object model. 9 | 10 | ![JSON Schema Structure](schema-structure.svg) 11 | 12 | 13 | The `JsonSchema` Object 14 | -------------------------- 15 | The [`JsonSchema` object](json-schema.md) is always the top-level object. It represents your entire schema, regardless of how many files it's composed of. A valid `JsonSchema` object will always have at least one file (the `rootFile`), which will have at least one resource (the `rootResource`). Even the most simple, single-file JSON Schema will consist of at least one file and one resource. 16 | 17 | In [our example above](#example), the `JsonSchema` object contains two files. That is, it's `files` array has two `File` objects in it. One of the files has a single resource, while the other file contains two resources. 18 | 19 | 20 | Files 21 | ------------------- 22 | Each [`File` object](file.md) corresponds to a physical file on disk or URL that was downloaded. 23 | 24 | The file's `url` property is the absolute URL of the file. This will be a `file://` URL for files read from the filesystem, or an `http://` or `https://` URL for downloaded files. Note that it's a [UR**L** (Uniform Resource Locator)](https://en.wikipedia.org/wiki/URL) because it specifies the physical location of the file, as opposed to the [UR**I** (Uniform Resource Identifier)](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) that resources, anchors, and references have, which are simply identifiers and may not correspond to a physical file anywhere. 25 | 26 | In [our example above](#example), the schema has two files: **schema.json** and **types.json**. 27 | 28 | 29 | Resources 30 | -------------------- 31 | Each file in a JSON Schema will always have at least one [`Resource` object](resource.md), but can have more than one. A resource is a schema that is identified by a [URI (Uniform Resource Identifier)](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier). 32 | 33 | Depending on the version of JSON Schema being used, the syntax for defining a resource URI varies. For example, [the 2019-09 spec used the `$id` keyword](https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.8.2.2), wheras [earlier versions used the `id` keyword](https://json-schema.org/draft-04/json-schema-core.html#rfc.section.7.2) (without a dollar sign). 34 | 35 | In [our example above](#example), the **schema.json** file has an `$id` keyword at the top level, which defines the URI for the file's root resource. The `$id` (`person`) is resolved relative to the file's URL (`file://path/to/schema.json`), so the resulting resource URI is `file://path/to/person`. 36 | 37 | In the **types.json** file, the root resource has no `$id` keyword, so its URI is the same as the file's URL (`file://path/to/types.json`). This resource contains three nested schema definitions (`Name`, `Address`, and `AddressType`), but only `AddressType` has an `$id` keyword, so only it becomes a separate resource. `Name` and `Address` are simply sub-schemas in the root resource. 38 | 39 | > **WARNING:** Adding an `$id` keyword creates a resource, which alters how references and anchors within that resource are resolved. This can break references that previously worked, so be sure you know what you're doing. 40 | 41 | ### Canonical URI 42 | A resource always has a single, absolute, canonical URI, which is defined by its `$id` keyword, resolved against the parent resource URI or file URL. For example, the `AddressType` schema's `$id` is `address-type`. Its parent resource is the root resource of the **types.json** file, which has a URI of `file://path/to/types.json`. Therefore, the canonical URI of the `AddressType` schema is: 43 | 44 | ``` 45 | file://path/to/address-type 46 | ``` 47 | 48 | ### Non-Canonical URIs 49 | In addition to the canonical URI, a resource can be referenced by non-canonical URIs. For example, each of the following URIs also references the `AddressType` schema in [our example above](#example): 50 | 51 | #### File URL + JSON Pointer Fragment 52 | This URI uses a [JSON Pointer fragment](https://json-schema.org/draft/2019-09/json-schema-core.html#embedded), relative to the file URL. 53 | 54 | ``` 55 | file://path/to/types.json#/$defs/AddressType 56 | ``` 57 | 58 | #### Resource URI + Empty JSON Pointer Fragment 59 | This URI uses a [JSON Pointer fragment](https://json-schema.org/draft/2019-09/json-schema-core.html#embedded), relative to the resource URI. Note that an empty fragment points to the whole resource. 60 | 61 | ``` 62 | file://path/to/address-type# 63 | ``` 64 | 65 | #### Resource URI + Anchor 66 | This URI uses a plain-name fragment, which is defined by [the `$anchor` keyword](https://json-schema.org/draft/2019-09/json-schema-core.html#anchor). Notice that the anchor is relative to the resource URI, _not_ the file URL. 67 | 68 | ``` 69 | file://path/to/address-type#address-type 70 | ``` 71 | 72 | 73 | 74 | Anchors 75 | ---------------------- 76 | An [`Anchor` object](anchor.md) identifies a sub-schema within a resource via a plain-name fragment. They're created using [the `$anchor` keyword in JSON Schema](https://json-schema.org/draft/2019-09/json-schema-core.html#anchor). 77 | 78 | Like resources, anchors have a [URI (Uniform Resource Identifier)](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier), but anchor URIs _don't_ create a new resource and _don't_ alter the resolution scope of other URIs. 79 | 80 | > **NOTE:** Unlike the `$id` keyword, it's safe to add an `$anchor` to a schema without worrying about breaking other anchors or references. 81 | 82 | In [our example above](#example), the `Name` and `Address` schemas in **types.json** each have an `$anchor` keyword, which allows them to be referenced using plain-name fragments, like this: 83 | 84 | ``` 85 | file://path/to/types.json#name 86 | file://path/to/types.json#address 87 | ``` 88 | 89 | The `AddressType` schema in **types.json** also has an `$anchor` keyword, but since the `AddressType` schema is a resource (it has an `$id` keyword), the anchor is relative to the resource's URI: 90 | 91 | ``` 92 | file://path/to/address-type#address-type 93 | ``` 94 | 95 | 96 | 97 | References 98 | ------------------------- 99 | A [`Reference` object](reference.md) will be created for every `$ref` in your JSON Schema. References point to another value in the schema, which can be in the same file or a different file. 100 | 101 | Like anchors, references are relative to their parent resource URI. In [our example above](#example), the `name` and `address` properties in **schema.json** both reference the **types.json** file, whereas the `parent` property referenes its own parent resource. 102 | 103 | In **types.json**, there's a `$ref` that points to `address-type`. When resolved against the parent resource URI (`file://path/to/types.json`), it points to the `AddressType` resource URI (`file://path/to/address-type`). 104 | -------------------------------------------------------------------------------- /docs/schema-structure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | types.json 6 | { 7 | "$defs": { 8 | "Name": { 9 | "$anchor": "name", 10 | "title": "Name", 11 | "properties": { 12 | "first": { "type": "string" }, 13 | "last": { "type": "string" } 14 | } 15 | }, 16 | "Address": { 17 | "$anchor": "address", 18 | "title": "Address", 19 | "properties": { 20 | "type": { 21 | "$ref": "address-type" 22 | }, 23 | "street": { "type": "string" }, 24 | "city": { "type": "string" }, 25 | "postalCode": { "type": "string" } 26 | } 27 | }, 28 | "AddressType": { 29 | "$id": "address-type", 30 | "$anchor": "address-type", 31 | "enum": ["home", "office"] 32 | } 33 | } 34 | } 35 | 36 | 37 | schema.json 38 | { 39 | "$schema": "https://json-schema.org/draft/2019-09/schema", 40 | "$id": "person", 41 | "title": "Person", 42 | "properties": { 43 | "name": { 44 | "$ref": "types.json#/$defs/Name" 45 | }, 46 | "address": { 47 | "$ref": "types.json#address" 48 | }, 49 | "parent": { 50 | "$ref": "person" 51 | } 52 | } 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | JsonSchema 61 | 62 | 63 | 64 | 65 | 66 | 67 | File 68 | file://path/to/types.json 69 | 70 | 71 | 72 | 73 | 74 | 75 | Resource 76 | file://path/to/address-type 77 | 78 | 79 | 80 | 81 | 82 | 83 | Anchor 84 | file://path/to/address-type#address-type 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Resource 94 | file://path/to/types.json 95 | 96 | 97 | 98 | 99 | 100 | 101 | Ref 102 | file://path/to/address-type 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Anchor 111 | file://path/to/types.json#address 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Anchor 120 | file://path/to/types.json#name 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | File 131 | file://path/to/schema.json 132 | 133 | 134 | 135 | 136 | 137 | 138 | Resource 139 | file://path/to/person 140 | 141 | 142 | 143 | 144 | 145 | 146 | Ref 147 | file://path/to/person 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | Ref 156 | file://path/to/types.json#address 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | Ref 165 | file://path/to/types.json#/$defs/Name 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | Anchor 174 | file://path/to/person#person 175 | 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma config 2 | // https://karma-runner.github.io/0.12/config/configuration-file.html 3 | // https://jstools.dev/karma-config/ 4 | 5 | "use strict"; 6 | const { karmaConfig } = require("@jsdevtools/karma-config"); 7 | const { host } = require("@jsdevtools/host-environment"); 8 | 9 | module.exports = karmaConfig({ 10 | sourceDir: "esm", 11 | tests: "test/specs/**/*spec.js", 12 | browsers: { 13 | chrome: host.ci ? host.os.linux : true, 14 | firefox: host.ci ? host.os.linux : true, 15 | safari: host.ci ? host.os.linux : host.os.mac, 16 | edge: host.ci ? host.os.linux : host.os.windows, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apidevtools/json-schema-reader", 3 | "version": "0.0.0-alpha.0", 4 | "description": "Reads multi-file JSON Schemas from the filesystem, URLs, and other sources", 5 | "keywords": [ 6 | "json-schema", 7 | "jsonschema", 8 | "ref", 9 | "$ref", 10 | "swagger", 11 | "openapi", 12 | "open-api", 13 | "read", 14 | "reader", 15 | "resolve", 16 | "resolver", 17 | "parse", 18 | "parser", 19 | "isomorphic", 20 | "browser" 21 | ], 22 | "author": { 23 | "name": "James Messinger", 24 | "url": "https://jamesmessinger.com" 25 | }, 26 | "license": "MIT", 27 | "homepage": "https://apitools.dev/json-schema-reader", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/APIDevTools/json-schema-reader.git" 31 | }, 32 | "main": "cjs/index.js", 33 | "module": "esm/index.js", 34 | "types": "esm/index.d.ts", 35 | "browser": { 36 | "./cjs/isomorphic.node.js": "./cjs/isomorphic.browser.js", 37 | "./esm/isomorphic.node.js": "./esm/isomorphic.browser.js", 38 | "node-fetch": false 39 | }, 40 | "files": [ 41 | "cjs", 42 | "esm" 43 | ], 44 | "scripts": { 45 | "clean": "shx rm -rf .nyc_output coverage cjs esm", 46 | "lint": "eslint src test", 47 | "build": "npm run build:cjs && npm run build:esm", 48 | "build:esm": "tsc", 49 | "build:cjs": "tsc --module commonjs --outDir cjs", 50 | "watch": "npm run build:cjs -- --watch", 51 | "test": "npm run test:node && npm run test:browser && npm run lint", 52 | "test:node": "mocha", 53 | "test:browser": "karma start --single-run", 54 | "coverage": "npm run coverage:node && npm run coverage:browser", 55 | "coverage:node": "nyc node_modules/mocha/bin/mocha", 56 | "coverage:browser": "npm run test:browser -- --coverage", 57 | "upgrade": "npm-check -u && npm audit fix", 58 | "bump": "bump prerelease --tag --push --all", 59 | "release": "npm run upgrade && npm run clean && npm run build && npm test && npm run bump" 60 | }, 61 | "engines": { 62 | "node": ">=10" 63 | }, 64 | "devDependencies": { 65 | "@jsdevtools/eslint-config": "^1.1.4", 66 | "@jsdevtools/host-environment": "^2.1.2", 67 | "@jsdevtools/karma-config": "^3.1.7", 68 | "@jsdevtools/version-bump-prompt": "^6.0.6", 69 | "@types/chai": "^4.2.12", 70 | "@types/mime": "^2.0.3", 71 | "@types/mocha": "^8.0.3", 72 | "@types/node": "^14.6.2", 73 | "chai": "^4.2.0", 74 | "eslint": "^7.7.0", 75 | "karma": "^5.1.1", 76 | "karma-cli": "^2.0.0", 77 | "mocha": "^8.1.3", 78 | "npm-check": "^5.9.0", 79 | "nyc": "^15.1.0", 80 | "shx": "^0.3.2", 81 | "source-map-support": "^0.5.19", 82 | "typescript": "^4.0.2" 83 | }, 84 | "dependencies": { 85 | "mime": "^2.4.6", 86 | "node-fetch": "^3.0.0-beta.4" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { createErrorHandler, File, Helpers as JsonSchemaHelpers } from "@apidevtools/json-schema"; 2 | import { determineFileType } from "./hooks/determine-file-type"; 3 | import { indexFile } from "./hooks/index-file"; 4 | import { parseFile } from "./hooks/parse-file"; 5 | import { readFile } from "./hooks/read-file"; 6 | import { FileType, NormalizedOptions } from "./options"; 7 | 8 | 9 | /** 10 | * Helpful information and utilities that are passed to all custom functions 11 | */ 12 | export interface Helpers extends JsonSchemaHelpers { 13 | /** 14 | * The current working directory. In web browsers, this is the URL of the current page. 15 | */ 16 | cwd: string; 17 | 18 | /** 19 | * Options that determine the behavior when downloading files over HTTP(s) or similar protocols. 20 | * Some of these options only apply when running in a web browser, not in Node.js. 21 | */ 22 | http: RequestInit; 23 | 24 | /** 25 | * Calls the default `readFile` implementation, which can download files from HTTP and HTTPS URLs, 26 | * and can read files from the filesystem in Node.js. 27 | */ 28 | readFile(file: File, helpers: Helpers): Promise | void; 29 | 30 | /** 31 | * Calls the default `determineFileType` implementation, which supports many common text and 32 | * binary file formats. 33 | */ 34 | determineFileType(file: File, helpers: Helpers): FileType | void; 35 | 36 | /** 37 | * Calls the default `parseFile` implementation, which can only parse JSON files. 38 | */ 39 | parseFile(file: File, helpers: Helpers): void; 40 | 41 | /** 42 | * Calls the default `indexFile` implementation, which attempts to determine the appropriate 43 | * version of the JSON Schema spec, and indexes the file according to that spec version. 44 | */ 45 | indexFile(file: File, helpers: Helpers): void; 46 | } 47 | 48 | 49 | /** 50 | * Determines how the `Helpers` object is created 51 | */ 52 | export interface HelperConfig { 53 | file: File; 54 | code: string; 55 | message?: string; 56 | options: NormalizedOptions; 57 | readFile?(file: File, helpers: Helpers): Promise | void; 58 | determineFileType?(file: File, helpers: Helpers): FileType | void; 59 | parseFile?(file: File, helpers: Helpers): void; 60 | indexFile?(file: File, helpers: Helpers): void; 61 | } 62 | 63 | 64 | /** 65 | * Creates the `Helpers` object that gets passed to all custom functions 66 | */ 67 | export function createHelpers(config: HelperConfig): Helpers { 68 | let { file, code, message, options } = config; 69 | let { continueOnError } = options; 70 | 71 | return { 72 | cwd: options.cwd, 73 | http: options.http, 74 | readFile: config.readFile || ((f) => readFile(f, options)), 75 | determineFileType: config.determineFileType || ((f) => determineFileType(f, options)), 76 | parseFile: config.parseFile || ((f) => parseFile(f, options)), 77 | indexFile: config.indexFile || ((f) => indexFile(f, options)), 78 | handleError: createErrorHandler({ file, code, message, continueOnError }), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/hooks/determine-file-type.ts: -------------------------------------------------------------------------------- 1 | import { File } from "@apidevtools/json-schema"; 2 | import { createHelpers, Helpers } from "../helpers"; 3 | import { FileType, NormalizedOptions } from "../options"; 4 | 5 | /** 6 | * This RegExp pattern matches text-based media types like: 7 | * 8 | * - text/plain 9 | * - text/x-markdown 10 | * - application/json 11 | * - application/xml 12 | * - application/xhtml+xml 13 | * - image/svg+xml; charset=utf-8 14 | * - application/vnd.github.v3.raw+json 15 | */ 16 | const textMediaTypes = /(\btext\/|\/(javascript|json|xml|svg)\b|\+(json|xml)\b)/i; 17 | 18 | // Matches common text-based file extensions 19 | const textExtensions = /\.(txt|jsx?|tsx?|mdx?|json|yml|yaml|html?|xml|xslt?|svg|csv|tsv)$/i; 20 | 21 | 22 | /** 23 | * Determines whether a file should be read as binary or text 24 | */ 25 | export function determineFileType(file: File, options: NormalizedOptions): FileType { 26 | let type: FileType; 27 | let helpers = createHelpers({ file, options, 28 | code: "ERR_FILE_TYPE", 29 | message: `Unable to determine the file type of ${file}`, 30 | determineFileType: defaultDetermineFileType, 31 | }); 32 | 33 | try { 34 | if (options.determineFileType) { 35 | // Call the custom implementation 36 | type = options.determineFileType.call(undefined, file, helpers); 37 | } 38 | else { 39 | // Call the default implementation 40 | type = defaultDetermineFileType(file, helpers); 41 | } 42 | 43 | // Treat anything other than "text" as binary 44 | if (type !== "text") { 45 | type = "binary"; 46 | } 47 | 48 | return type; 49 | } 50 | catch (error) { 51 | helpers.handleError(error); 52 | return "binary"; 53 | } 54 | } 55 | 56 | 57 | /** 58 | * Determines whether a file should be read as binary or text, based on its media type 59 | */ 60 | export function defaultDetermineFileType(file: File, _helpers: Helpers): FileType { 61 | if (textMediaTypes.test(file.mediaType) || textExtensions.test(file.url.pathname)) { 62 | return "text"; 63 | } 64 | else { 65 | return "binary"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/hooks/index-file.ts: -------------------------------------------------------------------------------- 1 | import { File, jsonSchema, SchemaError } from "@apidevtools/json-schema"; 2 | import { createHelpers, Helpers } from "../helpers"; 3 | import { NormalizedOptions } from "../options"; 4 | 5 | /** 6 | * Indexes a file's contents 7 | */ 8 | export function indexFile(file: File, options: NormalizedOptions): void { 9 | let helpers = createHelpers({ file, options, 10 | code: "ERR_INDEX", 11 | indexFile: defaultIndexFile, 12 | }); 13 | 14 | try { 15 | if (options.indexFile) { 16 | // Call the custom implementation 17 | options.indexFile.call(undefined, file, helpers); 18 | } 19 | else { 20 | // Call the default implemenation 21 | defaultIndexFile(file, helpers); 22 | } 23 | } 24 | catch (error) { 25 | helpers.handleError(error); 26 | } 27 | } 28 | 29 | 30 | /** 31 | * The default implementation of the `indexFile` hook. It first tries to auto-detect the 32 | * JSON Schema version. If that fails, then it falls-back to the latest version of JSON Schema. 33 | */ 34 | export function defaultIndexFile(file: File, helpers: Helpers): void { 35 | try { 36 | // Default to auto-detecting the JSON Schema version 37 | jsonSchema.auto.indexFile(file, helpers); 38 | } 39 | catch (error) { 40 | if ((error as SchemaError).code === "ERR_SCHEMA_VERSION") { 41 | // We were unable to auto-detect the JSON Schema version, 42 | // so try again using the latest version of JSON Schema 43 | jsonSchema.latest.indexFile(file, helpers); 44 | } 45 | else { 46 | throw error; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/parse-file.ts: -------------------------------------------------------------------------------- 1 | import { File } from "@apidevtools/json-schema"; 2 | import { createHelpers, Helpers } from "../helpers"; 3 | import { NormalizedOptions } from "../options"; 4 | 5 | /** 6 | * This RegExp pattern matches JSON media types like: 7 | * 8 | * - text/json 9 | * - application/json 10 | * - application/vnd.oai.openapi+json 11 | * - application/json; charset=utf-8 12 | * - application/vnd.github.v3.raw+json 13 | */ 14 | const jsonMediaType = /\/json\b|\+json\b/i; 15 | 16 | 17 | /** 18 | * Parses a file's contents 19 | */ 20 | export function parseFile(file: File, options: NormalizedOptions): void { 21 | let helpers = createHelpers({ file, options, 22 | code: "ERR_PARSE", 23 | message: `Unable to parse ${file}`, 24 | parseFile: defaultParseFile, 25 | }); 26 | 27 | try { 28 | if (options.parseFile) { 29 | // Call the custom implementation 30 | options.parseFile.call(undefined, file, helpers); 31 | } 32 | else { 33 | // Call the default implemenation 34 | defaultParseFile(file, helpers); 35 | } 36 | } 37 | catch (error) { 38 | helpers.handleError(error); 39 | } 40 | } 41 | 42 | 43 | /** 44 | * Parses JSON files 45 | */ 46 | export function defaultParseFile(file: File, _helpers: Helpers): void { 47 | // Determine if this is a JSON file 48 | let isText = typeof file.data === "string"; 49 | let isJSON = file.url.pathname.endsWith(".json") || jsonMediaType.test(file.mediaType); 50 | 51 | if (isText && isJSON) { 52 | // It's a JSON file, so parse it 53 | file.data = JSON.parse(file.data as string) as unknown; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks/read-file.ts: -------------------------------------------------------------------------------- 1 | import { File } from "@apidevtools/json-schema"; 2 | import { createHelpers, Helpers } from "../helpers"; 3 | import { getFetchImplementation } from "../isomorphic.node"; 4 | import { NormalizedOptions } from "../options"; 5 | 6 | /** 7 | * Reads a single file from its source 8 | */ 9 | export async function readFile(file: File, options: NormalizedOptions): Promise { 10 | let helpers = createHelpers({ file, options, 11 | code: "ERR_READ", 12 | message: `Unable to read ${file}`, 13 | readFile: defaultReadFile, 14 | }); 15 | 16 | try { 17 | if (options.readFile) { 18 | // Call the custom implementation 19 | await options.readFile.call(undefined, file, helpers); 20 | } 21 | else { 22 | // Call the default implemenation 23 | await defaultReadFile(file, helpers); 24 | } 25 | } 26 | catch (error) { 27 | helpers.handleError(error); 28 | } 29 | } 30 | 31 | 32 | /** 33 | * Reads a file from the local filesystem or a web URL 34 | */ 35 | export async function defaultReadFile(file: File, helpers: Helpers): Promise { 36 | // Fetch the file using the appropriate fetch implemenation, 37 | // based on the runtime environment and URL type 38 | let fetch = getFetchImplementation(file.url); 39 | let response = await fetch(file.url.href, helpers.http); 40 | 41 | if (!response.ok) { 42 | let { status, statusText } = response; 43 | throw new URIError( 44 | `HTTP ${status || "Error"} (${statusText || "Unknown Error"}) while fetching ${file.url.href}`); 45 | } 46 | 47 | // Update the File with info from the HTTP response 48 | file.mediaType = response.headers.get("Content-Type") || ""; 49 | response.status && (file.metadata.status = response.status); 50 | response.statusText && (file.metadata.statusText = response.statusText); 51 | response.redirected && (file.metadata.redirected = response.redirected); 52 | response.url && (file.metadata.responseURL = response.url); 53 | 54 | response.headers.forEach((value, header) => { 55 | file.metadata[header] = value; 56 | }); 57 | 58 | // Determine whether to read the response as text or binary 59 | let fileType = helpers.determineFileType(file, helpers); 60 | 61 | if (fileType === "text") { 62 | file.data = await response.text(); 63 | } 64 | else { 65 | file.data = await response.arrayBuffer(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readJsonSchema } from "./read-json-schema"; 2 | 3 | export { Anchor, File, JsonSchema, Pointer, Reference, Resource, SchemaError } from "@apidevtools/json-schema"; 4 | export { Helpers } from "./helpers"; 5 | export { MultiError } from "./multi-error"; 6 | export { FileType, Options } from "./options"; 7 | export { readJsonSchema }; 8 | 9 | /** 10 | * Reads JSON schemas from files, URLs, and other sources 11 | */ 12 | const jsonSchemaReader = { 13 | /** 14 | * Reads a multi-file JSON schema from any combination of files, URLs, and other sources 15 | */ 16 | read: readJsonSchema, 17 | }; 18 | 19 | export default jsonSchemaReader; 20 | 21 | // CommonJS default export hack 22 | /* eslint-env commonjs */ 23 | if (typeof module === "object" && typeof module.exports === "object") { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 25 | module.exports = Object.assign(module.exports.default, module.exports); 26 | } 27 | -------------------------------------------------------------------------------- /src/isomorphic.browser.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | /// 3 | 4 | import { resolveRelativeURL } from "./url-utils"; 5 | 6 | 7 | /** 8 | * Returns the URL of the current web page 9 | */ 10 | export function getCWD(): string { 11 | return window.location.href; 12 | } 13 | 14 | 15 | /** 16 | * Resolves the path of a file, relative to another file 17 | */ 18 | export function resolveFilePath(base: string, relative: string): string { 19 | return resolveRelativeURL(base, relative); 20 | } 21 | 22 | 23 | /** 24 | * In web browsers, we always use the browser's `fetch()` implementation 25 | */ 26 | export function getFetchImplementation() { 27 | return fetch; 28 | } 29 | -------------------------------------------------------------------------------- /src/isomorphic.node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /// 3 | /// 4 | 5 | import { promises as fs } from "fs"; 6 | import { getType } from "mime"; 7 | import nodeFetch from "node-fetch"; 8 | import * as path from "path"; 9 | import { fileURLToPath } from "url"; 10 | import { absoluteUrlPattern, resolveRelativeURL } from "./url-utils"; 11 | 12 | const isWindows = process.platform === "win32"; 13 | 14 | 15 | // eslint-disable-next-line no-undef 16 | type Fetch = typeof fetch; 17 | 18 | 19 | /** 20 | * Returns the current working directory 21 | */ 22 | export function getCWD(): string { 23 | return process.cwd(); 24 | } 25 | 26 | 27 | /** 28 | * Resolves the path of a file, relative to another file 29 | */ 30 | export function resolveFilePath(base: string, relative: string): string { 31 | if (absoluteUrlPattern.test(base)) { 32 | // This is a URL, not a filesystem path 33 | return resolveRelativeURL(base, relative); 34 | } 35 | 36 | let lastChar = base[base.length - 1]; 37 | let dir: string; 38 | 39 | if (base.length === 0 || lastChar === "/" || (isWindows && lastChar === "\\")) { 40 | // The base path is a directory, not a file 41 | dir = base; 42 | } 43 | else { 44 | // The base path is a file, so get its directory 45 | dir = path.dirname(base); 46 | } 47 | 48 | // Resolve the path, relative to the base directory 49 | return path.join(dir, relative); 50 | } 51 | 52 | 53 | /** 54 | * Returns the appropriate `fetch()` implementation for the given URL 55 | */ 56 | export function getFetchImplementation(url: URL): Fetch { 57 | if (url.protocol === "file:") { 58 | return filesystemFetch; 59 | } 60 | else { 61 | return nodeFetch as unknown as Fetch; 62 | } 63 | } 64 | 65 | 66 | /** 67 | * A Fetch-compatible wrapper around `fs.readFile()` 68 | */ 69 | async function filesystemFetch(url: string): Promise { 70 | // Read the file 71 | let filePath = fileURLToPath(url); 72 | let stats = await fs.stat(filePath); 73 | let data = await fs.readFile(filePath); 74 | 75 | // Convert file stats to a Fetch-compatible Headers object 76 | let headers: Record = { 77 | "content-location": filePath, 78 | "content-type": getType(filePath), 79 | "content-length": data.byteLength.toString(), 80 | "last-modified": stats.mtime.toUTCString(), 81 | ...stats, 82 | }; 83 | 84 | // Return the file as a Fetch-compatible Response object 85 | let response = { 86 | ok: true, 87 | 88 | text() { 89 | return data.toString("utf8"); 90 | }, 91 | 92 | arrayBuffer() { 93 | return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); 94 | }, 95 | 96 | headers: { 97 | get(headerName: string) { 98 | headerName = headerName.toLowerCase(); 99 | return headers[headerName] || null; 100 | }, 101 | 102 | forEach(iterator: (value: unknown, header: string) => void) { 103 | for (let [stat, value] of Object.entries(headers)) { 104 | iterator(value, stat); 105 | } 106 | } 107 | } 108 | }; 109 | 110 | return response as unknown as Response; 111 | } 112 | -------------------------------------------------------------------------------- /src/multi-error.ts: -------------------------------------------------------------------------------- 1 | import { JsonSchema, SchemaError } from "@apidevtools/json-schema"; 2 | 3 | // TODO: Extend from AggregateError instead, once it's supported in Node 4 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError 5 | 6 | /** 7 | * An error that wraps multiple other errors. This error type is thrown when the 8 | * `continueOnError` option is enabled and one or more errors occur. 9 | */ 10 | export class MultiError extends Error { 11 | /** 12 | * A string that identifies the type of error 13 | */ 14 | public code: string; 15 | 16 | /** 17 | * The JSON schema, including all files that were successfully read 18 | */ 19 | public schema: JsonSchema; 20 | 21 | /** 22 | * All errors that occurred while reading the schema 23 | */ 24 | public errors: SchemaError[]; 25 | 26 | 27 | public constructor(schema: JsonSchema) { 28 | let errors = [...schema.errors]; 29 | let plural = errors.length > 1 ? "errors" : "error"; 30 | super(`${errors.length} ${plural} occurred while reading ${schema.rootFile}`); 31 | 32 | this.name = "MultiError"; 33 | this.code = "ERR_MULTIPLE"; 34 | this.schema = schema; 35 | this.errors = errors; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { File } from "@apidevtools/json-schema"; 2 | import { Helpers } from "./helpers"; 3 | import { getCWD } from "./isomorphic.node"; 4 | 5 | 6 | /** 7 | * JSON Schema Reader options 8 | */ 9 | export interface Options { 10 | /** 11 | * The directory path/URL used to resolve the root JSON scheam location, if it's a relative path/URL. 12 | * This defaults to `process.cwd()` in Node.js, and defaults to `window.location.href` in web brwosers. 13 | */ 14 | cwd?: string; 15 | 16 | /** 17 | * Indicates whether the reader should continue reading as many files as possible, 18 | * even if errors are encountered. When `false` (the default) the first error is thrown. 19 | * When `true` and an error occurs, an error is thrown after all possible files are read, 20 | * and the error's `errors` property is an array of all errors that ocurred. 21 | */ 22 | continueOnError?: boolean; 23 | 24 | /** 25 | * Options that determine the behavior when downloading files over HTTP(s) or similar protocols. 26 | * Some of these options only apply when running in a web browser, not in Node.js. 27 | */ 28 | http?: RequestInit; 29 | 30 | /** 31 | * Overrides the default functionality for reading/downloading files. You can fallback to the 32 | * default implementation by calling `helpers.readFile()`. 33 | * 34 | * This function is responsible for and setting `file.data`, `file.mediaType`, `file.metadata`, 35 | * and any other relevant properties of the `file` object. 36 | */ 37 | readFile?(file: File, helpers: Helpers): void | Promise; 38 | 39 | /** 40 | * Overrides the default functionality for determining whether a file should be read as binary 41 | * or text. You can fallback to the default implementation by calling `helpers.determineFileType()`. 42 | * 43 | * @returns 44 | * One of the following values: 45 | * 46 | * - `"text"` if the file should be read as text. This should be used for JSON, YAML, HTML, SVG, 47 | * CSV, plain-text, and other text-based file formats 48 | * 49 | * - `"binary"` if the file should be read as binary data, which is suitable for PNG, GIF, JPEG, 50 | * and other binary files. 51 | * 52 | * - Any other value will be treated as binary 53 | */ 54 | determineFileType?(file: File, helpers: Helpers): FileType; 55 | 56 | /** 57 | * Overrides the default functionality for parsing file contents. You can fallback to the 58 | * default implementation by calling `helpers.parseFile()`. 59 | * 60 | * This function is responsible for parsing the `file.data` property and replacing it with the 61 | * parsed results. It may also update other relevant properties of the `file` object. 62 | */ 63 | parseFile?(file: File, helpers: Helpers): void; 64 | 65 | /** 66 | * Overrides the dfeault functionality for indexing file contents. You can fallback to the 67 | * default implementation by calling `helpers.indexFile()`. 68 | * 69 | * This function is responsible for populating the `File.resources` array, including the 70 | * `Resource.anchors` and `Resource.references` arrays of each resource. 71 | */ 72 | indexFile?(file: File, helpers: Helpers): void; 73 | } 74 | 75 | 76 | /** 77 | * Indicates whether a file should be read as text or binary 78 | */ 79 | export type FileType = "text" | "binary"; 80 | 81 | 82 | /** 83 | * Normalized and sanitized options with defaults 84 | */ 85 | export interface NormalizedOptions extends Options { 86 | cwd: string; 87 | continueOnError: boolean; 88 | http: RequestInit; 89 | } 90 | 91 | 92 | /** 93 | * Normalizes and sanitizes user-provided options 94 | */ 95 | export function normalizeOptions(options: Options = {}): NormalizedOptions { 96 | let normalized: NormalizedOptions = Object.assign({}, options, { 97 | cwd: (options.cwd && typeof options.cwd === "string") ? options.cwd : getCWD(), 98 | continueOnError: Boolean(options.continueOnError), 99 | http: Object.assign({}, options.http), 100 | }); 101 | 102 | return normalized; 103 | } 104 | -------------------------------------------------------------------------------- /src/process-file.ts: -------------------------------------------------------------------------------- 1 | import { File, JsonSchema } from "@apidevtools/json-schema"; 2 | import { indexFile } from "./hooks/index-file"; 3 | import { parseFile } from "./hooks/parse-file"; 4 | import { readFile } from "./hooks/read-file"; 5 | import { resolveFilePath } from "./isomorphic.node"; 6 | import { NormalizedOptions } from "./options"; 7 | import { relativeURL, removeHash } from "./url-utils"; 8 | 9 | 10 | /** 11 | * Reads a file from its source, parses it, resolves its references, and recursively processes those files 12 | */ 13 | export async function processFile(file: File, options: NormalizedOptions): Promise { 14 | // Read the file from disk, network, web, etc. 15 | await readFile(file, options); 16 | 17 | // Parse the file contents 18 | parseFile(file, options); 19 | 20 | // Build an index of all JSON Schema resources, references, and anchors in the file 21 | indexFile(file, options); 22 | 23 | // Read and process any referenced files 24 | await processReferencedFiles(file, options); 25 | } 26 | 27 | 28 | /** 29 | * Process all new external files that are referenced in a file 30 | */ 31 | async function processReferencedFiles(file: File, options: NormalizedOptions): Promise { 32 | let { schema } = file; 33 | let promises = []; 34 | 35 | for (let ref of file.references) { 36 | if (!alreadyExists(ref.targetURI, file.schema)) { 37 | // Get the URL of the target file, without the hash 38 | let url = removeHash(ref.targetURI); 39 | 40 | // Set the file path based on the current file path 41 | let relative = relativeURL(file.url, url); 42 | let path = relative ? resolveFilePath(file.path, relative) : url.href; 43 | 44 | // Create the new file and add it to the schema 45 | let newFile = new File({ schema, url, path }); 46 | schema.files.push(newFile); 47 | 48 | // Start processing the file asynchronously 49 | promises.push(processFile(newFile, options)); 50 | } 51 | } 52 | 53 | // Wait for all referenced files to finish processing 54 | await Promise.all(promises); 55 | } 56 | 57 | 58 | /** 59 | * Determines whether a file or resource with the given URL already exists in the schema 60 | */ 61 | function alreadyExists(url: URL, schema: JsonSchema) { 62 | for (let file of schema.files) { 63 | if (compareURLs(url, file.url)) return true; 64 | } 65 | for (let resource of schema.resources) { 66 | if (compareURLs(url, resource.uri)) return true; 67 | } 68 | } 69 | 70 | 71 | /** 72 | * Determines whether to URLs are equivalent. 73 | */ 74 | function compareURLs(a: URL, b: URL) { 75 | // Compare the two URIs piece-by-piece to short-circuit as early as possible. 76 | // NOTE: We don't compare the hashes because we're only interested in whole files that can be read 77 | return a.pathname === b.pathname && 78 | a.hostname === b.hostname && 79 | a.search === b.search && 80 | a.port === b.port && 81 | a.protocol === b.protocol; 82 | } 83 | -------------------------------------------------------------------------------- /src/read-json-schema.ts: -------------------------------------------------------------------------------- 1 | import { JsonSchema } from "@apidevtools/json-schema"; 2 | import { MultiError } from "./multi-error"; 3 | import { normalizeOptions, Options } from "./options"; 4 | import { processFile } from "./process-file"; 5 | 6 | /** 7 | * Reads a multi-file JSON Schema from any combination of files, URLs, and other sources 8 | * 9 | * @param location 10 | * The path of the JSON Schema to read. By default, this can be a filesystem path or a web URL. 11 | * A custom `readFile` implementation can provide support for additional locations, such as a 12 | * database, CMS, RSS feed, etc. 13 | * 14 | * @param options - Options that customize the behavior and override/extend default implementations. 15 | */ 16 | export async function readJsonSchema(location: string | URL, options?: Options): Promise { 17 | let opt = normalizeOptions(options); 18 | 19 | let schema = new JsonSchema({ 20 | cwd: opt.cwd, 21 | path: location, 22 | }); 23 | 24 | await processFile(schema.rootFile, opt); 25 | 26 | if (opt.continueOnError && schema.hasErrors) { 27 | throw new MultiError(schema); 28 | } 29 | 30 | return schema; 31 | } 32 | -------------------------------------------------------------------------------- /src/url-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A regular expression to test whether a string is an absolute URL 3 | */ 4 | export const absoluteUrlPattern = /^(\w{2,}):\/\//; 5 | 6 | 7 | /** 8 | * Returns a copy of the given URL, without a hash 9 | */ 10 | export function removeHash(url: URL): URL { 11 | let copy = new URL(url.href); 12 | copy.hash = ""; 13 | return copy; 14 | } 15 | 16 | 17 | /** 18 | * Returns the relative URL between two absolute URLs, if possible. 19 | * 20 | * NOTE: This implementation is based on "url-relative" by Juno Suárez 21 | * 22 | * @see https://github.com/junosuarez/url-relative 23 | * @see https://tools.ietf.org/html/rfc1808 24 | */ 25 | export function relativeURL(from: URL, to: URL): string | undefined { 26 | if ( 27 | from.host !== to.host || 28 | from.protocol !== to.protocol || 29 | from.username !== to.username || 30 | from.password !== to.password 31 | ) { 32 | // There is no relative URL between these two 33 | return undefined; 34 | } 35 | 36 | // left to right, look for closest common path segment 37 | let fromPath = from.pathname; 38 | let toPath = to.pathname; 39 | let fromSegments = fromPath.substr(1).split("/"); 40 | let toSegments = toPath.substr(1).split("/"); 41 | 42 | while (fromSegments[0] === toSegments[0]) { 43 | fromSegments.shift(); 44 | toSegments.shift(); 45 | } 46 | 47 | let length = fromSegments.length - toSegments.length; 48 | if (length > 0) { 49 | if (fromPath.endsWith("/")) { 50 | toSegments.unshift(".."); 51 | } 52 | while (length--) { 53 | toSegments.unshift(".."); 54 | } 55 | return toSegments.join("/"); 56 | } 57 | else if (length < 0) { 58 | return toSegments.join("/"); 59 | } 60 | else { 61 | length = toSegments.length - 1; 62 | while (length--) { 63 | toSegments.unshift(".."); 64 | } 65 | return toSegments.join("/"); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Resolves a relative URL, relative to a base URL 72 | */ 73 | export function resolveRelativeURL(base: string, relative: string): string { 74 | let baseSegments = base.split("/"); 75 | let relativeSegments = relative.split("/"); 76 | 77 | // Remove the last segment if it's a file name 78 | if (baseSegments[baseSegments.length - 1] !== "") { 79 | baseSegments.pop(); 80 | } 81 | 82 | for (let segment of relativeSegments) { 83 | if (segment === ".") continue; 84 | 85 | if (segment === ".." && baseSegments.length > 0) { 86 | baseSegments.pop(); 87 | } 88 | else { 89 | baseSegments.push(segment); 90 | } 91 | } 92 | 93 | return baseSegments.join("/"); 94 | } 95 | -------------------------------------------------------------------------------- /test/specs/circular-refs/ancestor/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "id": "#person", 4 | "properties": { 5 | "name": { "type": "string" }, 6 | "spouse": { "$ref": "schema.json#person" }, 7 | "children": { 8 | "type": "array", 9 | "items": { "$ref": "schema.json" } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/specs/circular-refs/ancestor/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/circular-refs/ancestor"; 9 | 10 | describe("Circular $refs to ancestor", () => { 11 | 12 | it("relative path", async () => { 13 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`)); 14 | assertSchema(schema, path.rel(`${dir}/schema.json`)); 15 | }); 16 | 17 | it("absolute path", async () => { 18 | let schema = await readJsonSchema(path.abs(`${dir}/schema.json`)); 19 | assertSchema(schema, path.abs(`${dir}/schema.json`)); 20 | }); 21 | 22 | it("URL", async () => { 23 | let schema = await readJsonSchema(path.url(`${dir}/schema.json`)); 24 | assertSchema(schema, path.url(`${dir}/schema.json`).href); 25 | }); 26 | 27 | function assertSchema (schema, filePath) { 28 | assert.schema(schema, { 29 | files: [{ 30 | url: path.url(`${dir}/schema.json`), 31 | path: filePath, 32 | mediaType: "application/json", 33 | resources: [ 34 | { 35 | uri: path.url(`${dir}/schema.json`, "#person"), 36 | data: schemaJSON, 37 | locationInFile: { 38 | tokens: [], 39 | path: "", 40 | hash: "#", 41 | }, 42 | references: [ 43 | { 44 | value: "schema.json#person", 45 | targetURI: path.url(`${dir}/schema.json`, "#person"), 46 | data: schemaJSON.properties.spouse, 47 | locationInFile: { 48 | tokens: ["properties", "spouse"], 49 | path: "/properties/spouse", 50 | hash: "#/properties/spouse", 51 | }, 52 | }, 53 | { 54 | value: "schema.json", 55 | targetURI: path.url(`${dir}/schema.json`), 56 | data: schemaJSON.properties.children.items, 57 | locationInFile: { 58 | tokens: ["properties", "children", "items"], 59 | path: "/properties/children/items", 60 | hash: "#/properties/children/items", 61 | }, 62 | }, 63 | ], 64 | }, 65 | ] 66 | }], 67 | }); 68 | } 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /test/specs/circular-refs/cross-reference/child.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "child", 3 | "properties": { 4 | "name": { "type": "string" }, 5 | "parent": { "$ref": "parent.json" } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/specs/circular-refs/cross-reference/parent.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "parent", 3 | "properties": { 4 | "name": { "type": "string" }, 5 | "children": { 6 | "type": "array", 7 | "items": { "$ref": "child.json" } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/specs/circular-refs/cross-reference/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2019-09/schema", 3 | "$defs": { 4 | "parent": { "$ref": "parent.json" }, 5 | "child": { "$ref": "child.json" } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/specs/circular-refs/cross-reference/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | const parentJSON = require("./parent.json"); 8 | const childJSON = require("./child.json"); 9 | 10 | const dir = "test/specs/circular-refs/cross-reference"; 11 | 12 | describe("Circular cross-references", () => { 13 | 14 | it("relative path", async () => { 15 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`)); 16 | assertSchema(schema, 17 | path.rel(`${dir}/schema.json`), 18 | path.rel(`${dir}/parent.json`), 19 | path.rel(`${dir}/child.json`), 20 | ); 21 | }); 22 | 23 | it("absolute path", async () => { 24 | let schema = await readJsonSchema(path.abs(`${dir}/schema.json`)); 25 | assertSchema(schema, 26 | path.abs(`${dir}/schema.json`), 27 | path.abs(`${dir}/parent.json`), 28 | path.abs(`${dir}/child.json`), 29 | ); 30 | }); 31 | 32 | it("URL", async () => { 33 | let schema = await readJsonSchema(path.url(`${dir}/schema.json`)); 34 | assertSchema(schema, 35 | path.url(`${dir}/schema.json`).href, 36 | path.url(`${dir}/parent.json`).href, 37 | path.url(`${dir}/child.json`).href, 38 | ); 39 | }); 40 | 41 | function assertSchema (schema, rootFilePath, parentPath, childPath) { 42 | assert.schema(schema, { 43 | files: [ 44 | { 45 | url: path.url(`${dir}/schema.json`), 46 | path: rootFilePath, 47 | mediaType: "application/json", 48 | resources: [ 49 | { 50 | uri: path.url(`${dir}/schema.json`), 51 | data: schemaJSON, 52 | locationInFile: { 53 | tokens: [], 54 | path: "", 55 | hash: "#", 56 | }, 57 | references: [ 58 | { 59 | value: "parent.json", 60 | targetURI: path.url(`${dir}/parent.json`), 61 | data: schemaJSON.$defs.parent, 62 | locationInFile: { 63 | tokens: ["$defs", "parent"], 64 | path: "/$defs/parent", 65 | hash: "#/$defs/parent", 66 | }, 67 | }, 68 | { 69 | value: "child.json", 70 | targetURI: path.url(`${dir}/child.json`), 71 | data: schemaJSON.$defs.child, 72 | locationInFile: { 73 | tokens: ["$defs", "child"], 74 | path: "/$defs/child", 75 | hash: "#/$defs/child", 76 | }, 77 | }, 78 | ], 79 | }, 80 | ] 81 | }, 82 | { 83 | url: path.url(`${dir}/parent.json`), 84 | path: parentPath, 85 | mediaType: "application/json", 86 | resources: [ 87 | { 88 | uri: path.url(`${dir}/parent`), 89 | data: parentJSON, 90 | locationInFile: { 91 | tokens: [], 92 | path: "", 93 | hash: "#", 94 | }, 95 | references: [ 96 | { 97 | value: "child.json", 98 | targetURI: path.url(`${dir}/child.json`), 99 | data: parentJSON.properties.children.items, 100 | locationInFile: { 101 | tokens: ["properties", "children", "items"], 102 | path: "/properties/children/items", 103 | hash: "#/properties/children/items", 104 | }, 105 | }, 106 | ], 107 | }, 108 | ], 109 | }, 110 | { 111 | url: path.url(`${dir}/child.json`), 112 | path: childPath, 113 | mediaType: "application/json", 114 | resources: [ 115 | { 116 | uri: path.url(`${dir}/child`), 117 | data: childJSON, 118 | locationInFile: { 119 | tokens: [], 120 | path: "", 121 | hash: "#", 122 | }, 123 | references: [ 124 | { 125 | value: "parent.json", 126 | targetURI: path.url(`${dir}/parent.json`), 127 | data: childJSON.properties.parent, 128 | locationInFile: { 129 | tokens: ["properties", "parent"], 130 | path: "/properties/parent", 131 | hash: "#/properties/parent", 132 | }, 133 | }, 134 | ], 135 | }, 136 | ], 137 | }, 138 | ], 139 | }); 140 | } 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /test/specs/circular-refs/direct/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "id": "#person", 4 | "$ref": "schema.json", 5 | "properties": { 6 | "name": { "type": "string" } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/specs/circular-refs/direct/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/circular-refs/direct"; 9 | 10 | describe("Circular $refs to itself", () => { 11 | 12 | it("relative path", async () => { 13 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`)); 14 | assertSchema(schema, path.rel(`${dir}/schema.json`)); 15 | }); 16 | 17 | it("absolute path", async () => { 18 | let schema = await readJsonSchema(path.abs(`${dir}/schema.json`)); 19 | assertSchema(schema, path.abs(`${dir}/schema.json`)); 20 | }); 21 | 22 | it("URL", async () => { 23 | let schema = await readJsonSchema(path.url(`${dir}/schema.json`)); 24 | assertSchema(schema, path.url(`${dir}/schema.json`).href); 25 | }); 26 | 27 | function assertSchema (schema, filePath) { 28 | assert.schema(schema, { 29 | files: [{ 30 | url: path.url(`${dir}/schema.json`), 31 | path: filePath, 32 | mediaType: "application/json", 33 | data: schemaJSON, 34 | resources: [ 35 | { 36 | uri: path.url(`${dir}/schema.json`, "#person"), 37 | data: schemaJSON, 38 | locationInFile: { 39 | tokens: [], 40 | path: "", 41 | hash: "#", 42 | }, 43 | references: [ 44 | { 45 | value: "schema.json", 46 | targetURI: path.url(`${dir}/schema.json`), 47 | data: schemaJSON, 48 | locationInFile: { 49 | tokens: [], 50 | path: "", 51 | hash: "#", 52 | }, 53 | }, 54 | ], 55 | }, 56 | ] 57 | }], 58 | }); 59 | } 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /test/specs/exports.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const commonJSExport = require("../../"); 4 | const { default: defaultExport } = require("../../"); 5 | const { readJsonSchema, Anchor, File, JsonSchema, MultiError, Pointer, Reference, Resource, SchemaError } = require("../../"); 6 | const { expect } = require("chai"); 7 | const { host } = require("@jsdevtools/host-environment"); 8 | 9 | describe("json-schema-reader package exports", () => { 10 | 11 | function isJsonSchemaReader (reader) { 12 | expect(reader).to.be.an("object"); 13 | expect(reader.read).to.be.a("function").with.property("name", "readJsonSchema"); 14 | return true; 15 | } 16 | 17 | it("should export the jsonSchemaReader object as the default CommonJS export", () => { 18 | if (host.node) { 19 | expect(commonJSExport).to.satisfy(isJsonSchemaReader); 20 | } 21 | else { 22 | // Browser tests are only ESM, not CommonJS 23 | expect(commonJSExport).to.be.a("Module"); 24 | } 25 | 26 | }); 27 | 28 | it("should export the jsonSchemaReader object as the default ESM export", () => { 29 | expect(defaultExport).to.satisfy(isJsonSchemaReader); 30 | }); 31 | 32 | it("should export the readJsonSchema() function as a named export", () => { 33 | expect(readJsonSchema).to.be.a("function"); 34 | expect(readJsonSchema.name).to.equal("readJsonSchema"); 35 | }); 36 | 37 | it("should re-export @apidevtools/json-schema classes as named exports", () => { 38 | expect(Anchor).to.be.a("function").with.property("name", "Anchor"); 39 | expect(File).to.be.a("function").with.property("name", "File"); 40 | expect(JsonSchema).to.be.a("function").with.property("name", "JsonSchema"); 41 | expect(MultiError).to.be.a("function").with.property("name", "MultiError"); 42 | expect(Pointer).to.be.a("function").with.property("name", "Pointer"); 43 | expect(Reference).to.be.a("function").with.property("name", "Reference"); 44 | expect(Resource).to.be.a("function").with.property("name", "Resource"); 45 | expect(SchemaError).to.be.a("function").with.property("name", "SchemaError"); 46 | }); 47 | 48 | it("should not export anything else", () => { 49 | let namedExports = [ 50 | "readJsonSchema", "Anchor", "File", "JsonSchema", "MultiError", 51 | "Pointer", "Reference", "Resource", "SchemaError" 52 | ]; 53 | 54 | if (host.node) { 55 | expect(commonJSExport).to.have.same.keys("default", "read", ...namedExports); 56 | } 57 | else { 58 | expect(commonJSExport).to.have.same.keys("default", ...namedExports); 59 | } 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/specs/multi-file/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "address", 3 | "$anchor": "address", 4 | "title": "Address", 5 | "type": "object", 6 | "properties": { 7 | "street": { 8 | "type": "array", 9 | "minItems": 1, 10 | "items": { "$ref": "non-empty-string" }, 11 | "description": "Each line of the street address" 12 | }, 13 | "city": { 14 | "$ref": "schema.json#non-empty-string", 15 | "description": "The name of the city or town" 16 | }, 17 | "state": { 18 | "$ref": "schema.json#/$defs/nonEmptyString", 19 | "description": "The name of the state or province" 20 | }, 21 | "postalCode": { 22 | "$ref": "../../specs/multi-file/schema.json#non-empty-string", 23 | "description": "The postal code (aka \"zip code\")" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/specs/multi-file/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "person", 3 | "title": "Person", 4 | "type": "object", 5 | "required": ["name", "age", "homeAddress"], 6 | "properties": { 7 | "name": { 8 | "$anchor": "name", 9 | "type": "object", 10 | "required": ["first", "last"], 11 | "properties": { 12 | "first": { 13 | "$ref": "non-empty-string", 14 | "description": "The person's first name" 15 | }, 16 | "middle": { 17 | "$ref": "schema.json#non-empty-string", 18 | "description": "The person's middle name, if any" 19 | }, 20 | "last": { 21 | "$ref": "schema.json#/$defs/nonEmptyString", 22 | "description": "The person's last name" 23 | } 24 | } 25 | }, 26 | "age": { 27 | "type": "integer", 28 | "minimum": 0, 29 | "description": "The person's age, in whole years" 30 | }, 31 | "homeAddress": { 32 | "$ref": "address.json", 33 | "description": "The person's home address" 34 | }, 35 | "workAddress": { 36 | "$ref": "address.json#address", 37 | "description": "The person's work address" 38 | }, 39 | "schoolAddress": { 40 | "$ref": "foo/bar/../../address.json", 41 | "description": "The person's school address" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/specs/multi-file/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "nonEmptyString": { 4 | "$id": "non-empty-string", 5 | "$anchor": "non-empty-string", 6 | "title": "Non-Empty String", 7 | "type": "string", 8 | "minLength": 1, 9 | "pattern": "\\S", 10 | "description": "A string containing at least one non-whitespace character" 11 | }, 12 | "person": { 13 | "$ref": "person.json" 14 | }, 15 | "name": { 16 | "$ref": "person.json#name" 17 | }, 18 | "address": { 19 | "$ref": "address.json" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/specs/multi-file/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../"); 4 | const path = require("../../utils/path"); 5 | const assert = require("../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | const personJSON = require("./person.json"); 8 | const addressJSON = require("./address.json"); 9 | 10 | const dir = "test/specs/multi-file"; 11 | 12 | describe("Multi-file schema with cross-references", () => { 13 | 14 | it("relative path", async () => { 15 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`)); 16 | assertSchema(schema, 17 | path.rel(`${dir}/schema.json`), 18 | path.rel(`${dir}/person.json`), 19 | path.rel(`${dir}/address.json`), 20 | ); 21 | }); 22 | 23 | it("absolute path", async () => { 24 | let schema = await readJsonSchema(path.abs(`${dir}/schema.json`)); 25 | assertSchema(schema, 26 | path.abs(`${dir}/schema.json`), 27 | path.abs(`${dir}/person.json`), 28 | path.abs(`${dir}/address.json`), 29 | ); 30 | }); 31 | 32 | it("URL", async () => { 33 | let schema = await readJsonSchema(path.url(`${dir}/schema.json`)); 34 | assertSchema(schema, 35 | path.url(`${dir}/schema.json`).href, 36 | path.url(`${dir}/person.json`).href, 37 | path.url(`${dir}/address.json`).href, 38 | ); 39 | }); 40 | 41 | function assertSchema (schema, rootFilePath, personPath, addressPath) { 42 | assert.schema(schema, { 43 | files: [ 44 | { 45 | url: path.url(`${dir}/schema.json`), 46 | path: rootFilePath, 47 | mediaType: "application/json", 48 | resources: [ 49 | { 50 | uri: path.url(`${dir}/schema.json`), 51 | data: schemaJSON, 52 | locationInFile: { 53 | tokens: [], 54 | path: "", 55 | hash: "#", 56 | }, 57 | references: [ 58 | { 59 | value: "person.json", 60 | targetURI: path.url(`${dir}/person.json`), 61 | data: schemaJSON.$defs.person, 62 | locationInFile: { 63 | tokens: ["$defs", "person"], 64 | path: "/$defs/person", 65 | hash: "#/$defs/person", 66 | }, 67 | }, 68 | { 69 | value: "person.json#name", 70 | targetURI: path.url(`${dir}/person.json`, "#name"), 71 | data: schemaJSON.$defs.name, 72 | locationInFile: { 73 | tokens: ["$defs", "name"], 74 | path: "/$defs/name", 75 | hash: "#/$defs/name", 76 | }, 77 | }, 78 | { 79 | value: "address.json", 80 | targetURI: path.url(`${dir}/address.json`), 81 | data: schemaJSON.$defs.address, 82 | locationInFile: { 83 | tokens: ["$defs", "address"], 84 | path: "/$defs/address", 85 | hash: "#/$defs/address", 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | uri: path.url(`${dir}/non-empty-string`), 92 | data: schemaJSON.$defs.nonEmptyString, 93 | locationInFile: { 94 | tokens: ["$defs", "nonEmptyString"], 95 | path: "/$defs/nonEmptyString", 96 | hash: "#/$defs/nonEmptyString", 97 | }, 98 | anchors: [ 99 | { 100 | name: "non-empty-string", 101 | uri: path.url(`${dir}/non-empty-string`, "#non-empty-string"), 102 | data: schemaJSON.$defs.nonEmptyString, 103 | locationInFile: { 104 | tokens: ["$defs", "nonEmptyString"], 105 | path: "/$defs/nonEmptyString", 106 | hash: "#/$defs/nonEmptyString", 107 | } 108 | } 109 | ], 110 | }, 111 | ] 112 | }, 113 | { 114 | url: path.url(`${dir}/person.json`), 115 | path: personPath, 116 | mediaType: "application/json", 117 | resources: [ 118 | { 119 | uri: path.url(`${dir}/person`), 120 | data: personJSON, 121 | locationInFile: { 122 | tokens: [], 123 | path: "", 124 | hash: "#", 125 | }, 126 | anchors: [ 127 | { 128 | name: "name", 129 | uri: path.url(`${dir}/person`, "#name"), 130 | data: personJSON.properties.name, 131 | locationInFile: { 132 | tokens: ["properties", "name"], 133 | path: "/properties/name", 134 | hash: "#/properties/name", 135 | } 136 | } 137 | ], 138 | references: [ 139 | { 140 | value: "non-empty-string", 141 | targetURI: path.url(`${dir}/non-empty-string`), 142 | data: personJSON.properties.name.properties.first, 143 | locationInFile: { 144 | tokens: ["properties", "name", "properties", "first"], 145 | path: "/properties/name/properties/first", 146 | hash: "#/properties/name/properties/first", 147 | }, 148 | }, 149 | { 150 | value: "schema.json#non-empty-string", 151 | targetURI: path.url(`${dir}/schema.json`, "#non-empty-string"), 152 | data: personJSON.properties.name.properties.middle, 153 | locationInFile: { 154 | tokens: ["properties", "name", "properties", "middle"], 155 | path: "/properties/name/properties/middle", 156 | hash: "#/properties/name/properties/middle", 157 | }, 158 | }, 159 | { 160 | value: "schema.json#/$defs/nonEmptyString", 161 | targetURI: path.url(`${dir}/schema.json`, "#/$defs/nonEmptyString"), 162 | data: personJSON.properties.name.properties.last, 163 | locationInFile: { 164 | tokens: ["properties", "name", "properties", "last"], 165 | path: "/properties/name/properties/last", 166 | hash: "#/properties/name/properties/last", 167 | }, 168 | }, 169 | { 170 | value: "address.json", 171 | targetURI: path.url(`${dir}/address.json`), 172 | data: personJSON.properties.homeAddress, 173 | locationInFile: { 174 | tokens: ["properties", "homeAddress"], 175 | path: "/properties/homeAddress", 176 | hash: "#/properties/homeAddress", 177 | }, 178 | }, 179 | { 180 | value: "address.json#address", 181 | targetURI: path.url(`${dir}/address.json`, "#address"), 182 | data: personJSON.properties.workAddress, 183 | locationInFile: { 184 | tokens: ["properties", "workAddress"], 185 | path: "/properties/workAddress", 186 | hash: "#/properties/workAddress", 187 | }, 188 | }, 189 | { 190 | value: "foo/bar/../../address.json", 191 | targetURI: path.url(`${dir}/address.json`), 192 | data: personJSON.properties.schoolAddress, 193 | locationInFile: { 194 | tokens: ["properties", "schoolAddress"], 195 | path: "/properties/schoolAddress", 196 | hash: "#/properties/schoolAddress", 197 | }, 198 | }, 199 | ], 200 | }, 201 | ], 202 | }, 203 | { 204 | url: path.url(`${dir}/address.json`), 205 | path: addressPath, 206 | mediaType: "application/json", 207 | resources: [ 208 | { 209 | uri: path.url(`${dir}/address`), 210 | data: addressJSON, 211 | locationInFile: { 212 | tokens: [], 213 | path: "", 214 | hash: "#", 215 | }, 216 | anchors: [ 217 | { 218 | name: "address", 219 | uri: path.url(`${dir}/address`, "#address"), 220 | data: addressJSON, 221 | locationInFile: { 222 | tokens: [], 223 | path: "", 224 | hash: "#", 225 | } 226 | } 227 | ], 228 | references: [ 229 | { 230 | value: "non-empty-string", 231 | targetURI: path.url(`${dir}/non-empty-string`), 232 | data: addressJSON.properties.street.items, 233 | locationInFile: { 234 | tokens: ["properties", "street", "items"], 235 | path: "/properties/street/items", 236 | hash: "#/properties/street/items", 237 | }, 238 | }, 239 | { 240 | value: "schema.json#non-empty-string", 241 | targetURI: path.url(`${dir}/schema.json`, "#non-empty-string"), 242 | data: addressJSON.properties.city, 243 | locationInFile: { 244 | tokens: ["properties", "city"], 245 | path: "/properties/city", 246 | hash: "#/properties/city", 247 | }, 248 | }, 249 | { 250 | value: "schema.json#/$defs/nonEmptyString", 251 | targetURI: path.url(`${dir}/schema.json`, "#/$defs/nonEmptyString"), 252 | data: addressJSON.properties.state, 253 | locationInFile: { 254 | tokens: ["properties", "state"], 255 | path: "/properties/state", 256 | hash: "#/properties/state", 257 | }, 258 | }, 259 | { 260 | value: "../../specs/multi-file/schema.json#non-empty-string", 261 | targetURI: path.url(`${dir}/schema.json`, "#non-empty-string"), 262 | data: addressJSON.properties.postalCode, 263 | locationInFile: { 264 | tokens: ["properties", "postalCode"], 265 | path: "/properties/postalCode", 266 | hash: "#/properties/postalCode", 267 | }, 268 | }, 269 | ], 270 | }, 271 | ], 272 | }, 273 | ], 274 | }); 275 | } 276 | 277 | }); 278 | -------------------------------------------------------------------------------- /test/specs/one-file/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "person", 3 | "title": "Person", 4 | "type": "object", 5 | "required": ["name", "age", "homeAddress"], 6 | "properties": { 7 | "name": { 8 | "type": "object", 9 | "required": ["first", "last"], 10 | "properties": { 11 | "first": { 12 | "$ref": "non-empty-string", 13 | "description": "The person's first name" 14 | }, 15 | "middle": { 16 | "$ref": "person#non-empty-string", 17 | "description": "The person's middle name, if any" 18 | }, 19 | "last": { 20 | "$ref": "#/$defs/nonEmptyString", 21 | "description": "The person's last name" 22 | } 23 | } 24 | }, 25 | "age": { 26 | "type": "integer", 27 | "minimum": 0, 28 | "description": "The person's age, in whole years" 29 | }, 30 | "homeAddress": { 31 | "$ref": "address", 32 | "description": "The person's home address" 33 | }, 34 | "workAddress": { 35 | "$ref": "person#address", 36 | "description": "The person's work address" 37 | }, 38 | "schoolAddress": { 39 | "$ref": "#/$defs/address", 40 | "description": "The person's school address" 41 | } 42 | }, 43 | "$defs": { 44 | "nonEmptyString": { 45 | "$id": "non-empty-string", 46 | "$anchor": "non-empty-string", 47 | "title": "Non-Empty String", 48 | "type": "string", 49 | "minLength": 1, 50 | "pattern": "\\S", 51 | "description": "A string containing at least one non-whitespace character" 52 | }, 53 | "address": { 54 | "$id": "address", 55 | "$anchor": "address", 56 | "title": "Address", 57 | "type": "object", 58 | "properties": { 59 | "street": { 60 | "type": "array", 61 | "minItems": 1, 62 | "items": { "$ref": "non-empty-string" }, 63 | "description": "Each line of the street address" 64 | }, 65 | "city": { 66 | "$ref": "person#non-empty-string", 67 | "description": "The name of the city or town" 68 | }, 69 | "state": { 70 | "$ref": "person#/$defs/nonEmptyString", 71 | "description": "The name of the state or province" 72 | }, 73 | "postalCode": { 74 | "$ref": "person#/$defs/nonEmptyString", 75 | "description": "The postal code (aka \"zip code\")" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/specs/one-file/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../"); 4 | const path = require("../../utils/path"); 5 | const assert = require("../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/one-file"; 9 | 10 | describe("Single-file schema with internal $refs", () => { 11 | 12 | it("relative path", async () => { 13 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`)); 14 | assertSchema(schema, path.rel(`${dir}/schema.json`)); 15 | }); 16 | 17 | it("absolute path", async () => { 18 | let schema = await readJsonSchema(path.abs(`${dir}/schema.json`)); 19 | assertSchema(schema, path.abs(`${dir}/schema.json`)); 20 | }); 21 | 22 | it("URL", async () => { 23 | let schema = await readJsonSchema(path.url(`${dir}/schema.json`)); 24 | assertSchema(schema, path.url(`${dir}/schema.json`).href); 25 | }); 26 | 27 | function assertSchema (schema, filePath) { 28 | assert.schema(schema, { 29 | files: [{ 30 | url: path.url(`${dir}/schema.json`), 31 | path: filePath, 32 | mediaType: "application/json", 33 | data: schemaJSON, 34 | resources: [ 35 | { 36 | uri: path.url(`${dir}/person`), 37 | data: schemaJSON, 38 | locationInFile: { 39 | tokens: [], 40 | path: "", 41 | hash: "#", 42 | }, 43 | references: [ 44 | { 45 | value: "non-empty-string", 46 | targetURI: path.url(`${dir}/non-empty-string`), 47 | data: schemaJSON.properties.name.properties.first, 48 | locationInFile: { 49 | tokens: ["properties", "name", "properties", "first"], 50 | path: "/properties/name/properties/first", 51 | hash: "#/properties/name/properties/first", 52 | }, 53 | }, 54 | { 55 | value: "person#non-empty-string", 56 | targetURI: path.url(`${dir}/person`, "#non-empty-string"), 57 | data: schemaJSON.properties.name.properties.middle, 58 | locationInFile: { 59 | tokens: ["properties", "name", "properties", "middle"], 60 | path: "/properties/name/properties/middle", 61 | hash: "#/properties/name/properties/middle", 62 | }, 63 | }, 64 | { 65 | value: "#/$defs/nonEmptyString", 66 | targetURI: path.url(`${dir}/person`, "#/$defs/nonEmptyString"), 67 | data: schemaJSON.properties.name.properties.last, 68 | locationInFile: { 69 | tokens: ["properties", "name", "properties", "last"], 70 | path: "/properties/name/properties/last", 71 | hash: "#/properties/name/properties/last", 72 | }, 73 | }, 74 | { 75 | value: "address", 76 | targetURI: path.url(`${dir}/address`), 77 | data: schemaJSON.properties.homeAddress, 78 | locationInFile: { 79 | tokens: ["properties", "homeAddress"], 80 | path: "/properties/homeAddress", 81 | hash: "#/properties/homeAddress", 82 | }, 83 | }, 84 | { 85 | value: "person#address", 86 | targetURI: path.url(`${dir}/person`, "#address"), 87 | data: schemaJSON.properties.workAddress, 88 | locationInFile: { 89 | tokens: ["properties", "workAddress"], 90 | path: "/properties/workAddress", 91 | hash: "#/properties/workAddress", 92 | }, 93 | }, 94 | { 95 | value: "#/$defs/address", 96 | targetURI: path.url(`${dir}/person`, "#/$defs/address"), 97 | data: schemaJSON.properties.schoolAddress, 98 | locationInFile: { 99 | tokens: ["properties", "schoolAddress"], 100 | path: "/properties/schoolAddress", 101 | hash: "#/properties/schoolAddress", 102 | }, 103 | }, 104 | ], 105 | }, 106 | { 107 | uri: path.url(`${dir}/non-empty-string`), 108 | data: schemaJSON.$defs.nonEmptyString, 109 | locationInFile: { 110 | tokens: ["$defs", "nonEmptyString"], 111 | path: "/$defs/nonEmptyString", 112 | hash: "#/$defs/nonEmptyString", 113 | }, 114 | anchors: [ 115 | { 116 | name: "non-empty-string", 117 | uri: path.url(`${dir}/non-empty-string`, "#non-empty-string"), 118 | data: schemaJSON.$defs.nonEmptyString, 119 | locationInFile: { 120 | tokens: ["$defs", "nonEmptyString"], 121 | path: "/$defs/nonEmptyString", 122 | hash: "#/$defs/nonEmptyString", 123 | } 124 | } 125 | ], 126 | }, 127 | { 128 | uri: path.url(`${dir}/address`), 129 | data: schemaJSON.$defs.address, 130 | locationInFile: { 131 | tokens: ["$defs", "address"], 132 | path: "/$defs/address", 133 | hash: "#/$defs/address", 134 | }, 135 | anchors: [ 136 | { 137 | name: "address", 138 | uri: path.url(`${dir}/address`, "#address"), 139 | data: schemaJSON.$defs.address, 140 | locationInFile: { 141 | tokens: ["$defs", "address"], 142 | path: "/$defs/address", 143 | hash: "#/$defs/address", 144 | } 145 | } 146 | ], 147 | references: [ 148 | { 149 | value: "non-empty-string", 150 | targetURI: path.url(`${dir}/non-empty-string`), 151 | data: schemaJSON.$defs.address.properties.street.items, 152 | locationInFile: { 153 | tokens: ["$defs", "address", "properties", "street", "items"], 154 | path: "/$defs/address/properties/street/items", 155 | hash: "#/$defs/address/properties/street/items", 156 | }, 157 | }, 158 | { 159 | value: "person#non-empty-string", 160 | targetURI: path.url(`${dir}/person`, "#non-empty-string"), 161 | data: schemaJSON.$defs.address.properties.city, 162 | locationInFile: { 163 | tokens: ["$defs", "address", "properties", "city"], 164 | path: "/$defs/address/properties/city", 165 | hash: "#/$defs/address/properties/city", 166 | }, 167 | }, 168 | { 169 | value: "person#/$defs/nonEmptyString", 170 | targetURI: path.url(`${dir}/person`, "#/$defs/nonEmptyString"), 171 | data: schemaJSON.$defs.address.properties.state, 172 | locationInFile: { 173 | tokens: ["$defs", "address", "properties", "state"], 174 | path: "/$defs/address/properties/state", 175 | hash: "#/$defs/address/properties/state", 176 | }, 177 | }, 178 | { 179 | value: "person#/$defs/nonEmptyString", 180 | targetURI: path.url(`${dir}/person`, "#/$defs/nonEmptyString"), 181 | data: schemaJSON.$defs.address.properties.postalCode, 182 | locationInFile: { 183 | tokens: ["$defs", "address", "properties", "postalCode"], 184 | path: "/$defs/address/properties/postalCode", 185 | hash: "#/$defs/address/properties/postalCode", 186 | }, 187 | }, 188 | ], 189 | }, 190 | ] 191 | }], 192 | }); 193 | } 194 | 195 | }); 196 | -------------------------------------------------------------------------------- /test/specs/options/continue-on-error/address.json: -------------------------------------------------------------------------------- 1 | { syntax: error } 2 | -------------------------------------------------------------------------------- /test/specs/options/continue-on-error/company.xyz: -------------------------------------------------------------------------------- 1 | This file has an unknown file extension, so it will be read as binary data 2 | -------------------------------------------------------------------------------- /test/specs/options/continue-on-error/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "#person", 3 | "title": "Person", 4 | "type": "object", 5 | "required": ["name", "age", "homeAddress"], 6 | "properties": { 7 | "name": { 8 | "$anchor": "#name", 9 | "type": "object", 10 | "required": ["first", "last"], 11 | "properties": { 12 | "first": { 13 | "$ref": "non-empty-string", 14 | "description": "The person's first name" 15 | }, 16 | "middle": { 17 | "$ref": "#non-empty-string", 18 | "description": "The person's middle name, if any" 19 | }, 20 | "last": { 21 | "$ref": "schema.json#/$defs/nonEmptyString", 22 | "description": "The person's last name" 23 | } 24 | } 25 | }, 26 | "age": { 27 | "type": "integer", 28 | "minimum": 0, 29 | "description": "The person's age, in whole years" 30 | }, 31 | "homeAddress": { 32 | "$ref": "address.json", 33 | "description": "The person's home address" 34 | }, 35 | "workAddress": { 36 | "$ref": "address.json#address", 37 | "description": "The person's work address" 38 | }, 39 | "schoolAddress": { 40 | "$ref": "invalid/path/address.json", 41 | "description": "The person's school address" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/specs/options/continue-on-error/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "nonEmptyString": { 4 | "$id": "non-empty-string", 5 | "$anchor": "Non Empty String", 6 | "type": "string", 7 | "minLength": 1, 8 | "pattern": "\\S", 9 | "description": "A string containing at least one non-whitespace character" 10 | }, 11 | "person": { 12 | "$ref": "person.json" 13 | }, 14 | "name": { 15 | "$ref": "person.json#name" 16 | }, 17 | "age": { 18 | "$ref": "age.json" 19 | }, 20 | "address": { 21 | "$ref": "address.json" 22 | }, 23 | "company": { 24 | "$ref": "company.xyz" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/specs/options/continue-on-error/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const { host } = require("@jsdevtools/host-environment"); 5 | const path = require("../../../utils/path"); 6 | const assert = require("../../../utils/assert"); 7 | const schemaJSON = require("./schema.json"); 8 | const personJSON = require("./person.json"); 9 | 10 | const dir = "test/specs/options/continue-on-error"; 11 | 12 | describe("Options: continueOnError", () => { 13 | 14 | it("relative path", async () => { 15 | try { 16 | await readJsonSchema(path.rel(`${dir}/schema.json`), { continueOnError: true }); 17 | assert.failed(); 18 | } 19 | catch (error) { 20 | assertError(error, 21 | path.rel(`${dir}/schema.json`), 22 | path.rel(`${dir}/person.json`), 23 | path.rel(`${dir}/age.json`), 24 | path.rel(`${dir}/address.json`), 25 | path.rel(`${dir}/company.xyz`), 26 | path.rel(`${dir}/invalid/path/address.json`), 27 | ); 28 | } 29 | }); 30 | 31 | it("absolute path", async () => { 32 | try { 33 | await readJsonSchema(path.abs(`${dir}/schema.json`), { continueOnError: true }); 34 | assert.failed(); 35 | } 36 | catch (error) { 37 | assertError(error, 38 | path.abs(`${dir}/schema.json`), 39 | path.abs(`${dir}/person.json`), 40 | path.abs(`${dir}/age.json`), 41 | path.abs(`${dir}/address.json`), 42 | path.abs(`${dir}/company.xyz`), 43 | path.abs(`${dir}/invalid/path/address.json`), 44 | ); 45 | } 46 | }); 47 | 48 | it("URL", async () => { 49 | try { 50 | await readJsonSchema(path.url(`${dir}/schema.json`), { continueOnError: true }); 51 | assert.failed(); 52 | } 53 | catch (error) { 54 | assertError(error, 55 | path.url(`${dir}/schema.json`).href, 56 | path.url(`${dir}/person.json`).href, 57 | path.url(`${dir}/age.json`).href, 58 | path.url(`${dir}/address.json`).href, 59 | path.url(`${dir}/company.xyz`).href, 60 | path.url(`${dir}/invalid/path/address.json`).href, 61 | ); 62 | } 63 | }); 64 | 65 | function assertError (error, rootFilePath, personPath, agePath, addressPath, companyPath, invalidPath) { 66 | let error1 = { 67 | name: "SchemaError", 68 | code: "ERR_INDEX", 69 | input: "Non Empty String", 70 | message: 71 | `Error in ${rootFilePath} at /$defs/nonEmptyString/$anchor\n` + 72 | " $anchor contains illegal characters.", 73 | originalError: { 74 | name: "SyntaxError", 75 | code: "ERR_INVALID_ANCHOR", 76 | message: "$anchor contains illegal characters.", 77 | input: "Non Empty String", 78 | }, 79 | }; 80 | let error2 = { 81 | name: "SchemaError", 82 | code: "ERR_INDEX", 83 | input: "#person", 84 | message: 85 | `Error in ${personPath} at /$id\n` + 86 | " $id cannot include a fragment: #person", 87 | originalError: { 88 | name: "URIError", 89 | code: "ERR_INVALID_URL", 90 | message: "$id cannot include a fragment: #person", 91 | input: "#person", 92 | }, 93 | }; 94 | let error3 = { 95 | name: "SchemaError", 96 | code: "ERR_INDEX", 97 | input: "#name", 98 | message: 99 | `Error in ${personPath} at /properties/name/$anchor\n` + 100 | " $anchor cannot start with a \"#\" character.", 101 | originalError: { 102 | name: "SyntaxError", 103 | code: "ERR_INVALID_ANCHOR", 104 | message: "$anchor cannot start with a \"#\" character.", 105 | input: "#name", 106 | }, 107 | }; 108 | let error4 = { 109 | name: "SchemaError", 110 | code: "ERR_READ", 111 | errno: host.browser ? undefined : -4058, 112 | syscall: host.browser ? undefined : "stat", 113 | path: host.browser ? undefined : path.abs(`${dir}/age.json`), 114 | message: /^Unable to read .*age\.json\n /, 115 | originalError: { 116 | name: host.browser ? "URIError" : "Error", 117 | code: host.browser ? undefined : "ENOENT", 118 | message: /age\.json/, 119 | errno: host.browser ? undefined : -4058, 120 | syscall: host.browser ? undefined : "stat", 121 | path: host.browser ? undefined : path.abs(`${dir}/age.json`), 122 | }, 123 | }; 124 | let error5 = { 125 | name: "SchemaError", 126 | code: "ERR_PARSE", 127 | message: /^Unable to parse .*address\.json\n /, 128 | originalError: { 129 | name: "SyntaxError", 130 | message: /Invalid|expected/, 131 | }, 132 | }; 133 | let error6 = { 134 | name: "SchemaError", 135 | code: "ERR_READ", 136 | errno: host.browser ? undefined : -4058, 137 | syscall: host.browser ? undefined : "stat", 138 | path: host.browser ? undefined : path.abs(`${dir}/invalid/path/address.json`), 139 | message: /^Unable to read .*address\.json\n /, 140 | originalError: { 141 | name: host.browser ? "URIError" : "Error", 142 | code: host.browser ? undefined : "ENOENT", 143 | message: /address\.json/, 144 | errno: host.browser ? undefined : -4058, 145 | syscall: host.browser ? undefined : "stat", 146 | path: host.browser ? undefined : path.abs(`${dir}/invalid/path/address.json`), 147 | }, 148 | }; 149 | 150 | assert.error(error, { 151 | name: "MultiError", 152 | code: "ERR_MULTIPLE", 153 | message: `6 errors occurred while reading ${rootFilePath}`, 154 | errors: [error1, error2, error3, error4, error5, error6] 155 | }); 156 | 157 | assert.schema(error.schema, { 158 | hasErrors: true, 159 | files: [ 160 | { 161 | url: path.url(`${dir}/schema.json`), 162 | path: rootFilePath, 163 | mediaType: "application/json", 164 | errors: [error1], 165 | resources: [ 166 | { 167 | uri: path.url(`${dir}/schema.json`), 168 | data: schemaJSON, 169 | locationInFile: { 170 | tokens: [], 171 | path: "", 172 | hash: "#", 173 | }, 174 | references: [ 175 | { 176 | value: "person.json", 177 | targetURI: path.url(`${dir}/person.json`), 178 | data: schemaJSON.$defs.person, 179 | locationInFile: { 180 | tokens: ["$defs", "person"], 181 | path: "/$defs/person", 182 | hash: "#/$defs/person", 183 | }, 184 | }, 185 | { 186 | value: "person.json#name", 187 | targetURI: path.url(`${dir}/person.json`, "#name"), 188 | data: schemaJSON.$defs.name, 189 | locationInFile: { 190 | tokens: ["$defs", "name"], 191 | path: "/$defs/name", 192 | hash: "#/$defs/name", 193 | }, 194 | }, 195 | { 196 | value: "age.json", 197 | targetURI: path.url(`${dir}/age.json`), 198 | data: schemaJSON.$defs.age, 199 | locationInFile: { 200 | tokens: ["$defs", "age"], 201 | path: "/$defs/age", 202 | hash: "#/$defs/age", 203 | }, 204 | }, 205 | { 206 | value: "address.json", 207 | targetURI: path.url(`${dir}/address.json`), 208 | data: schemaJSON.$defs.address, 209 | locationInFile: { 210 | tokens: ["$defs", "address"], 211 | path: "/$defs/address", 212 | hash: "#/$defs/address", 213 | }, 214 | }, 215 | { 216 | value: "company.xyz", 217 | targetURI: path.url(`${dir}/company.xyz`), 218 | data: schemaJSON.$defs.company, 219 | locationInFile: { 220 | tokens: ["$defs", "company"], 221 | path: "/$defs/company", 222 | hash: "#/$defs/company", 223 | }, 224 | }, 225 | ], 226 | }, 227 | { 228 | uri: path.url(`${dir}/non-empty-string`), 229 | data: schemaJSON.$defs.nonEmptyString, 230 | locationInFile: { 231 | tokens: ["$defs", "nonEmptyString"], 232 | path: "/$defs/nonEmptyString", 233 | hash: "#/$defs/nonEmptyString", 234 | }, 235 | }, 236 | ] 237 | }, 238 | { 239 | url: path.url(`${dir}/person.json`), 240 | path: personPath, 241 | mediaType: "application/json", 242 | errors: [error2, error3], 243 | resources: [ 244 | { 245 | uri: path.url(`${dir}/person.json`), 246 | data: personJSON, 247 | locationInFile: { 248 | tokens: [], 249 | path: "", 250 | hash: "#", 251 | }, 252 | references: [ 253 | { 254 | value: "non-empty-string", 255 | targetURI: path.url(`${dir}/non-empty-string`), 256 | data: personJSON.properties.name.properties.first, 257 | locationInFile: { 258 | tokens: ["properties", "name", "properties", "first"], 259 | path: "/properties/name/properties/first", 260 | hash: "#/properties/name/properties/first", 261 | }, 262 | }, 263 | { 264 | value: "#non-empty-string", 265 | targetURI: path.url(`${dir}/person.json`, "#non-empty-string"), 266 | data: personJSON.properties.name.properties.middle, 267 | locationInFile: { 268 | tokens: ["properties", "name", "properties", "middle"], 269 | path: "/properties/name/properties/middle", 270 | hash: "#/properties/name/properties/middle", 271 | }, 272 | }, 273 | { 274 | value: "schema.json#/$defs/nonEmptyString", 275 | targetURI: path.url(`${dir}/schema.json`, "#/$defs/nonEmptyString"), 276 | data: personJSON.properties.name.properties.last, 277 | locationInFile: { 278 | tokens: ["properties", "name", "properties", "last"], 279 | path: "/properties/name/properties/last", 280 | hash: "#/properties/name/properties/last", 281 | }, 282 | }, 283 | { 284 | value: "address.json", 285 | targetURI: path.url(`${dir}/address.json`), 286 | data: personJSON.properties.homeAddress, 287 | locationInFile: { 288 | tokens: ["properties", "homeAddress"], 289 | path: "/properties/homeAddress", 290 | hash: "#/properties/homeAddress", 291 | }, 292 | }, 293 | { 294 | value: "address.json#address", 295 | targetURI: path.url(`${dir}/address.json`, "#address"), 296 | data: personJSON.properties.workAddress, 297 | locationInFile: { 298 | tokens: ["properties", "workAddress"], 299 | path: "/properties/workAddress", 300 | hash: "#/properties/workAddress", 301 | }, 302 | }, 303 | { 304 | value: "invalid/path/address.json", 305 | targetURI: path.url(`${dir}/invalid/path/address.json`), 306 | data: personJSON.properties.schoolAddress, 307 | locationInFile: { 308 | tokens: ["properties", "schoolAddress"], 309 | path: "/properties/schoolAddress", 310 | hash: "#/properties/schoolAddress", 311 | }, 312 | }, 313 | ], 314 | }, 315 | ], 316 | }, 317 | { 318 | url: path.url(`${dir}/age.json`), 319 | path: agePath, 320 | mediaType: "", 321 | errors: [error4], 322 | resources: [ 323 | { 324 | uri: path.url(`${dir}/age.json`), 325 | data: undefined, 326 | locationInFile: { 327 | tokens: [], 328 | path: "", 329 | hash: "#", 330 | }, 331 | }, 332 | ], 333 | }, 334 | { 335 | url: path.url(`${dir}/address.json`), 336 | path: addressPath, 337 | mediaType: "application/json", 338 | errors: [error5], 339 | resources: [ 340 | { 341 | uri: path.url(`${dir}/address.json`), 342 | data: "{ syntax: error }\n", 343 | locationInFile: { 344 | tokens: [], 345 | path: "", 346 | hash: "#", 347 | }, 348 | }, 349 | ], 350 | }, 351 | { 352 | url: path.url(`${dir}/company.xyz`), 353 | path: companyPath, 354 | mediaType: "chemical/x-xyz", 355 | resources: [ 356 | { 357 | uri: path.url(`${dir}/company.xyz`), 358 | data: new Uint8Array([ 359 | 84, 104, 105, 115, 32, 102, 105, 108, 101, 32, 104, 97, 115, 32, 97, 110, 360 | 32, 117, 110, 107, 110, 111, 119, 110, 32, 102, 105, 108, 101, 32, 101, 361 | 120, 116, 101, 110, 115, 105, 111, 110, 44, 32, 115, 111, 32, 105, 116, 32, 362 | 119, 105, 108, 108, 32, 98, 101, 32, 114, 101, 97, 100, 32, 97, 115, 32, 98, 363 | 105, 110, 97, 114, 121, 32, 100, 97, 116, 97, 10 364 | ]).buffer, 365 | locationInFile: { 366 | tokens: [], 367 | path: "", 368 | hash: "#", 369 | }, 370 | }, 371 | ], 372 | }, 373 | { 374 | url: path.url(`${dir}/invalid/path/address.json`), 375 | path: invalidPath, 376 | mediaType: "", 377 | errors: [error6], 378 | resources: [ 379 | { 380 | uri: path.url(`${dir}/invalid/path/address.json`), 381 | data: undefined, 382 | locationInFile: { 383 | tokens: [], 384 | path: "", 385 | hash: "#", 386 | }, 387 | }, 388 | ], 389 | }, 390 | ], 391 | }); 392 | } 393 | 394 | }); 395 | -------------------------------------------------------------------------------- /test/specs/options/determine-file-type/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "person", 3 | "title": "Person", 4 | "type": "object", 5 | "required": ["name", "age"], 6 | "properties": { 7 | "name": { "type": "string" }, 8 | "age": { 9 | "type": "integer", 10 | "minimum": 0, 11 | "description": "The person's age, in whole years" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/specs/options/determine-file-type/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/options/determine-file-type"; 9 | 10 | describe("Options: determineFileType", () => { 11 | it("should augment the default implementation", async () => { 12 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 13 | determineFileType (file, helpers) { 14 | file.mediaType = "text/json"; 15 | return helpers.determineFileType(file, helpers); 16 | } 17 | }); 18 | 19 | assert.schema(schema, { 20 | files: [{ 21 | url: path.url(`${dir}/schema.json`), 22 | path: path.rel(`${dir}/schema.json`), 23 | mediaType: "text/json", 24 | resources: [{ 25 | uri: path.url(`${dir}/person`), 26 | data: schemaJSON, 27 | locationInFile: { 28 | tokens: [], 29 | path: "", 30 | hash: "#", 31 | }, 32 | }], 33 | }], 34 | }); 35 | }); 36 | 37 | it("should replace the default implementation", async () => { 38 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 39 | determineFileType () { 40 | return "binary"; 41 | } 42 | }); 43 | 44 | assert.schema(schema, { 45 | files: [ 46 | { 47 | url: path.url(`${dir}/schema.json`), 48 | path: path.rel(`${dir}/schema.json`), 49 | mediaType: "application/json", 50 | resources: [{ 51 | uri: path.url(`${dir}/schema.json`), 52 | data: new Uint8Array([ 53 | 123, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 112, 101, 114, 115, 111, 54 | 110, 34, 44, 10, 32, 32, 34, 116, 105, 116, 108, 101, 34, 58, 32, 34, 80, 55 | 101, 114, 115, 111, 110, 34, 44, 10, 32, 32, 34, 116, 121, 112, 101, 34, 58, 56 | 32, 34, 111, 98, 106, 101, 99, 116, 34, 44, 10, 32, 32, 34, 114, 101, 113, 57 | 117, 105, 114, 101, 100, 34, 58, 32, 91, 34, 110, 97, 109, 101, 34, 44, 32, 58 | 34, 97, 103, 101, 34, 93, 44, 10, 32, 32, 34, 112, 114, 111, 112, 101, 114, 59 | 116, 105, 101, 115, 34, 58, 32, 123, 10, 32, 32, 32, 32, 34, 110, 97, 109, 60 | 101, 34, 58, 32, 123, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 115, 116, 61 | 114, 105, 110, 103, 34, 32, 125, 44, 10, 32, 32, 32, 32, 34, 97, 103, 101, 62 | 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 63 | 32, 34, 105, 110, 116, 101, 103, 101, 114, 34, 44, 10, 32, 32, 32, 32, 32, 64 | 32, 34, 109, 105, 110, 105, 109, 117, 109, 34, 58, 32, 48, 44, 10, 32, 32, 65 | 32, 32, 32, 32, 34, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 34, 66 | 58, 32, 34, 84, 104, 101, 32, 112, 101, 114, 115, 111, 110, 39, 115, 32, 97, 67 | 103, 101, 44, 32, 105, 110, 32, 119, 104, 111, 108, 101, 32, 121, 101, 97, 68 | 114, 115, 34, 10, 32, 32, 32, 32, 125, 10, 32, 32, 125, 10, 125, 10 69 | ]).buffer, 70 | locationInFile: { 71 | tokens: [], 72 | path: "", 73 | hash: "#", 74 | }, 75 | }], 76 | }, 77 | ], 78 | }); 79 | }); 80 | 81 | it("should handle a thrown error", async () => { 82 | try { 83 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 84 | determineFileType () { 85 | throw new RangeError("BOOM"); 86 | } 87 | }); 88 | } 89 | catch (error) { 90 | let errorPOJO = { 91 | name: "SchemaError", 92 | code: "ERR_FILE_TYPE", 93 | message: 94 | `Unable to determine the file type of ${path.rel(`${dir}/schema.json`)}\n` + 95 | " BOOM", 96 | originalError: { 97 | name: "RangeError", 98 | message: "BOOM", 99 | } 100 | }; 101 | 102 | assert.error(error, errorPOJO); 103 | assert.schema(error.schema, { 104 | hasErrors: true, 105 | files: [ 106 | { 107 | url: path.url(`${dir}/schema.json`), 108 | path: path.rel(`${dir}/schema.json`), 109 | mediaType: "application/json", 110 | errors: [errorPOJO], 111 | resources: [{ 112 | uri: path.url(`${dir}/schema.json`), 113 | data: undefined, 114 | locationInFile: { 115 | tokens: [], 116 | path: "", 117 | hash: "#", 118 | }, 119 | }], 120 | }, 121 | ], 122 | }); 123 | } 124 | }); 125 | 126 | it("should re-throw the first error by default", async () => { 127 | try { 128 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 129 | determineFileType (file, helpers) { 130 | helpers.handleError(new RangeError("BOOM 1")); 131 | helpers.handleError(new RangeError("BOOM 2")); 132 | helpers.handleError(new RangeError("BOOM 3")); 133 | } 134 | }); 135 | } 136 | catch (error) { 137 | let errorPOJO = { 138 | name: "SchemaError", 139 | code: "ERR_FILE_TYPE", 140 | message: 141 | `Unable to determine the file type of ${path.rel(`${dir}/schema.json`)}\n` + 142 | " BOOM 1", 143 | originalError: { 144 | name: "RangeError", 145 | message: "BOOM 1", 146 | } 147 | }; 148 | 149 | assert.error(error, errorPOJO); 150 | assert.schema(error.schema, { 151 | hasErrors: true, 152 | files: [ 153 | { 154 | url: path.url(`${dir}/schema.json`), 155 | path: path.rel(`${dir}/schema.json`), 156 | mediaType: "application/json", 157 | errors: [errorPOJO], 158 | resources: [{ 159 | uri: path.url(`${dir}/schema.json`), 160 | data: undefined, 161 | locationInFile: { 162 | tokens: [], 163 | path: "", 164 | hash: "#", 165 | }, 166 | }], 167 | }, 168 | ], 169 | }); 170 | } 171 | }); 172 | 173 | it("should handle multiple errors when continueOnError is enabled", async () => { 174 | try { 175 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 176 | continueOnError: true, 177 | determineFileType (file, helpers) { 178 | helpers.handleError(new RangeError("BOOM 1")); 179 | helpers.handleError(new RangeError("BOOM 2")); 180 | helpers.handleError(new RangeError("BOOM 3")); 181 | } 182 | }); 183 | } 184 | catch (error) { 185 | const makeErrorPOJO = (msg) => ({ 186 | name: "SchemaError", 187 | code: "ERR_FILE_TYPE", 188 | message: `Unable to determine the file type of ${path.rel(`${dir}/schema.json`)}\n ${msg}`, 189 | originalError: { 190 | name: "RangeError", 191 | message: msg, 192 | } 193 | }); 194 | 195 | let error1 = makeErrorPOJO("BOOM 1"); 196 | let error2 = makeErrorPOJO("BOOM 2"); 197 | let error3 = makeErrorPOJO("BOOM 3"); 198 | 199 | assert.error(error, { 200 | name: "MultiError", 201 | code: "ERR_MULTIPLE", 202 | message: `3 errors occurred while reading ${path.rel(`${dir}/schema.json`)}`, 203 | errors: [error1, error2, error3] 204 | }); 205 | assert.schema(error.schema, { 206 | hasErrors: true, 207 | files: [ 208 | { 209 | url: path.url(`${dir}/schema.json`), 210 | path: path.rel(`${dir}/schema.json`), 211 | mediaType: "application/json", 212 | errors: [error1, error2, error3], 213 | resources: [{ 214 | uri: path.url(`${dir}/schema.json`), 215 | data: new Uint8Array([ 216 | 123, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 112, 101, 114, 115, 111, 217 | 110, 34, 44, 10, 32, 32, 34, 116, 105, 116, 108, 101, 34, 58, 32, 34, 80, 218 | 101, 114, 115, 111, 110, 34, 44, 10, 32, 32, 34, 116, 121, 112, 101, 34, 58, 219 | 32, 34, 111, 98, 106, 101, 99, 116, 34, 44, 10, 32, 32, 34, 114, 101, 113, 220 | 117, 105, 114, 101, 100, 34, 58, 32, 91, 34, 110, 97, 109, 101, 34, 44, 32, 221 | 34, 97, 103, 101, 34, 93, 44, 10, 32, 32, 34, 112, 114, 111, 112, 101, 114, 222 | 116, 105, 101, 115, 34, 58, 32, 123, 10, 32, 32, 32, 32, 34, 110, 97, 109, 223 | 101, 34, 58, 32, 123, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 115, 116, 224 | 114, 105, 110, 103, 34, 32, 125, 44, 10, 32, 32, 32, 32, 34, 97, 103, 101, 225 | 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 226 | 32, 34, 105, 110, 116, 101, 103, 101, 114, 34, 44, 10, 32, 32, 32, 32, 32, 227 | 32, 34, 109, 105, 110, 105, 109, 117, 109, 34, 58, 32, 48, 44, 10, 32, 32, 228 | 32, 32, 32, 32, 34, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 34, 229 | 58, 32, 34, 84, 104, 101, 32, 112, 101, 114, 115, 111, 110, 39, 115, 32, 97, 230 | 103, 101, 44, 32, 105, 110, 32, 119, 104, 111, 108, 101, 32, 121, 101, 97, 231 | 114, 115, 34, 10, 32, 32, 32, 32, 125, 10, 32, 32, 125, 10, 125, 10 232 | ]).buffer, 233 | locationInFile: { 234 | tokens: [], 235 | path: "", 236 | hash: "#", 237 | }, 238 | }], 239 | }, 240 | ], 241 | }); 242 | } 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /test/specs/options/index-file/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "person", 3 | "$anchor": "person", 4 | "title": "Person", 5 | "type": "object", 6 | "required": [ 7 | "name", 8 | "age" 9 | ], 10 | "properties": { 11 | "name": { 12 | "type": "string" 13 | }, 14 | "age": { 15 | "type": "integer", 16 | "minimum": 0, 17 | "description": "The person's age, in whole years" 18 | }, 19 | "parent": { 20 | "$ref": "#person" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/specs/options/index-file/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema, Resource } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/options/index-file"; 9 | 10 | describe("Options: indexFile", () => { 11 | it("should augment the default implementation", async () => { 12 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 13 | indexFile (file, helpers) { 14 | helpers.indexFile(file, helpers); 15 | file.rootResource.anchors[0].name = "custom-name"; 16 | file.rootResource.references[0].value = "custom-value"; 17 | } 18 | }); 19 | 20 | assert.schema(schema, { 21 | files: [{ 22 | url: path.url(`${dir}/schema.json`), 23 | path: path.rel(`${dir}/schema.json`), 24 | mediaType: "application/json", 25 | resources: [{ 26 | uri: path.url(`${dir}/person`), 27 | data: schemaJSON, 28 | locationInFile: { 29 | tokens: [], 30 | path: "", 31 | hash: "#", 32 | }, 33 | anchors: [{ 34 | name: "custom-name", 35 | uri: path.url(`${dir}/person`, "#person"), 36 | data: schemaJSON, 37 | locationInFile: { 38 | tokens: [], 39 | path: "", 40 | hash: "#", 41 | }, 42 | }], 43 | references: [{ 44 | value: "custom-value", 45 | targetURI: path.url(`${dir}/person`, "#person"), 46 | data: schemaJSON.properties.parent, 47 | locationInFile: { 48 | tokens: ["properties", "parent"], 49 | path: "/properties/parent", 50 | hash: "#/properties/parent", 51 | }, 52 | }], 53 | }], 54 | }], 55 | }); 56 | }); 57 | 58 | it("should replace the default implementation", async () => { 59 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 60 | indexFile (file) { 61 | file.resources.push(new Resource({ 62 | file, 63 | uri: "http://example.com/foo/bar", 64 | locationInFile: ["fizz", "buzz"], 65 | data: { hello: "world" }, 66 | })); 67 | } 68 | }); 69 | 70 | assert.schema(schema, { 71 | files: [ 72 | { 73 | url: path.url(`${dir}/schema.json`), 74 | path: path.rel(`${dir}/schema.json`), 75 | mediaType: "application/json", 76 | resources: [ 77 | { 78 | uri: path.url(`${dir}/schema.json`), 79 | data: schemaJSON, 80 | locationInFile: { 81 | tokens: [], 82 | path: "", 83 | hash: "#", 84 | }, 85 | }, 86 | { 87 | uri: new URL("http://example.com/foo/bar"), 88 | data: { hello: "world" }, 89 | locationInFile: { 90 | tokens: ["fizz", "buzz"], 91 | path: "/fizz/buzz", 92 | hash: "#/fizz/buzz", 93 | }, 94 | }, 95 | ], 96 | }, 97 | ], 98 | }); 99 | }); 100 | 101 | it("should handle a thrown error", async () => { 102 | try { 103 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 104 | indexFile () { 105 | throw new RangeError("BOOM"); 106 | } 107 | }); 108 | } 109 | catch (error) { 110 | let errorPOJO = { 111 | name: "SchemaError", 112 | code: "ERR_INDEX", 113 | message: 114 | `Error in ${path.rel(`${dir}/schema.json`)}\n` + 115 | " BOOM", 116 | originalError: { 117 | name: "RangeError", 118 | message: "BOOM", 119 | } 120 | }; 121 | 122 | assert.error(error, errorPOJO); 123 | assert.schema(error.schema, { 124 | hasErrors: true, 125 | files: [ 126 | { 127 | url: path.url(`${dir}/schema.json`), 128 | path: path.rel(`${dir}/schema.json`), 129 | mediaType: "application/json", 130 | errors: [errorPOJO], 131 | resources: [{ 132 | uri: path.url(`${dir}/schema.json`), 133 | data: schemaJSON, 134 | locationInFile: { 135 | tokens: [], 136 | path: "", 137 | hash: "#", 138 | }, 139 | }], 140 | }, 141 | ], 142 | }); 143 | } 144 | }); 145 | 146 | it("should re-throw the first error by default", async () => { 147 | try { 148 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 149 | indexFile (file, helpers) { 150 | helpers.handleError(new RangeError("BOOM 1")); 151 | helpers.handleError(new RangeError("BOOM 2")); 152 | helpers.handleError(new RangeError("BOOM 3")); 153 | } 154 | }); 155 | } 156 | catch (error) { 157 | let errorPOJO = { 158 | name: "SchemaError", 159 | code: "ERR_INDEX", 160 | message: 161 | `Error in ${path.rel(`${dir}/schema.json`)}\n` + 162 | " BOOM 1", 163 | originalError: { 164 | name: "RangeError", 165 | message: "BOOM 1", 166 | } 167 | }; 168 | 169 | assert.error(error, errorPOJO); 170 | assert.schema(error.schema, { 171 | hasErrors: true, 172 | files: [ 173 | { 174 | url: path.url(`${dir}/schema.json`), 175 | path: path.rel(`${dir}/schema.json`), 176 | mediaType: "application/json", 177 | errors: [errorPOJO], 178 | resources: [{ 179 | uri: path.url(`${dir}/schema.json`), 180 | data: schemaJSON, 181 | locationInFile: { 182 | tokens: [], 183 | path: "", 184 | hash: "#", 185 | }, 186 | }], 187 | }, 188 | ], 189 | }); 190 | } 191 | }); 192 | 193 | it("should handle multiple errors when continueOnError is enabled", async () => { 194 | try { 195 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 196 | continueOnError: true, 197 | indexFile (file, helpers) { 198 | helpers.handleError(new RangeError("BOOM 1")); 199 | helpers.handleError(new RangeError("BOOM 2")); 200 | helpers.handleError(new RangeError("BOOM 3")); 201 | } 202 | }); 203 | } 204 | catch (error) { 205 | const makeErrorPOJO = (msg) => ({ 206 | name: "SchemaError", 207 | code: "ERR_INDEX", 208 | message: `Error in ${path.rel(`${dir}/schema.json`)}\n ${msg}`, 209 | originalError: { 210 | name: "RangeError", 211 | message: msg, 212 | } 213 | }); 214 | 215 | let error1 = makeErrorPOJO("BOOM 1"); 216 | let error2 = makeErrorPOJO("BOOM 2"); 217 | let error3 = makeErrorPOJO("BOOM 3"); 218 | 219 | assert.error(error, { 220 | name: "MultiError", 221 | code: "ERR_MULTIPLE", 222 | message: `3 errors occurred while reading ${path.rel(`${dir}/schema.json`)}`, 223 | errors: [error1, error2, error3] 224 | }); 225 | assert.schema(error.schema, { 226 | hasErrors: true, 227 | files: [ 228 | { 229 | url: path.url(`${dir}/schema.json`), 230 | path: path.rel(`${dir}/schema.json`), 231 | mediaType: "application/json", 232 | errors: [error1, error2, error3], 233 | resources: [{ 234 | uri: path.url(`${dir}/schema.json`), 235 | data: schemaJSON, 236 | locationInFile: { 237 | tokens: [], 238 | path: "", 239 | hash: "#", 240 | }, 241 | }], 242 | }, 243 | ], 244 | }); 245 | } 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /test/specs/options/parse-file/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "person", 3 | "title": "Person", 4 | "type": "object", 5 | "required": [ 6 | "name", 7 | "age" 8 | ], 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "age": { 14 | "type": "integer", 15 | "minimum": 0, 16 | "description": "The person's age, in whole years" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/specs/options/parse-file/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const dir = "test/specs/options/parse-file"; 9 | 10 | describe("Options: parseFile", () => { 11 | it("should augment the default implementation", async () => { 12 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 13 | parseFile (file, helpers) { 14 | file.metadata = { 15 | length: file.data.length, 16 | }; 17 | helpers.parseFile(file, helpers); 18 | } 19 | }); 20 | 21 | assert.schema(schema, { 22 | files: [{ 23 | url: path.url(`${dir}/schema.json`), 24 | path: path.rel(`${dir}/schema.json`), 25 | mediaType: "application/json", 26 | resources: [{ 27 | uri: path.url(`${dir}/person`), 28 | data: schemaJSON, 29 | locationInFile: { 30 | tokens: [], 31 | path: "", 32 | hash: "#", 33 | }, 34 | }], 35 | }], 36 | }); 37 | }); 38 | 39 | it("should replace the default implementation", async () => { 40 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 41 | parseFile (file) { 42 | file.data = { custom: "data" }; 43 | } 44 | }); 45 | 46 | assert.schema(schema, { 47 | files: [ 48 | { 49 | url: path.url(`${dir}/schema.json`), 50 | path: path.rel(`${dir}/schema.json`), 51 | mediaType: "application/json", 52 | resources: [{ 53 | uri: path.url(`${dir}/schema.json`), 54 | data: { custom: "data" }, 55 | locationInFile: { 56 | tokens: [], 57 | path: "", 58 | hash: "#", 59 | }, 60 | }], 61 | }, 62 | ], 63 | }); 64 | }); 65 | 66 | it("should handle a thrown error", async () => { 67 | try { 68 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 69 | parseFile () { 70 | throw new RangeError("BOOM"); 71 | } 72 | }); 73 | } 74 | catch (error) { 75 | let errorPOJO = { 76 | name: "SchemaError", 77 | code: "ERR_PARSE", 78 | message: 79 | `Unable to parse ${path.rel(`${dir}/schema.json`)}\n` + 80 | " BOOM", 81 | originalError: { 82 | name: "RangeError", 83 | message: "BOOM", 84 | } 85 | }; 86 | 87 | assert.error(error, errorPOJO); 88 | assert.schema(error.schema, { 89 | hasErrors: true, 90 | files: [ 91 | { 92 | url: path.url(`${dir}/schema.json`), 93 | path: path.rel(`${dir}/schema.json`), 94 | mediaType: "application/json", 95 | errors: [errorPOJO], 96 | resources: [{ 97 | uri: path.url(`${dir}/schema.json`), 98 | data: JSON.stringify(schemaJSON, null, 2) + "\n", 99 | locationInFile: { 100 | tokens: [], 101 | path: "", 102 | hash: "#", 103 | }, 104 | }], 105 | }, 106 | ], 107 | }); 108 | } 109 | }); 110 | 111 | it("should re-throw the first error by default", async () => { 112 | try { 113 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 114 | parseFile (file, helpers) { 115 | helpers.handleError(new RangeError("BOOM 1")); 116 | helpers.handleError(new RangeError("BOOM 2")); 117 | helpers.handleError(new RangeError("BOOM 3")); 118 | } 119 | }); 120 | } 121 | catch (error) { 122 | let errorPOJO = { 123 | name: "SchemaError", 124 | code: "ERR_PARSE", 125 | message: 126 | `Unable to parse ${path.rel(`${dir}/schema.json`)}\n` + 127 | " BOOM 1", 128 | originalError: { 129 | name: "RangeError", 130 | message: "BOOM 1", 131 | } 132 | }; 133 | 134 | assert.error(error, errorPOJO); 135 | assert.schema(error.schema, { 136 | hasErrors: true, 137 | files: [ 138 | { 139 | url: path.url(`${dir}/schema.json`), 140 | path: path.rel(`${dir}/schema.json`), 141 | mediaType: "application/json", 142 | errors: [errorPOJO], 143 | resources: [{ 144 | uri: path.url(`${dir}/schema.json`), 145 | data: JSON.stringify(schemaJSON, null, 2) + "\n", 146 | locationInFile: { 147 | tokens: [], 148 | path: "", 149 | hash: "#", 150 | }, 151 | }], 152 | }, 153 | ], 154 | }); 155 | } 156 | }); 157 | 158 | it("should handle multiple errors when continueOnError is enabled", async () => { 159 | try { 160 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 161 | continueOnError: true, 162 | parseFile (file, helpers) { 163 | helpers.handleError(new RangeError("BOOM 1")); 164 | helpers.handleError(new RangeError("BOOM 2")); 165 | helpers.handleError(new RangeError("BOOM 3")); 166 | } 167 | }); 168 | } 169 | catch (error) { 170 | const makeErrorPOJO = (msg) => ({ 171 | name: "SchemaError", 172 | code: "ERR_PARSE", 173 | message: `Unable to parse ${path.rel(`${dir}/schema.json`)}\n ${msg}`, 174 | originalError: { 175 | name: "RangeError", 176 | message: msg, 177 | } 178 | }); 179 | 180 | let error1 = makeErrorPOJO("BOOM 1"); 181 | let error2 = makeErrorPOJO("BOOM 2"); 182 | let error3 = makeErrorPOJO("BOOM 3"); 183 | 184 | assert.error(error, { 185 | name: "MultiError", 186 | code: "ERR_MULTIPLE", 187 | message: `3 errors occurred while reading ${path.rel(`${dir}/schema.json`)}`, 188 | errors: [error1, error2, error3] 189 | }); 190 | assert.schema(error.schema, { 191 | hasErrors: true, 192 | files: [ 193 | { 194 | url: path.url(`${dir}/schema.json`), 195 | path: path.rel(`${dir}/schema.json`), 196 | mediaType: "application/json", 197 | errors: [error1, error2, error3], 198 | resources: [{ 199 | uri: path.url(`${dir}/schema.json`), 200 | data: JSON.stringify(schemaJSON, null, 2) + "\n", 201 | locationInFile: { 202 | tokens: [], 203 | path: "", 204 | hash: "#", 205 | }, 206 | }], 207 | }, 208 | ], 209 | }); 210 | } 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/specs/options/read-file/person.json: -------------------------------------------------------------------------------- 1 | THIS FILE NEVER ACTUALLY GETS READ. 2 | THE CUSTOM READFILE FUNCTION REPLACES THE DEFAULT IMPLEMENTATION. 3 | -------------------------------------------------------------------------------- /test/specs/options/read-file/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "person": { 4 | "$ref": "person.json" 5 | }, 6 | "address": { 7 | "$ref": "address.json" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/specs/options/read-file/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../../"); 4 | const path = require("../../../utils/path"); 5 | const assert = require("../../../utils/assert"); 6 | const schemaJSON = require("./schema.json"); 7 | 8 | const personJSON = { 9 | title: "Person", 10 | properties: { 11 | name: { type: "string" } 12 | }, 13 | }; 14 | 15 | const addressJSON = { 16 | title: "Address", 17 | properties: { 18 | city: { type: "string" } 19 | }, 20 | }; 21 | 22 | const dir = "test/specs/options/read-file"; 23 | 24 | describe("Options: readFile", () => { 25 | it("should augment the default implementation", async () => { 26 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 27 | async readFile (file, helpers) { 28 | if (file.url.pathname.endsWith("person.json")) { 29 | file.data = personJSON; 30 | } 31 | else if (file.url.pathname.endsWith("address.json")) { 32 | file.data = addressJSON; 33 | } 34 | else { 35 | await helpers.readFile(file, helpers); 36 | } 37 | 38 | file.mediaType = "custom/json"; 39 | file.metadata.foo = "bar"; 40 | } 41 | }); 42 | 43 | assert.schema(schema, { 44 | files: [ 45 | { 46 | url: path.url(`${dir}/schema.json`), 47 | path: path.rel(`${dir}/schema.json`), 48 | mediaType: "custom/json", 49 | metadata: Object.assign({}, schema.rootFile.metadata, { foo: "bar" }), 50 | resources: [{ 51 | uri: path.url(`${dir}/schema.json`), 52 | data: schemaJSON, 53 | locationInFile: { 54 | tokens: [], 55 | path: "", 56 | hash: "#", 57 | }, 58 | references: [ 59 | { 60 | value: "person.json", 61 | targetURI: path.url(`${dir}/person.json`), 62 | data: schemaJSON.$defs.person, 63 | locationInFile: { 64 | tokens: ["$defs", "person"], 65 | path: "/$defs/person", 66 | hash: "#/$defs/person", 67 | }, 68 | }, 69 | { 70 | value: "address.json", 71 | targetURI: path.url(`${dir}/address.json`), 72 | data: schemaJSON.$defs.address, 73 | locationInFile: { 74 | tokens: ["$defs", "address"], 75 | path: "/$defs/address", 76 | hash: "#/$defs/address", 77 | }, 78 | }, 79 | ], 80 | }], 81 | }, 82 | { 83 | url: path.url(`${dir}/person.json`), 84 | path: path.rel(`${dir}/person.json`), 85 | mediaType: "custom/json", 86 | metadata: { foo: "bar" }, 87 | resources: [{ 88 | uri: path.url(`${dir}/person.json`), 89 | data: personJSON, 90 | locationInFile: { 91 | tokens: [], 92 | path: "", 93 | hash: "#", 94 | }, 95 | }], 96 | }, 97 | { 98 | url: path.url(`${dir}/address.json`), 99 | path: path.rel(`${dir}/address.json`), 100 | mediaType: "custom/json", 101 | metadata: { foo: "bar" }, 102 | resources: [{ 103 | uri: path.url(`${dir}/address.json`), 104 | data: addressJSON, 105 | locationInFile: { 106 | tokens: [], 107 | path: "", 108 | hash: "#", 109 | }, 110 | }], 111 | }, 112 | ], 113 | }); 114 | }); 115 | 116 | it("should replace the default implementation", async () => { 117 | let schema = await readJsonSchema(path.rel(`${dir}/schema.json`), { 118 | readFile (file) { 119 | file.mediaType = "custom/json"; 120 | file.metadata.foo = "bar"; 121 | file.data = { fizz: "buzz" }; 122 | } 123 | }); 124 | 125 | assert.schema(schema, { 126 | files: [ 127 | { 128 | url: path.url(`${dir}/schema.json`), 129 | path: path.rel(`${dir}/schema.json`), 130 | mediaType: "custom/json", 131 | metadata: { foo: "bar" }, 132 | resources: [{ 133 | uri: path.url(`${dir}/schema.json`), 134 | data: { fizz: "buzz" }, 135 | locationInFile: { 136 | tokens: [], 137 | path: "", 138 | hash: "#", 139 | }, 140 | }], 141 | }, 142 | ], 143 | }); 144 | }); 145 | 146 | it("should handle a thrown error", async () => { 147 | try { 148 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 149 | readFile () { 150 | throw new RangeError("BOOM"); 151 | } 152 | }); 153 | } 154 | catch (error) { 155 | let errorPOJO = { 156 | name: "SchemaError", 157 | code: "ERR_READ", 158 | message: 159 | `Unable to read ${path.rel(`${dir}/schema.json`)}\n` + 160 | " BOOM", 161 | originalError: { 162 | name: "RangeError", 163 | message: "BOOM", 164 | } 165 | }; 166 | 167 | assert.error(error, errorPOJO); 168 | assert.schema(error.schema, { 169 | hasErrors: true, 170 | files: [ 171 | { 172 | url: path.url(`${dir}/schema.json`), 173 | path: path.rel(`${dir}/schema.json`), 174 | mediaType: "application/schema+json", 175 | metadata: {}, 176 | errors: [errorPOJO], 177 | resources: [{ 178 | uri: path.url(`${dir}/schema.json`), 179 | data: undefined, 180 | locationInFile: { 181 | tokens: [], 182 | path: "", 183 | hash: "#", 184 | }, 185 | }], 186 | }, 187 | ], 188 | }); 189 | } 190 | }); 191 | 192 | it("should re-throw the first error by default", async () => { 193 | try { 194 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 195 | readFile (file, helpers) { 196 | helpers.handleError(new RangeError("BOOM 1")); 197 | helpers.handleError(new RangeError("BOOM 2")); 198 | helpers.handleError(new RangeError("BOOM 3")); 199 | } 200 | }); 201 | } 202 | catch (error) { 203 | let errorPOJO = { 204 | name: "SchemaError", 205 | code: "ERR_READ", 206 | message: 207 | `Unable to read ${path.rel(`${dir}/schema.json`)}\n` + 208 | " BOOM 1", 209 | originalError: { 210 | name: "RangeError", 211 | message: "BOOM 1", 212 | } 213 | }; 214 | 215 | assert.error(error, errorPOJO); 216 | assert.schema(error.schema, { 217 | hasErrors: true, 218 | files: [ 219 | { 220 | url: path.url(`${dir}/schema.json`), 221 | path: path.rel(`${dir}/schema.json`), 222 | mediaType: "application/schema+json", 223 | metadata: {}, 224 | errors: [errorPOJO], 225 | resources: [{ 226 | uri: path.url(`${dir}/schema.json`), 227 | data: undefined, 228 | locationInFile: { 229 | tokens: [], 230 | path: "", 231 | hash: "#", 232 | }, 233 | }], 234 | }, 235 | ], 236 | }); 237 | } 238 | }); 239 | 240 | it("should handle multiple errors when continueOnError is enabled", async () => { 241 | try { 242 | await readJsonSchema(path.rel(`${dir}/schema.json`), { 243 | continueOnError: true, 244 | readFile (file, helpers) { 245 | helpers.handleError(new RangeError("BOOM 1")); 246 | helpers.handleError(new RangeError("BOOM 2")); 247 | helpers.handleError(new RangeError("BOOM 3")); 248 | } 249 | }); 250 | } 251 | catch (error) { 252 | const makeErrorPOJO = (msg) => ({ 253 | name: "SchemaError", 254 | code: "ERR_READ", 255 | message: `Unable to read ${path.rel(`${dir}/schema.json`)}\n ${msg}`, 256 | originalError: { 257 | name: "RangeError", 258 | message: msg, 259 | } 260 | }); 261 | 262 | let error1 = makeErrorPOJO("BOOM 1"); 263 | let error2 = makeErrorPOJO("BOOM 2"); 264 | let error3 = makeErrorPOJO("BOOM 3"); 265 | 266 | assert.error(error, { 267 | name: "MultiError", 268 | code: "ERR_MULTIPLE", 269 | message: `3 errors occurred while reading ${path.rel(`${dir}/schema.json`)}`, 270 | errors: [error1, error2, error3] 271 | }); 272 | assert.schema(error.schema, { 273 | hasErrors: true, 274 | files: [ 275 | { 276 | url: path.url(`${dir}/schema.json`), 277 | path: path.rel(`${dir}/schema.json`), 278 | mediaType: "application/schema+json", 279 | metadata: {}, 280 | errors: [error1, error2, error3], 281 | resources: [{ 282 | uri: path.url(`${dir}/schema.json`), 283 | data: undefined, 284 | locationInFile: { 285 | tokens: [], 286 | path: "", 287 | hash: "#", 288 | }, 289 | }], 290 | }, 291 | ], 292 | }); 293 | } 294 | }); 295 | }); 296 | -------------------------------------------------------------------------------- /test/specs/real-world/download.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { readJsonSchema } = require("../../../"); 4 | const { host } = require("@jsdevtools/host-environment"); 5 | const fetch = host.global.fetch || require("node-fetch"); 6 | 7 | const download = module.exports = { 8 | /** 9 | * Downloads a list of over 2000 real-world Swagger APIs from apis.guru, 10 | * and applies some custom filtering logic to it. 11 | */ 12 | async listOfAPIs () { 13 | let response = await fetch("https://api.apis.guru/v2/list.json"); 14 | 15 | if (!response.ok) { 16 | throw new Error("Unable to downlaod real-world APIs from apis.guru"); 17 | } 18 | 19 | let apiMap = await response.json(); 20 | 21 | // Flatten the API object structure into an array containing the latest version of each API 22 | let apiArray = []; 23 | 24 | for (let [name, api] of Object.entries(apiMap)) { 25 | let latestVersion = api.versions[api.preferred]; 26 | apiArray.push({ name, url: latestVersion.swaggerUrl }); 27 | } 28 | 29 | return apiArray; 30 | }, 31 | 32 | /** 33 | * Downloads an API definition from apis.guru. 34 | * Up to 3 download attempts are made before giving up. 35 | */ 36 | async api (url, retries = 2) { 37 | try { 38 | return await readJsonSchema(url); 39 | } 40 | catch (error) { 41 | if (error.code !== "ERR_READ") { 42 | throw error; 43 | } 44 | 45 | if (retries === 0) { 46 | console.error(" failed to download. giving up."); 47 | } 48 | else { 49 | // Wait a few seconds, then try the download again 50 | console.error(" failed to download. trying again..."); 51 | await new Promise((resolve) => setTimeout(resolve, 2000)); 52 | return download.api(url, retries - 1); 53 | } 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /test/specs/real-world/known-errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = handleError; 4 | 5 | /** 6 | * Re-throws the error, unless it's a known bug in an API definition on apis.guru 7 | */ 8 | function handleError (name, url, error) { 9 | for (let knownError of knownErrors) { 10 | if (isMatch(name, error, knownError)) { 11 | return; 12 | } 13 | } 14 | 15 | console.error( 16 | "\n\n========================== ERROR =============================\n\n" + 17 | `API Name: ${name}\n\n` + 18 | `Swagger URL: ${url}\n\n` + 19 | error.message + 20 | "\n\n========================== ERROR =============================\n\n" 21 | ); 22 | throw error; 23 | } 24 | 25 | /** 26 | * Determines whether an error in an API definition matches a known error 27 | */ 28 | function isMatch (api, error, knownError) { 29 | if (api.includes(knownError.api)) { 30 | if (typeof knownError.error === "string") { 31 | return error.message.includes(knownError.error); 32 | } 33 | else { 34 | return knownError.error.test(error.message); 35 | } 36 | } 37 | } 38 | 39 | 40 | const knownErrors = [ 41 | // Malformed URL 42 | { api: "azure.com:keyvault", error: "Invalid URL: https:// mykeyvault" }, 43 | 44 | // $refs to external files that don't exist 45 | { api: "azure.com", error: /^Cannot find resource: .*\.json/ }, 46 | ]; 47 | -------------------------------------------------------------------------------- /test/specs/real-world/real-world.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | 4 | const { host } = require("@jsdevtools/host-environment"); 5 | const download = require("./download"); 6 | const handleError = require("./known-errors"); 7 | 8 | // How many APIs to test in "quick mode" and normal mode 9 | const START_AT_INDEX = 0; 10 | const MAX_APIS_TO_TEST = START_AT_INDEX + ((!host.node || process.argv.includes("--quick-test")) ? 10 : 1700); 11 | 12 | describe("Real-world API definitions", () => { 13 | let realWorldAPIs = []; 14 | 15 | before(async function () { 16 | // This hook sometimes takes several seconds, due to the large download 17 | this.timeout(10000); 18 | 19 | // Download a list of over 1700 real-world OpenAPI definitions from apis.guru 20 | realWorldAPIs = await download.listOfAPIs(); 21 | }); 22 | 23 | beforeEach(function () { 24 | // Increase the timeouts by A LOT because: 25 | // 1) CI is really slow 26 | // 2) Some API definitions are HUGE and take a while to download 27 | // 3) If the download fails, we retry 2 times, which takes even more time 28 | // 4) Really large API definitions take longer to parse 29 | this.currentTest.timeout(host.ci ? 120000 : 30000); // 2 minutes in CI, 30 seconds locally 30 | this.currentTest.slow(5000); 31 | }); 32 | 33 | // Mocha requires us to create our tests synchronously. But the list of APIs is downloaded asynchronously. 34 | // So, we just create a bunch of placeholder tests, and then rename them later to reflect which API they're testing. 35 | for (let index = START_AT_INDEX; index < MAX_APIS_TO_TEST; index++) { 36 | it(title(index), async function () { // eslint-disable-line no-loop-func 37 | let api = realWorldAPIs[index]; 38 | if (!api) return; 39 | 40 | let { name, url } = api; 41 | this.test.title = title(index, name); 42 | 43 | try { 44 | let schema = await download.api(url); 45 | if (!schema) return; 46 | 47 | let resourceCount = 0, anchorCount = 0, refCount = 0; 48 | 49 | for (let resource of schema.resources) { 50 | resourceCount++; 51 | anchorCount += resource.anchors.length; 52 | refCount += resource.references.length; 53 | this.test.title = title(index, name, resourceCount, anchorCount, refCount); 54 | } 55 | } 56 | catch (error) { 57 | handleError(name, url, error); 58 | } 59 | }); 60 | } 61 | }); 62 | 63 | 64 | /** 65 | * Formats the test titles 66 | */ 67 | function title (index, name = "", resourceCount = 0, anchorCount = 0, refCount = 0) { 68 | return `${(index + 1).toString().padStart(4)} ` + 69 | `${name.padEnd(35).slice(0, 35)} ` + 70 | `resources: ${resourceCount.toString().padEnd(3)} ` + 71 | `$anchors: ${anchorCount.toString().padEnd(3)} ` + 72 | `$refs: ${refCount}`; 73 | } 74 | -------------------------------------------------------------------------------- /test/utils/assert.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const chai = require("chai"); 4 | const { expect } = require("chai"); 5 | const { JsonSchema, File, Resource, Anchor, Reference, Pointer, Resolution } = require("../../"); 6 | 7 | const assert = module.exports = Object.assign(chai.assert, { 8 | failed () { 9 | throw new Error("Expected the test to throw an error, but no error was thrown."); 10 | }, 11 | 12 | schema (actual, expected, p = "schema.") { 13 | expect(actual).to.be.an.instanceOf(JsonSchema, `${p} (actual)`); 14 | expect(expected).to.be.an("object", `${p} (expected)`); 15 | expect(actual.hasErrors).to.equal(expected.hasErrors || false, `${p}hasErrors`); 16 | expect(actual.rootFile).to.equal(actual.files[0], `${p}rootFile`); 17 | expect(paths(actual.files)).to.have.same.members(paths(expected.files), `${p}files`); 18 | 19 | for (let [index, expectedFile] of entries(expected.files)) { 20 | let actualFile = actual.files[index]; 21 | expect(actualFile.schema).to.equal(actual, `${p}files[${index}].schema`); 22 | assert.file(actualFile, expectedFile, `${p}files[${index}].`); 23 | } 24 | }, 25 | 26 | file (actual, expected, p = "file.") { 27 | expect(actual).to.be.an.instanceOf(File, `${p} (actual)`); 28 | expect(expected).to.be.an("object", `${p} (expected)`); 29 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 30 | expect(href(actual)).to.equal(href(expected), `${p}url`); 31 | expect(actual.path).to.equal(expected.path, `${p}path`); 32 | 33 | if ("mediaType" in expected) { 34 | expect(actual.mediaType).to.equal(expected.mediaType, `${p}mediaType`); 35 | } 36 | 37 | if (expected.metadata) { 38 | expect(actual.metadata).to.deep.equal(expected.metadata, `${p}metadata`); 39 | } 40 | else { 41 | expect(actual.metadata).to.be.an("object", `${p}metadata`); 42 | } 43 | 44 | expect(actual.rootResource).to.equal(actual.resources[0], `${p}rootResource`); 45 | expect(uris(actual.resources)).to.have.same.members(uris(expected.resources), `${p}resources`); 46 | 47 | for (let [index, expectedResource] of entries(expected.resources)) { 48 | let actualResource = actual.resources[index]; 49 | expect(actualResource.file).to.equal(actual, `${p}resources[${index}].file`); 50 | expect(actualResource.schema).to.equal(actual.schema, `${p}resources[${index}].schema`); 51 | assert.resource(actualResource, expectedResource, `${p}resources[${index}].`); 52 | } 53 | 54 | expect(codes(actual.errors)).to.have.same.members(codes(expected.errors), `${p}errors`); 55 | 56 | for (let [index, expectedError] of entries(expected.errors)) { 57 | let actualError = actual.errors[index]; 58 | expect(actualError.file).to.equal(actual, `${p}errors[${index}].file`); 59 | expect(actualError.schema).to.equal(actual.schema, `${p}errors[${index}].schema`); 60 | assert.error(actualError, expectedError, `${p}errors[${index}].`); 61 | } 62 | }, 63 | 64 | error (actual, expected, p = "error.") { 65 | expect(actual).to.be.an.instanceOf(Error, `${p} (actual)`); 66 | expect(expected).to.be.an("object", `${p} (expected)`); 67 | 68 | if (actual.schema !== undefined) { 69 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 70 | } 71 | 72 | if (actual.file !== undefined) { 73 | expect(actual.file).to.be.an.instanceOf(File, `${p}file`); 74 | } 75 | 76 | if (expected.message instanceof RegExp) { 77 | expect(actual.message).to.match(expected.message, `${p}message`); 78 | } 79 | else { 80 | expect(actual.message).to.equal(expected.message, `${p}message`); 81 | } 82 | 83 | expect(actual.name).to.equal(expected.name, `${p}name`); 84 | expect(actual.code).to.equal(expected.code, `${p}code`); 85 | 86 | if ("locationInFile" in expected) { 87 | assert.pointer(actual.locationInFile, expected.locationInFile, `${p}locationInFile.`); 88 | } 89 | 90 | if (actual.originalError || expected.originalError) { 91 | assert.error(actual.originalError, expected.originalError, `${p}originalError.`); 92 | } 93 | 94 | if (actual.errors || expected.errors) { 95 | expect(codes(actual.errors)).to.have.same.members(codes(expected.errors), `${p}errors`); 96 | 97 | for (let [index, expectedError] of entries(expected.errors)) { 98 | let actualError = actual.errors[index]; 99 | expect(actualError.schema).to.equal(actual.schema, `${p}errors[${index}].schema`); 100 | assert.error(actualError, expectedError, `${p}errors[${index}].`); 101 | } 102 | } 103 | 104 | let keys = new Set(Object.keys(actual).concat(Object.keys(expected))); 105 | keys.delete("message"); 106 | keys.delete("file"); 107 | keys.delete("schema"); 108 | keys.delete("locationInFile"); 109 | keys.delete("originalError"); 110 | keys.delete("errors"); 111 | 112 | for (let key of keys) { 113 | expect(actual[key]).to.deep.equal(expected[key], `${p}${key}`); 114 | } 115 | }, 116 | 117 | resource (actual, expected, p = "resource.") { 118 | expect(actual).to.be.an.instanceOf(Resource, `${p} (actual)`); 119 | expect(expected).to.be.an("object", `${p} (expected)`); 120 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 121 | expect(actual.file).to.be.an.instanceOf(File, `${p}file`); 122 | expect(actual.locationInFile).to.be.an.instanceOf(Pointer, `${p}locationInFile`); 123 | 124 | expect(href(actual.uri)).to.equal(href(expected.uri), `${p}uri`); 125 | expect(actual.data).to.deep.equal(expected.data, `${p}data`); 126 | assert.pointer(actual.locationInFile, expected.locationInFile, `${p}locationInFile.`); 127 | 128 | expect(names(actual.anchors)).to.have.same.members(names(expected.anchors), `${p}anchors`); 129 | 130 | for (let [index, expectedAnchor] of entries(expected.anchors)) { 131 | let actualAnchor = actual.anchors[index]; 132 | expect(actualAnchor.resource).to.equal(actual, `${p}anchors[${index}].resource`); 133 | expect(actualAnchor.schema).to.equal(actual.schema, `${p}anchors[${index}].schema`); 134 | expect(actualAnchor.file).to.equal(actual.file, `${p}anchors[${index}].file`); 135 | assert.anchor(actualAnchor, expectedAnchor, `${p}anchors[${index}].`); 136 | } 137 | 138 | expect(values(actual.references)).to.have.same.members(values(expected.references), `${p}references`); 139 | 140 | for (let [index, expectedReference] of entries(expected.references)) { 141 | let actualReference = actual.references[index]; 142 | expect(actualReference.resource).to.equal(actual, `${p}references[${index}].resource`); 143 | expect(actualReference.schema).to.equal(actual.schema, `${p}references[${index}].schema`); 144 | expect(actualReference.file).to.equal(actual.file, `${p}references[${index}].file`); 145 | assert.reference(actualReference, expectedReference, `${p}references[${index}].`); 146 | } 147 | }, 148 | 149 | anchor (actual, expected, p = "anchor.") { 150 | expect(actual).to.be.an.instanceOf(Anchor, `${p} (actual)`); 151 | expect(expected).to.be.an("object", `${p} (expected)`); 152 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 153 | expect(actual.file).to.be.an.instanceOf(File, `${p}file`); 154 | expect(actual.resource).to.be.an.instanceOf(Resource, `${p}resource`); 155 | expect(actual.locationInFile).to.be.an.instanceOf(Pointer, `${p}locationInFile`); 156 | 157 | expect(actual.name).to.equal(expected.name, `${p}name`); 158 | expect(href(actual)).to.equal(href(expected), `${p}uri`); 159 | expect(actual.data).to.be.an("object", `${p}data`).and.deep.equal(expected.data, `${p}data`); 160 | assert.pointer(actual.locationInFile, expected.locationInFile, `${p}locationInFile.`); 161 | }, 162 | 163 | reference (actual, expected, p = "reference.") { 164 | expect(actual).to.be.an.instanceOf(Reference, `${p} (actual)`); 165 | expect(expected).to.be.an("object", `${p} (expected)`); 166 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 167 | expect(actual.file).to.be.an.instanceOf(File, `${p}file`); 168 | expect(actual.resource).to.be.an.instanceOf(Resource, `${p}resource`); 169 | expect(actual.locationInFile).to.be.an.instanceOf(Pointer, `${p}locationInFile`); 170 | 171 | if ("file" in expected) { 172 | expect(href(actual.file)).to.equal(href(expected.file), `${p}file`); 173 | } 174 | 175 | if ("resource" in expected) { 176 | expect(href(actual.resource)).to.equal(href(expected.resource), `${p}resource`); 177 | } 178 | 179 | expect(actual.value).to.equal(expected.value, `${p}value`); 180 | expect(href(actual.targetURI)).to.equal(href(expected.targetURI), `${p}targetURI`); 181 | expect(actual.data).to.be.an("object", `${p}data`).and.deep.equal(expected.data, `${p}data`); 182 | assert.pointer(actual.locationInFile, expected.locationInFile, `${p}locationInFile.`); 183 | }, 184 | 185 | pointer (actual, expected, p = "pointer.") { 186 | expect(actual).to.be.an.instanceOf(Pointer, `${p} (actual)`); 187 | expect(expected).to.be.an("object", `${p} (expected)`); 188 | expect(actual.tokens).to.deep.equal(expected.tokens, `${p}tokens`); 189 | expect(actual.path).to.equal(expected.path, `${p}path`); 190 | expect(actual.hash).to.equal(expected.hash, `${p}hash`); 191 | }, 192 | 193 | resolution (actual, expected, p = "resolution.") { 194 | expect(actual).to.be.an.instanceOf(Resolution, `${p} (actual)`); 195 | expect(expected).to.be.an("object", `${p} (expected)`); 196 | expect(actual.schema).to.be.an.instanceOf(JsonSchema, `${p}schema`); 197 | expect(actual.file).to.be.an.instanceOf(File, `${p}file`); 198 | expect(actual.resource).to.be.an.instanceOf(Resource, `${p}resource`); 199 | expect(actual.locationInFile).to.be.an.instanceOf(Pointer, `${p}locationInFile`); 200 | 201 | expect(href(actual.file)).to.equal(href(expected.file), `${p}file`); 202 | expect(href(actual.resource)).to.equal(href(expected.resource), `${p}resource`); 203 | expect(href(actual)).to.equal(href(expected), `${p}uri`); 204 | expect(actual.data).to.deep.equal(expected.data, `${p}data`); 205 | assert.pointer(actual.locationInFile, expected.locationInFile, `${p}locationInFile.`); 206 | 207 | if (actual.reference || expected.reference) { 208 | expect(expected.reference).to.be.an("object", `${p}reference`); 209 | expect(expected.reference.file).to.be.an.instanceOf(File, `${p}reference.file`); 210 | expect(expected.reference.resource).to.be.an.instanceOf(Resource, `${p}reference.resource`); 211 | assert.reference(actual.reference, expected.reference, `${p}reference.`); 212 | } 213 | else { 214 | expect(actual.reference).to.equal(undefined); 215 | } 216 | 217 | if (actual.previousStep || expected.previousStep) { 218 | expect(actual.previousStep.schema).to.equal(actual.schema, `${p}previousStep.schema`); 219 | assert.resolution(actual.previousStep, expected.previousStep, `${p}previousStep.`); 220 | 221 | let steps = actual.steps; 222 | expect(steps).to.be.an("array", `${p}steps`).with.length.of.at.least(2, `${p}steps.length`); 223 | expect(steps[0]).to.equal(actual.firstStep, `${p}firstStep`); 224 | expect(steps[steps.length - 1]).to.equal(actual, `${p}steps[${steps.length - 1}]`); 225 | } 226 | else { 227 | expect(actual.previousStep).to.equal(undefined); 228 | 229 | let steps = actual.steps; 230 | expect(steps).to.be.an("array", `${p}steps`).with.lengthOf(1, `${p}steps.length`); 231 | expect(steps[0]).to.equal(actual, `${p}steps[0]`); 232 | expect(actual.firstStep).to.equal(actual, `${p}firstStep`); 233 | } 234 | }, 235 | }); 236 | 237 | function entries (array = []) { 238 | return array.entries(); 239 | } 240 | 241 | function paths (files = []) { 242 | return files.map(file => file.path); 243 | } 244 | 245 | function uris (resources = []) { 246 | return resources.map(resource => href(resource)); 247 | } 248 | 249 | function codes (errors = []) { 250 | return errors.map(error => error.code); 251 | } 252 | 253 | function values (refs = []) { 254 | return refs.map(ref => ref.value); 255 | } 256 | 257 | function names (anchors = []) { 258 | return anchors.map(anchor => anchor.name); 259 | } 260 | 261 | function href (obj = {}) { 262 | let url = obj.url || obj.uri || obj; 263 | return (url && url.href) ? url.href : ""; 264 | } 265 | -------------------------------------------------------------------------------- /test/utils/path.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | "use strict"; 3 | 4 | const { host } = require("@jsdevtools/host-environment"); 5 | 6 | if (host.node) { 7 | module.exports = nodePathHelpers(); 8 | } 9 | else { 10 | module.exports = browserPathHelpers(); 11 | } 12 | 13 | /** 14 | * Helper functions for getting local filesystem paths in various formats 15 | */ 16 | function nodePathHelpers () { 17 | const nodePath = require("path"); 18 | const { pathToFileURL } = require("url"); 19 | 20 | return { 21 | /** 22 | * Returns the relative path, formatted correctly for the current OS 23 | */ 24 | rel (relativePath) { 25 | return nodePath.normalize(relativePath); 26 | }, 27 | 28 | /** 29 | * Returns the absolute path 30 | */ 31 | abs (relativePath) { 32 | return nodePath.resolve(relativePath); 33 | }, 34 | 35 | /** 36 | * Returns the path as a "file://" URL object 37 | */ 38 | url (relativePath, hash = "") { 39 | let url = pathToFileURL(relativePath); 40 | url.hash = hash; 41 | return url; 42 | }, 43 | 44 | /** 45 | * Returns the absolute path of the current working directory 46 | */ 47 | cwd () { 48 | return process.cwd(); 49 | } 50 | }; 51 | } 52 | 53 | /** 54 | * Helper functions for getting URLs in various formats 55 | */ 56 | function browserPathHelpers () { 57 | // The URL of the base directory in Karma 58 | let rootURL = new URL("/base/", window.location.href); 59 | 60 | // The URL of the current page directory 61 | let cwd = new URL(".", window.location.href); 62 | 63 | /** 64 | * URI-encodes a path 65 | */ 66 | function encodePath (relativePath) { 67 | return encodeURIComponent(relativePath).split("%2F").join("/"); 68 | } 69 | 70 | return { 71 | /** 72 | * Returns the relative URL 73 | */ 74 | rel (relativePath) { 75 | let url = this.url(relativePath); 76 | let relativeURL = url.href.replace(cwd.href, ""); 77 | return relativeURL; 78 | }, 79 | 80 | /** 81 | * Returns the absolute URL string 82 | */ 83 | abs (relativePath) { 84 | return this.url(relativePath).href; 85 | }, 86 | 87 | /** 88 | * Returns the absolute URL object 89 | */ 90 | url (relativePath, hash = "") { 91 | // Encode special characters in paths 92 | relativePath = encodePath(relativePath); 93 | let url = new URL(relativePath, rootURL); 94 | url.hash = hash; 95 | return url; 96 | }, 97 | 98 | /** 99 | * Returns the URL of the current page. 100 | */ 101 | cwd () { 102 | return cwd; 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", // TODO: Once Edge w/ Chromium goes GA, update this to "esnext" 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["esnext"], 7 | 8 | "outDir": "esm", 9 | "sourceMap": true, 10 | "declaration": true, 11 | 12 | "newLine": "LF", 13 | "forceConsistentCasingInFileNames": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "strictBindCallApply": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": true, 20 | "stripInternal": true, 21 | 22 | "typeRoots": [ 23 | "node_modules/@types", 24 | "src/typings" 25 | ] 26 | }, 27 | "include": [ 28 | "src/**/*.ts" 29 | ], 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } 34 | --------------------------------------------------------------------------------