├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── bin └── json-refs ├── dist ├── json-refs-min.js ├── json-refs.js └── tester.html ├── docs ├── API.md ├── CLI.md └── README.md ├── gulpfile.js ├── index.d.ts ├── index.js ├── jsdoc.config.json ├── lib └── typedefs.js ├── package-lock.json ├── package.json ├── test ├── browser │ ├── documents │ │ ├── circular-child.yaml │ │ ├── circular-local.yaml │ │ ├── circular-root.yaml │ │ ├── nested │ │ │ ├── test-nested-1.yaml │ │ │ └── test-nested.yaml │ │ ├── test-document-1.yaml │ │ ├── test-document-same.yaml │ │ ├── test-document.json │ │ ├── test-document.yaml │ │ ├── test-types.yaml │ │ └── {id} │ │ │ └── person.json │ └── karma.conf.js ├── test-cli.js └── test-json-refs.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # ESLint 2 | # 3 | # Configuration: http://eslint.org/docs/user-guide/configuring 4 | # Rules: http://eslint.org/docs/rules/ 5 | 6 | --- 7 | env: 8 | node: true 9 | 10 | globals: 11 | Promise: true 12 | 13 | rules: 14 | array-bracket-spacing: [2, "never"] 15 | brace-style: [2, "1tbs"] 16 | comma-style: [2, "last"] 17 | computed-property-spacing: [2, "never"] 18 | curly: [2, "multi-line"] 19 | default-case: 2 20 | func-style: [2, "declaration"] 21 | guard-for-in: 2 22 | keyword-spacing: 2 23 | newline-after-var: 2 24 | no-floating-decimal: 2 25 | no-inner-declarations: [2, "both"] 26 | no-multiple-empty-lines: 2 27 | no-nested-ternary: 2 28 | no-path-concat: 2 29 | no-undef: 2 30 | no-unused-vars: 2 31 | object-curly-spacing: [2, "never"] 32 | quotes: [2, "single", "avoid-escape"] 33 | radix: 2 34 | semi: [2, "always"] 35 | space-before-blocks: [2, "always"] 36 | space-before-function-paren: [2, "always"] 37 | space-in-parens: [2, "never"] 38 | spaced-comment: [2, "always"] 39 | strict: [2, "global"] 40 | valid-jsdoc: [2, {"requireReturn": false, "prefer": {"return": "returns"}}] 41 | wrap-iife: 2 42 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.yaml text eol=lf 5 | *.yml text eol=lf 6 | *.js text eol=lf 7 | *.json text eol=lf 8 | *.md text eol=lf 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/npm-debug.log 3 | 4 | coverage/ 5 | .tern-project 6 | .idea/ 7 | .vscode/ 8 | jsconfig.json 9 | *.iml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | language: node_js 5 | node_js: 6 | - "node" 7 | - "6" 8 | sudo: false 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | The purpose of this guide is to familiarize yourself with the json-refs development process. My hopes are that this 2 | guide will make contributing to json-refs much simpler. 3 | 4 | # Development Process 5 | 6 | All of the information below assumes you've installed [Gulp][gulp] globally via `npm install -g gulp-cli`. If you do 7 | not want to install Gulp globally, just replace `gulp` with `node node_modules/gulp/bin/gulp.js` in the examples below. 8 | 9 | Before contributing to json-refs, it makes sense to understand the development process. Git of course is a given so 10 | no time will be spent talking about it. json-refs uses Gulp as its task runner, which is used to build, lint, test, 11 | etc. all parts of json-refs. Below are the gulp tasks: 12 | 13 | * `clean`: Removes all development artifacts *(``coverage`, ...)* 14 | * `docs`: Generates `docs/API.md` from the [jsdoc][jsdoc] in the necessary sources 15 | * `lint`: Lint checks the necessary sources using [ESLint][eslint] 16 | * `test-browser`: Runs the test suite for the browser 17 | * `test-node`: Runs the test suite for Node.js 18 | * `test`: Runs both `test-node` and `test-browser` 19 | 20 | If you just run `gulp`, all of these tasks mentioned above will be ran in the proper order. When working on json-refs 21 | myself, I typically just run `gulp test-node` while working on the bug fix or feature. Once I get the code ready to 22 | commit, I will then run `gulp` to lint check my code, generate the browser builds and sources, ... 23 | 24 | # Reporting Bugs 25 | 26 | To submit new a new bug report, please follow these steps: 27 | 28 | 1. Search that the bug hasn't already been reported 29 | 2. File the bug *(if the bug report is new)* 30 | 31 | Your bug report should meet the following criteria: 32 | 33 | 1. Include a reproduction recipe *(Document the steps required to reproduce the bug including any example code, etc.)* 34 | 2. Include what happens when the bug occurs 35 | 3. Include what you expect to happen when the bug is fixed 36 | 37 | In the end, please provide as much pertinent information as possible when describing the problem. A good bug report is 38 | clear, concise and requires no guess work on our part. Help us help you! *(I couldn't resist...)* 39 | 40 | # Submitting PRs 41 | 42 | To submit a new PR, please follow these steps: 43 | 44 | 1. Write a test to reproduce your bug or to test your enhancement/feature 45 | 2. Write your code *(I typically only run `gulp test-node` while working on the code until I get it done)* 46 | 3. Run `gulp` 47 | 4. Commit 48 | 49 | Your PR should meet the following criteria: 50 | 51 | 1. Should include all generated sources 52 | 2. Should pass lint checking and have all tests passing *(We do have [Travis CI][travis-ci] setup to catch failing lint 53 | checks and failing tests but this is a safety net only)* 54 | 3. Should *ideally* be squashed into one commit *(Regardless of how many commits, just make sure the commit messages are 55 | clear)* 56 | 4. Should include tests *(Bug fixes and features should have tests included with them at all times)* 57 | 58 | [browserify]: http://browserify.org/ 59 | [eslint]: http://eslint.org/ 60 | [gulp]: http://gulpjs.com/ 61 | [jsdoc]: http://usejsdoc.org/ 62 | [npm]: https://www.npmjs.com/ 63 | [travis-ci]: https://travis-ci.org/whitlockjc/json-refs 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jeremy Whitlock 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-refs 2 | 3 | json-refs is a simple library for interacting with [JSON References][json-reference-draft-spec] and 4 | [JSON Pointers][json-pointer-spec]. While the main purpose of this library is to provide JSON References features, 5 | since JSON References are a combination of `Object` structure and a `JSON Pointer`, this library also provides some 6 | features for JSON Pointers as well. 7 | 8 | ## Project Badges 9 | 10 | * Build status: [![Build Status](https://travis-ci.org/whitlockjc/json-refs.svg)](https://travis-ci.org/whitlockjc/json-refs) 11 | * Dependencies: [![Dependencies](https://david-dm.org/whitlockjc/json-refs.svg)](https://david-dm.org/whitlockjc/json-refs) 12 | * Developer dependencies: [![Dev Dependencies](https://david-dm.org/whitlockjc/json-refs/dev-status.svg)](https://david-dm.org/whitlockjc/json-refs#info=devDependencies&view=table) 13 | * Downloads: [![NPM Downloads Per Month](http://img.shields.io/npm/dm/json-refs.svg)](https://www.npmjs.org/package/json-refs) 14 | * Gitter: [![Join the chat at https://gitter.im/whitlockjc/json-refs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/whitlockjc/json-refs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 15 | * License: [![License](http://img.shields.io/npm/l/json-refs.svg)](https://github.com/whitlockjc/json-refs/blob/master/LICENSE) 16 | * Version: [![NPM Version](http://img.shields.io/npm/v/json-refs.svg)](https://www.npmjs.org/package/json-refs) 17 | 18 | ## Documentation 19 | 20 | The documentation for this project can be found at . 21 | Specific documentation can be found here: 22 | 23 | * API documentation can be found at 24 | * CLI can be found at 25 | 26 | ## Installation 27 | 28 | json-refs is available for both Node.js and the browser. Installation instructions for each environment are below. 29 | 30 | ### Browser 31 | 32 | json-refs binaries for the browser are available in the `dist/` directory: 33 | 34 | * [json-refs.js](https://raw.github.com/whitlockjc/json-refs/master/dist/json-refs.js): _2,292kb_, full source source maps 35 | * [json-refs-min.js](https://raw.github.com/whitlockjc/json-refs/master/dist/json-refs-min.js): _148kb_, minified, compressed and no sourcemap 36 | 37 | Of course, these links are for the master builds so feel free to download from the release of your choice. Once you've 38 | gotten them downloaded, to use the standalone binaries, your HTML include might look like this: 39 | 40 | ``` html 41 | 42 | 43 | 44 | ``` 45 | 46 | ### Node.js 47 | 48 | Installation for Node.js applications can be done via [NPM][npm]. 49 | 50 | ``` 51 | npm install json-refs --save 52 | ``` 53 | 54 | If you plan on using the `json-refs` CLI executable, you can install json-refs globally like this: 55 | 56 | ``` 57 | npm install json-refs --global 58 | ``` 59 | 60 | After this, feel free to run `json-refs help` to see what you can do or view the CLI documentation linked above 61 | 62 | [npm]: https://www.npmjs.com/ 63 | [json-reference-draft-spec]: http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 64 | [json-pointer-spec]: http://tools.ietf.org/html/rfc6901 65 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | 3 | ### TBD 4 | * Fix a bug where `resolveRefs` would fail to dereference URI where fragments were encoded with `:`, `/`, `?`, `#`, `@`, `$`, `&`, `+`, `,`, `;`, `=` *(Issue #186)* 5 | 6 | ### v3.0.15 (2020-02-25) 7 | 8 | * Fix a regression where a reference to the original JSON Reference defintion was not available in the resolution 9 | metadata *(Issue #176)* 10 | 11 | ### v3.0.14 (2020-02-25) 12 | 13 | * Fix a bug where `.` and `..` in relative references could be represented wrongly in `refDetails.fqURI` 14 | * Fix a bug where locally-circular references to itself is identified 15 | * Fix a bug where when locally-circular references in relative/remote documents are handled inappropriately such that 16 | the local reference is left intact instead of representing in a relative way *(Issue #175)* 17 | 18 | ### v3.0.13 (2019-04-01) 19 | 20 | * Upgrade `path-loader` so that the location being resolved is passed to `options.loaderOptions.processContent` 21 | *(Issue #157)* 22 | 23 | ### v3.0.12 (2018-10-05) 24 | 25 | * Another attempt at fixing the TypeScript definition for `RetrievedResolvedRefsResults` *(Issue #145)* 26 | 27 | ### v3.0.11 (2018-10-04) 28 | 29 | * Fixed the TypeScript definition for `RetrievedResolvedRefsResults` *(Issue #145)* 30 | 31 | ### v3.0.10 (2018-08-31) 32 | 33 | * Another attempt at fixing the TypeScript declarations *(Issue #139)* 34 | 35 | ### v3.0.9 (2018-08-26) 36 | 37 | * Another attempt at fixing the TypeScript declarations *(Issue #139)* 38 | 39 | ### v3.0.8 (2018-08-08) 40 | 41 | * Fixed API documentation and TypeScript declarations *(Issue #139)* 42 | 43 | ### v3.0.7 (2018-08-04) 44 | 45 | * Upgraded `path-loader` to upgrade `superagent` 46 | 47 | ### v3.0.6 (2018-07-14) 48 | 49 | * Fix bug where `options.location` having URI escapable characters in it breaks resolution *(Issue #138)* 50 | 51 | ### v3.0.5 (2018-07-06) 52 | 53 | * Added the `fqURI` property to the resolved reference metadata *(Issue #136)* 54 | * Fixed bug in circular reference processing where circular references span multiple documents *(Issue #135)* 55 | 56 | ### v3.0.4 (2018-02-11) 57 | 58 | * Fixed issue where `#getRefDetails` did not validate JSON Pointer values leading to runtime failure later when 59 | resolving references *(Issue #131)* 60 | * Fixed issue where URI encoded JSON References were not resolved properly *(Issue #130)* 61 | 62 | ### v3.0.3 (2018-01-03) 63 | 64 | * Fix bug where objects containing a `length` property were not processed properly *(PR #129)* 65 | 66 | ### v3.0.2 (2017-10-11) 67 | 68 | * Fix bug where local references containing remote references were unresolved *(Issue #125)* 69 | 70 | ### v3.0.1 (2017-10-09) 71 | 72 | * Updated dependencies for security 73 | 74 | ### v3.0.0 (2017-04-25) 75 | 76 | * Added `options.location` to allow for better relative reference resolution 77 | * Added `options.resolveCirculars` to allow for resolving circular references *(Issue #74)* 78 | * Removed `options.relativeBase` as it's too confusing and easier to get right using `options.location` 79 | * Fixed accidental feature of resolver that would that resolved remote references against parent documents *(Issue #100)* 80 | * Fixed issue where `json-refs resolve` did not handle a location with a fragment in it *(Issue #104)* 81 | * Fixed issue where `options.filter` could keep remote references from being fully resolved *(Issue #111)* 82 | * Fixed issue where circular reference in remote documents were not handled properly *(Issue #97)* 83 | * Fixed issue where references to the root document were not marked as circular at proper depth *(Issue #88)* 84 | * Fixed issue where references within the root document but outside of `options.subDocPath` were rolled up instead of 85 | reporting the real reference locatin *(Issue #112)* 86 | * Fixed issue where documents could be resolved more than once *(Issues #87, #89 and #103)* 87 | * Fixed issue with remote references not being fully resolved *(Issue #80)* 88 | * Rewrote resolver for accuracy and efficiency *(Issues #80, #87, #88, #89, #97, #100 and #103)* 89 | * Updated `#pathFromPtr` to include the reason why `#isPtr` fails *(Issue #85)* 90 | 91 | ### v2.1.7 (2017-04-22) 92 | 93 | * Updated dependencies for security *(Issues #108, #109)* 94 | 95 | ### v2.1.6 (2016-06-14) 96 | 97 | * Fixed a bug where identifying circular references failed for local, indirect references *(Issue #82)* 98 | 99 | ### v2.1.5 (2016-02-02) 100 | 101 | * Fixed an issue with altering the original input 102 | * Fixed an issue with recursively processing references already planned to be processed created extra reference 103 | metadata which caused issues with resolution *(Issue #73)* 104 | 105 | ### v2.1.4 (2016-02-02) 106 | 107 | * Fixed a problem where multiple references to the same object were not all fully resolved *(Issue #72)* 108 | 109 | ### v2.1.3 (2016-01-31) 110 | 111 | * Fixed a problem where references were not fully resolved for remote references with fragments *(Issue #70)* 112 | * Updated handling of resolving circular references to leave circular references as-is instead of resolving to an empty 113 | object *(Issue #69)* 114 | 115 | ### v2.1.2 (2016-01-23) 116 | 117 | * Scoped fixed for issue #67 to relative references only 118 | * Use Node.js `path` APIs instead of reinventing them 119 | 120 | ### v2.1.1 (2016-01-23) 121 | 122 | * Fixed an issue with `#findRefsAt` and `#resolveRefsAt` and relative locations *(Issue #67)* 123 | * Updated `json-refs resolve` to validate by default to avoid confusion *(Removed the `--validate` flag and replaced 124 | it with the `--force` flag to disable validation)* 125 | 126 | ### v2.1.0 (2016-01-23) 127 | 128 | * First pass at a `json-refs` CLI utility 129 | 130 | ### v2.0.8 (2016-01-23) 131 | 132 | * Fixed an issue with options that filter references *(`options.filter` and `options.subDocPath`)* and the internal 133 | document cache 134 | 135 | ### v2.0.7 (2016-01-23) 136 | 137 | * Further address issues with lack of encoding in JSON References *(path segments instead of fragments)* *(Issue #61)* 138 | 139 | ### v2.0.6 (2016-01-22) 140 | 141 | * Fix an issue where a JSON Reference at the root of the document was not resolved properly *(Issue #65)* 142 | 143 | ### v2.0.5 (2016-01-22) 144 | 145 | * Added support to work with URI encoded JSON References and to handle JSON References with unescaped special 146 | characters *(This means that if you have a reference like `#/paths/{petId}`, which is technically invalid, we will not 147 | mark it as invalid and will process it. It also means if your reference is escaped, like `#/definitions/My%20Pet`, it 148 | will also work as expected.)* *(Issue #61)* 149 | * Fix an issue with combining `options.filter` and `options.includeInvalid` *(Issue #63)* 150 | * We now clone the JSON Reference definition and JSON Reference details respectively for `options.refPreProcessor' and 151 | `options.refPostProcessor` *(Issue #64)* 152 | 153 | ### v2.0.4 (2016-01-21) 154 | 155 | * Fixed a bug where a reference to another object that shares a common pointer could be marked as `circular` 156 | erroneously *(For example: `#/definitions/Person/properties/name` references `#/definitions/PersonWithName` shares the 157 | same `#/definitions/Person` base but are to different objects)* *(PR #59)* 158 | 159 | ### v2.0.3 (2016-01-11) 160 | 161 | * Fixed a problem when loading relative paths with relative references 162 | 163 | ### v2.0.2 (2016-01-06) 164 | 165 | * Fixed another inconsistency with error handling 166 | 167 | ### v2.0.1 (2016-01-06) 168 | 169 | * Fix a consistency issue with error handling 170 | 171 | ### v2.0.0 (2016-01-06) 172 | 173 | * Added `#clearCache` to allow you to clear the remote document cache and its JSON References details 174 | * Added `#decodePath` to allow you to take an array of path segments and decode their JSON Pointer tokens *(Issue #47)* 175 | * Added `#encodePath` to allow you to take an array of path segments and encode the special JSON Pointer characters *(Issue #47)* 176 | * Added `#findRefsAt` to allow you to retrieve a remote document and then find its references 177 | * Added `#getRefDetails` to centralize the code used to generate reference metadata *(Also allows you to see why an 178 | object you expect to be returned by `#findRefs` is not returned.)* 179 | * Added `#resolveRefsAt` to allow you to retrieve a remote document and then resolve its references 180 | * Fixed a bug where Windows paths containing `\` were not processed properly *(Issue #48)* 181 | * Removed `#resolveLocalRefs` 182 | * Renamed `#isJsonPointer` to `#isPtr` 183 | * Renamed `#isJsonReference` to `#isRef` 184 | * Renamed `#pathFromPointer` to `#pathFromPtr` 185 | * Renamed `#pathToPointer` to `#pathToPtr` 186 | * Updated `#findRefs` to no longer process child properties of JSON Reference objects 187 | * Updated `#findRefs` to no longer use [js-traverse](https://github.com/substack/js-traverse) 188 | * Updated `#findRefs` to use an *options* object 189 | * `options.filter` allows you to filter references 190 | * `options.includeInvalid` allows you to include JSON Reference details for invalid references 191 | * `options.refPreProcessor` allows you to take a JSON Reference like object and process it prior to `#isRef` being 192 | called against it 193 | * `options.refPostProcessor` allows you to take the JSON Reference details/metadata and process it *(This runs prior 194 | to `options.filter`)* 195 | * `options.subDocPath` allows you to find JSON References at/below a certain location in the document 196 | like objects that fail validation so that you can identify invalid JSON References easier *(See API documentation for details)* 197 | * Updated `#isPtr` to validate the `$ref` value is a URI instead of treating all string values as valid 198 | * Updated `#isPtr` to validate the [tokens](http://tools.ietf.org/html/rfc6901#section-4) *(Issue #47)* 199 | * Updated `#isPtr` to have an optional second argument which dictates whether or not to throw an `Error` for invalid JSON 200 | Pointer values *(The `Error` would have the details as to why the provided value is not a JSON Pointer)* *(Issue #47)* 201 | * Updated `#isRef` to have an optional second argument which dictates whether or not to throw an `Error` for invalid JSON 202 | Reference values *(The `Error` would have the details as to why the provided value is not a JSON Reference)* *(Issue #47)* 203 | * Updated `#pathToPtr` to take an optional second argument to allow for returning both hash-based *(default)* and 204 | slash-based JSON Pointers 205 | * Updated `#resolveRefs` to work with the new `options` object 206 | * `options.depth` was removed 207 | * `options.loaderOptions` is now used for the options passed to [path-loader](https://github.com/whitlockjc/path-loader) 208 | * `options.prepareRequest` was removed *(Now available at `options.loaderOptions.prepareRequest`)* 209 | * `options.processContent` was removed *(Now available at `options.loaderOptions.processContent`)* 210 | * `options.location` was removed *(Now available at `options.relativeBase`)* 211 | * `options.relativeBase` is used to specify the root location to resolve relative references from 212 | * All `options` used by `#findRefs` are supported here 213 | 214 | ### v1.3.0 (2015-11-19) 215 | 216 | * Added `#resolveLocalRefs` to avoid forcing consumers only resolving local references to use a callback/Promise based 217 | API *(`#resolveRefs`)* 218 | * Update reference metadata to record when a reference is remote 219 | 220 | ### v1.2.1 (2015-11-18) 221 | 222 | * Updated `#findRefs` and `#resolveRefs` to work with arrays and objects *(Issue #39)* 223 | 224 | ### v1.2.0 (2015-11-16) 225 | 226 | * Added options to `#resolveRefs` that allow you to choose which reference type(s) to resolve *(Issue #27, PR #41)* 227 | 228 | ### v1.1.2 (2015-10-21) 229 | 230 | * Fix a bug in the handling of remote reference errors _(Issue #37)_ 231 | 232 | ### v1.1.1 (2015-09-28) 233 | 234 | * Fix issue where a hash in `options.location` could create a double slash in the requested path _(Issue #34)_ 235 | 236 | ### v1.1.0 (2015-09-18) 237 | 238 | * Fixed support for service/web workers *(Issue #32)* 239 | 240 | ### v1.0.5 (2015-08-31) 241 | 242 | * Fixed a bug where unresolved references occur for remote document fragments did not get reported 243 | 244 | ### v1.0.4 (2015-08-31) 245 | 246 | * Fix problem where local references in a remote document, referenced using a fragment, were not resolved _(Issue #30)_ 247 | 248 | ### v1.0.3 (2015-08-31) 249 | 250 | * Fix problem where local references in a remote document were not resolved _(Issue #30)_ 251 | 252 | ### v1.0.2 (2015-07-21) 253 | 254 | * Fix problem where references to schemas with circular composition/inheritance could result in attempting to update 255 | reference metadata that does not exist 256 | 257 | ### v1.0.1 (2015-07-20) 258 | 259 | * Fix problem where circular references caused by composition/inheritance wasn't caught properly 260 | 261 | ### v1.0.0 (2015-07-17) 262 | 263 | * Circular references are now identified in metadata _(Issue #22)_ 264 | * Fixed a few scenarios where local self references to root didn't work right 265 | * Rewrote using ES5 which removed the need for `lodash-compat` 266 | * `#resolveRefs` now collapses all reference pointers so that the metadata key is now the reference to the local 267 | document instead of where 268 | its `$ref` was *(This is a breaking change and that is why we are doing a `1.0` release)* 269 | * `#resolveRefs` now defers local reference resolution until after remote references are resolved _(Issue #26)_ 270 | * `#resolveRefs` now handles recursive relative references gracefully _(Issue #24)_ 271 | * `#resolveRefs` now records metadata for remote references _(Issue #25)_ 272 | * `#resolveRefs` now supports callbacks, as always, and promises 273 | _(Always returns a promise even if callbacks are used)_ 274 | 275 | ### v0.3.2 (2015-07-08) 276 | 277 | * Unresolved references leave the original reference in the document so as not to break JSON Schema validation 278 | 279 | ### v0.3.1 (2015-07-08) 280 | 281 | * Errors resolving remote references no longer bubble up errors and instead show up in metadata as unresolved 282 | 283 | ### v0.3.0 (2015-07-08) 284 | 285 | * Fix issue with Bower build as it had old dependency paths in it *(PR #15)* 286 | * Fix issue with circular references not being detected in arrays *(Issue #20)* 287 | * Fix problem with references at the root of the document and having hashes *(Issue #19)* 288 | * Support relative references *(Issue 11)* 289 | * Support to specify a depth for `#resolveRefs` for circular references *(Issue #5)* 290 | 291 | ### v0.2.0 (2015-05-12) 292 | 293 | * Replace file loading with [path-loader](https://github.com/whitlockjc/path-loader) 294 | 295 | ### v0.1.10 (2015-04-16) 296 | 297 | * Fixed an issue due to difference in superagent in browser vs. Node.js 298 | 299 | ### v0.1.9 (2015-04-16) 300 | 301 | * Fixed a browser build issue 302 | 303 | ### v0.1.8 (2015-04-16) 304 | 305 | * Updated `isRemotePointer` to only return `true` for explicit URLs or relative paths 306 | 307 | ### v0.1.7 (2015-04-16) 308 | 309 | * Added support in `resolveRefs` to alter a remote request prior to sending the request _(Useful for authentication)_ 310 | _(Issue #12)_ 311 | * Added support in `resolveRefs` to process the remote request responses 312 | * Fixed bug in `resolveRefs` where multiple remote references resulted in callback being called multiple times 313 | * Updated `isRemotePointer` to handle relative references and file URL references _(Issue #9)_ 314 | * Updated `resolveRefs` to return resolution metadata _(What references were resolved, where they were located and what 315 | they resolved to)_ 316 | -------------------------------------------------------------------------------- /bin/json-refs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2014 Jeremy Whitlock 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | 'use strict'; 28 | 29 | // Set here so that commander does the right thing 30 | process.stdout.columns = 120; 31 | 32 | var JsonRefs = require('..'); 33 | var program = require('commander'); 34 | var pkg = require('../package.json'); 35 | var YAML = require('js-yaml'); 36 | 37 | // Load promises polyfill if necessary 38 | /* istanbul ignore if */ 39 | if (typeof Promise === 'undefined') { 40 | require('native-promise-only'); 41 | } 42 | 43 | function exitWithError (err) { 44 | console.error(); 45 | console.error(' error: ' + err.message); 46 | console.error(); // Here only to match the output of commander.js 47 | 48 | process.exit(1); 49 | } 50 | 51 | function optionArrayAppender (values) { 52 | values = values || []; 53 | 54 | return function (value) { 55 | values.push(value); 56 | 57 | return values; 58 | }; 59 | } 60 | 61 | function handleUnknownCommand (command) { 62 | // Using log instead of error since commander.js uses console.log for help output 63 | console.log(program._name + ' does not support the ' + command.parent.args[0] + ' command.'); 64 | 65 | program.outputHelp(); 66 | }; 67 | 68 | // Set name and version 69 | program._name = 'json-refs'; 70 | program.version(pkg.version); 71 | 72 | // Help command 73 | program 74 | .command('help [command]') 75 | .description('Display help information') 76 | .action(function (name) { 77 | var command; 78 | 79 | if (typeof name !== 'undefined') { 80 | this.parent.commands.forEach(function (cmd) { 81 | if (cmd._name === name) { 82 | command = cmd; 83 | } 84 | }); 85 | 86 | if (typeof command === 'undefined') { 87 | handleUnknownCommand(name); 88 | } else { 89 | command.help(); 90 | } 91 | } else { 92 | program.outputHelp(); 93 | } 94 | }); 95 | 96 | // Resolve command 97 | program 98 | .command('resolve ') 99 | .description('Prints document at location with its JSON References resolved') 100 | .option('-f, --force', 'Do not fail when the document has invalid JSON References') 101 | .option('-H, --header
', 'The header to use when retrieving a remote document', optionArrayAppender(), []) 102 | .option('-I, --filter ', 'The type of JSON References to resolved', optionArrayAppender(), []) 103 | .option('-w, --warnings-as-errors', 'Treat warnings as errors') 104 | .option('-y, --yaml', 'Output as YAML') 105 | .action(function (location) { 106 | var options = { 107 | loaderOptions: { 108 | processContent: function (res, callback) { 109 | callback(undefined, YAML.safeLoad(res.text)); 110 | } 111 | } 112 | }; 113 | var that = this; 114 | 115 | if (this.filter.length > 0) { 116 | options.filter = this.filter; 117 | } 118 | 119 | if (!this.force) { 120 | options.includeInvalid = true; 121 | } 122 | 123 | if (this.header.length > 0) { 124 | options.loaderOptions.prepareRequest = function (req, callback) { 125 | that.header.forEach(function (header) { 126 | var headerParts = header.split(': '); 127 | 128 | req.set(headerParts[0], headerParts[1]); 129 | }); 130 | 131 | callback(undefined, req); 132 | }; 133 | } 134 | 135 | JsonRefs.resolveRefsAt(location, options) 136 | .then(function (results) { 137 | var errors = []; 138 | 139 | if (!that.force) { 140 | Object.keys(results.refs).sort().forEach(function (refPtr) { 141 | var refDetails = results.refs[refPtr]; 142 | 143 | if (refDetails.type === 'invalid' || refDetails.error || (that.warningsAsErrors && refDetails.warning)) { 144 | errors.push(' ' + refPtr + ': ' + (refDetails.warning ? refDetails.warning : refDetails.error)); 145 | } 146 | }); 147 | } 148 | 149 | if (errors.length > 0 && !that.force) { 150 | throw new Error('Document has invalid references:\n\n' + errors.join('\n')); 151 | } else { 152 | console.log(that.yaml || location.endsWith('.yaml') ? 153 | YAML.safeDump(results.resolved, {noRefs: true}) : 154 | JSON.stringify(results.resolved, null, 2)); 155 | } 156 | }) 157 | .catch(exitWithError); 158 | }); 159 | 160 | // Default command (handles all unregistered commands) 161 | program 162 | .command('*', null, {noHelp: true}) // null is required to avoid the implicit 'help' command being added 163 | .action(function (cmd) { 164 | handleUnknownCommand(cmd); 165 | }); 166 | 167 | // Process the CLI arguments and run 168 | if (!process.argv.slice(2).length) { 169 | program.outputHelp(); 170 | } else { 171 | program.parse(process.argv); 172 | } 173 | -------------------------------------------------------------------------------- /dist/tester.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | JsonRefs Test Page 4 | 5 | 6 |

Resolving references in the following:

7 |
{
 8 |   $ref: 'https://api.github.com/repos/whitlockjc/json-refs'
 9 | }
10 | 11 | 12 | 23 | 24 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## json-refs 4 | Various utilities for JSON References *(http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)* and 5 | JSON Pointers *(https://tools.ietf.org/html/rfc6901)*. 6 | 7 | 8 | * [json-refs](#module_json-refs) 9 | * [.JsonRefsOptions](#module_json-refs.JsonRefsOptions) : object 10 | * [.RefDetailsFilter](#module_json-refs.RefDetailsFilter) ⇒ boolean 11 | * [.RefPostProcessor](#module_json-refs.RefPostProcessor) ⇒ object 12 | * [.RefPreProcessor](#module_json-refs.RefPreProcessor) ⇒ object 13 | * [.ResolvedRefDetails](#module_json-refs.ResolvedRefDetails) : [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) 14 | * [.ResolvedRefsResults](#module_json-refs.ResolvedRefsResults) : object 15 | * [.RetrievedRefsResults](#module_json-refs.RetrievedRefsResults) : [ResolvedRefsResults](#module_json-refs.ResolvedRefsResults) 16 | * [.RetrievedResolvedRefsResults](#module_json-refs.RetrievedResolvedRefsResults) : object 17 | * [.UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) : object 18 | * [.clearCache()](#module_json-refs.clearCache) 19 | * [.decodePath(path)](#module_json-refs.decodePath) ⇒ Array.<string> 20 | * [.encodePath(path)](#module_json-refs.encodePath) ⇒ Array.<string> 21 | * [.findRefs(obj, [options])](#module_json-refs.findRefs) ⇒ Object.<string, ([UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails)\|undefined)> 22 | * [.findRefsAt(location, [options])](#module_json-refs.findRefsAt) ⇒ [Promise.<RetrievedRefsResults>](#module_json-refs.RetrievedRefsResults) 23 | * [.getRefDetails(obj)](#module_json-refs.getRefDetails) ⇒ [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) 24 | * [.isPtr(ptr, [throwWithDetails])](#module_json-refs.isPtr) ⇒ boolean 25 | * [.isRef(obj, [throwWithDetails])](#module_json-refs.isRef) ⇒ boolean 26 | * [.pathFromPtr(ptr)](#module_json-refs.pathFromPtr) ⇒ Array.<string> 27 | * [.pathToPtr(path, [hashPrefix])](#module_json-refs.pathToPtr) ⇒ string 28 | * [.resolveRefs(obj, [options])](#module_json-refs.resolveRefs) ⇒ [Promise.<ResolvedRefsResults>](#module_json-refs.ResolvedRefsResults) 29 | * [.resolveRefsAt(location, [options])](#module_json-refs.resolveRefsAt) ⇒ [Promise.<RetrievedResolvedRefsResults>](#module_json-refs.RetrievedResolvedRefsResults) 30 | 31 | 32 | 33 | ### json-refs.JsonRefsOptions : object 34 | The options used for various JsonRefs APIs. 35 | 36 | **Kind**: static typedef of [json-refs](#module_json-refs) 37 | **Properties** 38 | 39 | | Name | Type | Default | Description | 40 | | --- | --- | --- | --- | 41 | | filter | string | Array.<string> | function | "function () {return true;}" | The filter to use when gathering JSON References *(If this value is a single string or an array of strings, the value(s) are expected to be the `type(s)` you are interested in collecting as described in [getRefDetails](#module_json-refs.getRefDetails). If it is a function, it is expected that the function behaves like [RefDetailsFilter](#module_json-refs.RefDetailsFilter).)* | 42 | | includeInvalid | boolean | false | Whether or not to include invalid JSON Reference details *(This will make it so that objects that are like JSON Reference objects, as in they are an `Object` and the have a `$ref` property, but fail validation will be included. This is very useful for when you want to know if you have invalid JSON Reference definitions. This will not mean that APIs will process invalid JSON References but the reasons as to why the JSON References are invalid will be included in the returned metadata.)* | 43 | | loaderOptions | object | | The options to pass to [PathLoader~load](https://github.com/whitlockjc/path-loader/blob/master/docs/API.md#module_PathLoader.load) | 44 | | location | string | "root.json" | The location of the document being processed *(This property is only useful when resolving references as it will be used to locate relative references found within the document being resolved. If this value is relative, [path-loader](https://github.com/whitlockjc/path-loader) will use `window.location.href` for the browser and `process.cwd()` for Node.js.)* | 45 | | refPreProcessor | [RefPreProcessor](#module_json-refs.RefPreProcessor) | | The callback used to pre-process a JSON Reference like object *(This is called prior to validating the JSON Reference like object and getting its details)* | 46 | | refPostProcessor | [RefPostProcessor](#module_json-refs.RefPostProcessor) | | The callback used to post-process the JSON Reference metadata *(This is called prior filtering the references)* | 47 | | resolveCirculars | boolean | false | Whether to resolve circular references | 48 | | subDocPath | string | Array.<string> | "[]" | The JSON Pointer or array of path segments to the sub document location to search from | 49 | 50 | 51 | 52 | ### json-refs.RefDetailsFilter ⇒ boolean 53 | Simple function used to filter out JSON References. 54 | 55 | **Kind**: static typedef of [json-refs](#module_json-refs) 56 | **Returns**: boolean - whether the JSON Reference should be filtered *(out)* or not 57 | 58 | | Param | Type | Description | 59 | | --- | --- | --- | 60 | | refDetails | [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) | The JSON Reference details to test | 61 | | path | Array.<string> | The path to the JSON Reference | 62 | 63 | 64 | 65 | ### json-refs.RefPostProcessor ⇒ object 66 | Simple function used to post-process a JSON Reference details. 67 | 68 | **Kind**: static typedef of [json-refs](#module_json-refs) 69 | **Returns**: object - the processed JSON Reference details object 70 | 71 | | Param | Type | Description | 72 | | --- | --- | --- | 73 | | refDetails | [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) | The JSON Reference details to test | 74 | | path | Array.<string> | The path to the JSON Reference | 75 | 76 | 77 | 78 | ### json-refs.RefPreProcessor ⇒ object 79 | Simple function used to pre-process a JSON Reference like object. 80 | 81 | **Kind**: static typedef of [json-refs](#module_json-refs) 82 | **Returns**: object - the processed JSON Reference like object 83 | 84 | | Param | Type | Description | 85 | | --- | --- | --- | 86 | | obj | object | The JSON Reference like object | 87 | | path | Array.<string> | The path to the JSON Reference like object | 88 | 89 | 90 | 91 | ### json-refs.ResolvedRefDetails : [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) 92 | Detailed information about resolved JSON References. 93 | 94 | **Kind**: static typedef of [json-refs](#module_json-refs) 95 | **Properties** 96 | 97 | | Name | Type | Description | 98 | | --- | --- | --- | 99 | | circular | boolean | Whether or not the JSON Reference is circular *(Will not be set if the JSON Reference is not circular)* | 100 | | fqURI | string | The fully-qualified version of the `uri` property for [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) but with the value being relative to the root document | 101 | | missing | boolean | Whether or not the referenced value was missing or not *(Will not be set if the referenced value is not missing)* | 102 | | value | \* | The referenced value *(Will not be set if the referenced value is missing)* | 103 | 104 | 105 | 106 | ### json-refs.ResolvedRefsResults : object 107 | The results of resolving the JSON References of an array/object. 108 | 109 | **Kind**: static typedef of [json-refs](#module_json-refs) 110 | **Properties** 111 | 112 | | Name | Type | Description | 113 | | --- | --- | --- | 114 | | refs | [ResolvedRefDetails](#module_json-refs.ResolvedRefDetails) | An object whose keys are JSON Pointers *(fragment version)* to where the JSON Reference is defined and whose values are [ResolvedRefDetails](#module_json-refs.ResolvedRefDetails) | 115 | | resolved | object | The array/object with its JSON References fully resolved | 116 | 117 | 118 | 119 | ### json-refs.RetrievedRefsResults : [ResolvedRefsResults](#module_json-refs.ResolvedRefsResults) 120 | An object containing the retrieved document and detailed information about its JSON References. 121 | 122 | **Kind**: static typedef of [json-refs](#module_json-refs) 123 | **Properties** 124 | 125 | | Name | Type | Description | 126 | | --- | --- | --- | 127 | | value | object | The retrieved document | 128 | 129 | 130 | 131 | ### json-refs.RetrievedResolvedRefsResults : object 132 | An object containing the retrieved document, the document with its references resolved and detailed information 133 | about its JSON References. 134 | 135 | **Kind**: static typedef of [json-refs](#module_json-refs) 136 | **Properties** 137 | 138 | | Name | Type | Description | 139 | | --- | --- | --- | 140 | | refs | [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) | An object whose keys are JSON Pointers *(fragment version)* to where the JSON Reference is defined and whose values are [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) | 141 | | resolved | object | The array/object with its JSON References fully resolved | 142 | | value | object | The retrieved document | 143 | 144 | 145 | 146 | ### json-refs.UnresolvedRefDetails : object 147 | Detailed information about unresolved JSON References. 148 | 149 | **Kind**: static typedef of [json-refs](#module_json-refs) 150 | **Properties** 151 | 152 | | Name | Type | Description | 153 | | --- | --- | --- | 154 | | def | object | The JSON Reference definition | 155 | | error | string | The error information for invalid JSON Reference definition *(Only present when the JSON Reference definition is invalid or there was a problem retrieving a remote reference during resolution)* | 156 | | uri | string | The URI portion of the JSON Reference | 157 | | uriDetails | object | Detailed information about the URI as provided by [URI.parse](https://github.com/garycourt/uri-js). | 158 | | type | string | The JSON Reference type *(This value can be one of the following: `invalid`, `local`, `relative` or `remote`.)* | 159 | | warning | string | The warning information *(Only present when the JSON Reference definition produces a warning)* | 160 | 161 | 162 | 163 | ### json-refs.clearCache() 164 | Clears the internal cache of remote documents, reference details, etc. 165 | 166 | **Kind**: static method of [json-refs](#module_json-refs) 167 | 168 | 169 | ### json-refs.decodePath(path) ⇒ Array.<string> 170 | Takes an array of path segments and decodes the JSON Pointer tokens in them. 171 | 172 | **Kind**: static method of [json-refs](#module_json-refs) 173 | **Returns**: Array.<string> - the array of path segments with their JSON Pointer tokens decoded 174 | **Throws**: 175 | 176 | - Error if the path is not an `Array` 177 | 178 | **See**: [https://tools.ietf.org/html/rfc6901#section-3](https://tools.ietf.org/html/rfc6901#section-3) 179 | 180 | | Param | Type | Description | 181 | | --- | --- | --- | 182 | | path | Array.<string> | The array of path segments | 183 | 184 | 185 | 186 | ### json-refs.encodePath(path) ⇒ Array.<string> 187 | Takes an array of path segments and encodes the special JSON Pointer characters in them. 188 | 189 | **Kind**: static method of [json-refs](#module_json-refs) 190 | **Returns**: Array.<string> - the array of path segments with their JSON Pointer tokens encoded 191 | **Throws**: 192 | 193 | - Error if the path is not an `Array` 194 | 195 | **See**: [https://tools.ietf.org/html/rfc6901#section-3](https://tools.ietf.org/html/rfc6901#section-3) 196 | 197 | | Param | Type | Description | 198 | | --- | --- | --- | 199 | | path | Array.<string> | The array of path segments | 200 | 201 | 202 | 203 | ### json-refs.findRefs(obj, [options]) ⇒ Object.<string, ([UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails)\|undefined)> 204 | Finds JSON References defined within the provided array/object. 205 | 206 | **Kind**: static method of [json-refs](#module_json-refs) 207 | **Returns**: Object.<string, ([UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails)\|undefined)> - an object whose keys are JSON Pointers 208 | *(fragment version)* to where the JSON Reference is defined and whose values are [UnresolvedRefDetails](UnresolvedRefDetails). 209 | **Throws**: 210 | 211 | - Error when the input arguments fail validation or if `options.subDocPath` points to an invalid location 212 | 213 | 214 | | Param | Type | Description | 215 | | --- | --- | --- | 216 | | obj | array | object | The structure to find JSON References within | 217 | | [options] | [JsonRefsOptions](#module_json-refs.JsonRefsOptions) | The JsonRefs options | 218 | 219 | **Example** 220 | ```js 221 | // Finding all valid references 222 | var allRefs = JsonRefs.findRefs(obj); 223 | // Finding all remote references 224 | var remoteRefs = JsonRefs.findRefs(obj, {filter: ['relative', 'remote']}); 225 | // Finding all invalid references 226 | var invalidRefs = JsonRefs.findRefs(obj, {filter: 'invalid', includeInvalid: true}); 227 | ``` 228 | 229 | 230 | ### json-refs.findRefsAt(location, [options]) ⇒ [Promise.<RetrievedRefsResults>](#module_json-refs.RetrievedRefsResults) 231 | Finds JSON References defined within the document at the provided location. 232 | 233 | This API is identical to [findRefs](findRefs) except this API will retrieve a remote document and then 234 | return the result of [findRefs](findRefs) on the retrieved document. 235 | 236 | **Kind**: static method of [json-refs](#module_json-refs) 237 | **Returns**: [Promise.<RetrievedRefsResults>](#module_json-refs.RetrievedRefsResults) - a promise that resolves a 238 | [RetrievedRefsResults](#module_json-refs.RetrievedRefsResults) and rejects with an `Error` when the input arguments fail validation, 239 | when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 240 | resource 241 | 242 | | Param | Type | Description | 243 | | --- | --- | --- | 244 | | location | string | The location to retrieve *(Can be relative or absolute, just make sure you look at the [options documentation](#module_json-refs.JsonRefsOptions) to see how relative references are handled.)* | 245 | | [options] | [JsonRefsOptions](#module_json-refs.JsonRefsOptions) | The JsonRefs options | 246 | 247 | **Example** 248 | ```js 249 | // Example that only resolves references within a sub document 250 | JsonRefs.findRefsAt('http://petstore.swagger.io/v2/swagger.json', { 251 | subDocPath: '#/definitions' 252 | }) 253 | .then(function (res) { 254 | // Do something with the response 255 | // 256 | // res.refs: JSON Reference locations and details 257 | // res.value: The retrieved document 258 | }, function (err) { 259 | console.log(err.stack); 260 | }); 261 | ``` 262 | 263 | 264 | ### json-refs.getRefDetails(obj) ⇒ [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) 265 | Returns detailed information about the JSON Reference. 266 | 267 | **Kind**: static method of [json-refs](#module_json-refs) 268 | **Returns**: [UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails) - the detailed information 269 | 270 | | Param | Type | Description | 271 | | --- | --- | --- | 272 | | obj | object | The JSON Reference definition | 273 | 274 | 275 | 276 | ### json-refs.isPtr(ptr, [throwWithDetails]) ⇒ boolean 277 | Returns whether the argument represents a JSON Pointer. 278 | 279 | A string is a JSON Pointer if the following are all true: 280 | 281 | * The string is of type `String` 282 | * The string must be empty, `#` or start with a `/` or `#/` 283 | 284 | **Kind**: static method of [json-refs](#module_json-refs) 285 | **Returns**: boolean - the result of the check 286 | **Throws**: 287 | 288 | - error when the provided value is invalid and the `throwWithDetails` argument is `true` 289 | 290 | **See**: [https://tools.ietf.org/html/rfc6901#section-3](https://tools.ietf.org/html/rfc6901#section-3) 291 | 292 | | Param | Type | Default | Description | 293 | | --- | --- | --- | --- | 294 | | ptr | string | | The string to check | 295 | | [throwWithDetails] | boolean | false | Whether or not to throw an `Error` with the details as to why the value provided is invalid | 296 | 297 | **Example** 298 | ```js 299 | // Separating the different ways to invoke isPtr for demonstration purposes 300 | if (isPtr(str)) { 301 | // Handle a valid JSON Pointer 302 | } else { 303 | // Get the reason as to why the value is not a JSON Pointer so you can fix/report it 304 | try { 305 | isPtr(str, true); 306 | } catch (err) { 307 | // The error message contains the details as to why the provided value is not a JSON Pointer 308 | } 309 | } 310 | ``` 311 | 312 | 313 | ### json-refs.isRef(obj, [throwWithDetails]) ⇒ boolean 314 | Returns whether the argument represents a JSON Reference. 315 | 316 | An object is a JSON Reference only if the following are all true: 317 | 318 | * The object is of type `Object` 319 | * The object has a `$ref` property 320 | * The `$ref` property is a valid URI *(We do not require 100% strict URIs and will handle unescaped special 321 | characters.)* 322 | 323 | **Kind**: static method of [json-refs](#module_json-refs) 324 | **Returns**: boolean - the result of the check 325 | **Throws**: 326 | 327 | - error when the provided value is invalid and the `throwWithDetails` argument is `true` 328 | 329 | **See**: [http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3](http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3) 330 | 331 | | Param | Type | Default | Description | 332 | | --- | --- | --- | --- | 333 | | obj | object | | The object to check | 334 | | [throwWithDetails] | boolean | false | Whether or not to throw an `Error` with the details as to why the value provided is invalid | 335 | 336 | **Example** 337 | ```js 338 | // Separating the different ways to invoke isRef for demonstration purposes 339 | if (isRef(obj)) { 340 | // Handle a valid JSON Reference 341 | } else { 342 | // Get the reason as to why the value is not a JSON Reference so you can fix/report it 343 | try { 344 | isRef(str, true); 345 | } catch (err) { 346 | // The error message contains the details as to why the provided value is not a JSON Reference 347 | } 348 | } 349 | ``` 350 | 351 | 352 | ### json-refs.pathFromPtr(ptr) ⇒ Array.<string> 353 | Returns an array of path segments for the provided JSON Pointer. 354 | 355 | **Kind**: static method of [json-refs](#module_json-refs) 356 | **Returns**: Array.<string> - the path segments 357 | **Throws**: 358 | 359 | - Error if the provided `ptr` argument is not a JSON Pointer 360 | 361 | 362 | | Param | Type | Description | 363 | | --- | --- | --- | 364 | | ptr | string | The JSON Pointer | 365 | 366 | 367 | 368 | ### json-refs.pathToPtr(path, [hashPrefix]) ⇒ string 369 | Returns a JSON Pointer for the provided array of path segments. 370 | 371 | **Note:** If a path segment in `path` is not a `String`, it will be converted to one using `JSON.stringify`. 372 | 373 | **Kind**: static method of [json-refs](#module_json-refs) 374 | **Returns**: string - the corresponding JSON Pointer 375 | **Throws**: 376 | 377 | - Error if the `path` argument is not an array 378 | 379 | 380 | | Param | Type | Default | Description | 381 | | --- | --- | --- | --- | 382 | | path | Array.<string> | | The array of path segments | 383 | | [hashPrefix] | boolean | true | Whether or not create a hash-prefixed JSON Pointer | 384 | 385 | 386 | 387 | ### json-refs.resolveRefs(obj, [options]) ⇒ [Promise.<ResolvedRefsResults>](#module_json-refs.ResolvedRefsResults) 388 | Finds JSON References defined within the provided array/object and resolves them. 389 | 390 | **Kind**: static method of [json-refs](#module_json-refs) 391 | **Returns**: [Promise.<ResolvedRefsResults>](#module_json-refs.ResolvedRefsResults) - a promise that resolves a 392 | [ResolvedRefsResults](#module_json-refs.ResolvedRefsResults) and rejects with an `Error` when the input arguments fail validation, 393 | when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 394 | resource 395 | 396 | | Param | Type | Description | 397 | | --- | --- | --- | 398 | | obj | array | object | The structure to find JSON References within | 399 | | [options] | [JsonRefsOptions](#module_json-refs.JsonRefsOptions) | The JsonRefs options | 400 | 401 | **Example** 402 | ```js 403 | // Example that only resolves relative and remote references 404 | JsonRefs.resolveRefs(swaggerObj, { 405 | filter: ['relative', 'remote'] 406 | }) 407 | .then(function (res) { 408 | // Do something with the response 409 | // 410 | // res.refs: JSON Reference locations and details 411 | // res.resolved: The document with the appropriate JSON References resolved 412 | }, function (err) { 413 | console.log(err.stack); 414 | }); 415 | ``` 416 | 417 | 418 | ### json-refs.resolveRefsAt(location, [options]) ⇒ [Promise.<RetrievedResolvedRefsResults>](#module_json-refs.RetrievedResolvedRefsResults) 419 | Resolves JSON References defined within the document at the provided location. 420 | 421 | This API is identical to [resolveRefs](#module_json-refs.resolveRefs) except this API will retrieve a remote document and 422 | then return the result of [resolveRefs](#module_json-refs.resolveRefs) on the retrieved document. 423 | 424 | **Kind**: static method of [json-refs](#module_json-refs) 425 | **Returns**: [Promise.<RetrievedResolvedRefsResults>](#module_json-refs.RetrievedResolvedRefsResults) - a promise that resolves a 426 | [RetrievedResolvedRefsResults](#module_json-refs.RetrievedResolvedRefsResults) and rejects with an `Error` when the input arguments fail 427 | validation, when `options.subDocPath` points to an invalid location or when the location argument points to an 428 | unloadable resource 429 | 430 | | Param | Type | Description | 431 | | --- | --- | --- | 432 | | location | string | The location to retrieve *(Can be relative or absolute, just make sure you look at the [options documentation](#module_json-refs.JsonRefsOptions) to see how relative references are handled.)* | 433 | | [options] | [JsonRefsOptions](#module_json-refs.JsonRefsOptions) | The JsonRefs options | 434 | 435 | **Example** 436 | ```js 437 | // Example that loads a JSON document (No options.loaderOptions.processContent required) and resolves all references 438 | JsonRefs.resolveRefsAt('./swagger.json') 439 | .then(function (res) { 440 | // Do something with the response 441 | // 442 | // res.refs: JSON Reference locations and details 443 | // res.resolved: The document with the appropriate JSON References resolved 444 | // res.value: The retrieved document 445 | }, function (err) { 446 | console.log(err.stack); 447 | }); 448 | ``` 449 | -------------------------------------------------------------------------------- /docs/CLI.md: -------------------------------------------------------------------------------- 1 | json-refs provides a simple CLI that provides various utilities. Below is more information on how the json-refs can be 2 | used and what to expect. 3 | 4 | ## Global Help 5 | 6 | The global help for `json-refs` can be requested by doing any of the following: 7 | 8 | * Running `json-refs` without any other arguments 9 | * Running `json-refs` with the `help` command *(Example: `json-refs help`)* 10 | * Running `json-refs` with the `-h` or `--help` flag *(Example: `json-refs --help`)* 11 | * Running `json-refs` with an unsupported comand *(Example: `json-refs unsupported`)* 12 | 13 | Here is the current global help output: 14 | 15 | ``` 16 | 17 | Usage: json-refs [options] [command] 18 | 19 | 20 | Commands: 21 | 22 | help [command] Display help information 23 | resolve [options] Prints document at location with its JSON References resolved 24 | 25 | Options: 26 | 27 | -h, --help output usage information 28 | -V, --version output the version number 29 | 30 | 31 | ``` 32 | 33 | ## Available Commands 34 | 35 | Below is the list of supported commands for `json-refs`: 36 | 37 | * `help [command]`: This will print out command-specific help or the global help if no command is provided 38 | * `resolve ` This will retrieve the document at the requested location, resolve its JSON References based 39 | on the provided arguments and print the resolved document to standard out 40 | 41 | For more details on each command, view the command-specific information below 42 | 43 | ### The `help` Command 44 | 45 | The `help` command is there to either print global help, as discussed above, or to print command-specific help. This 46 | command is self-explanatory so there is not much to discuss here. 47 | 48 | ### The `resolve` Command 49 | 50 | The `resolve` command takes a required `location` argument, which is either a local filesystem path or a remote URL, 51 | retrieves the document, resolves the requested JSON References and then prints the resolved document. This is basically 52 | a wrapper for [JsonRefs.resolveRefsAt](https://github.com/whitlockjc/json-refs/blob/master/docs/API.md#module_JsonRefs.resolveRefsAt). 53 | 54 | ### Caveats 55 | 56 | Below are a few things that are worth mentioning but cannot fit into the command help: 57 | 58 | * `json-refs` only works with JSON and YAML files at this time 59 | * The `-H|--header` option for `json-refs` requires a pattern like this: `: ` *(Notice the 60 | space after the colon, it is **required**)* 61 | * To avoid confusion, `json-refs` will fail whenever there are invalid JSON References in the document(s) processed. 62 | But if you want to skip validation and generated the resolved document with the invalid JSON References unresolved, 63 | use the `--force` flag. 64 | 65 | ### Help 66 | 67 | ``` 68 | 69 | Usage: resolve [options] 70 | 71 | Prints document at location with its JSON References resolved 72 | 73 | Options: 74 | 75 | -h, --help output usage information 76 | -f, --force Do not fail when the document has invalid JSON References 77 | -H, --header
The header to use when retrieving a remote document 78 | -I, --filter The type of JSON References to resolved 79 | -y, --yaml Output as YAML 80 | 81 | 82 | ``` 83 | 84 | ### Examples 85 | 86 | Here are a few examples: 87 | 88 | #### Basic Authentication 89 | 90 | `json-refs resolve http://somesecurehost/some/secure/path/swagger.yaml -H 'Basic: anNvbi1yZWZzOmlmIHlvdSBjYW4gcmVhZCB0aGlzLCBJIGFtIHNvcnJ5'` 91 | 92 | #### Resolve only remote references 93 | 94 | **Note:** There are two types of remote references, `relative` and `remote`. 95 | 96 | `json-refs resolve https://cdn.rawgit.com/whitlockjc/json-refs/master/test/browser/documents/test-document.yaml --filter relative --filter remote` 97 | 98 | #### Skip Validation 99 | 100 | `json-refs resolve https://cdn.rawgit.com/whitlockjc/json-refs/master/test/browser/documents/test-document.yaml --force` 101 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | json-refs is a simple library for interacting with [JSON References][json-reference-draft-spec] and 2 | [JSON Pointers][json-pointer-spec]. While the main purpose of this library is to provide JSON References features, 3 | since JSON References are a combination of `Object` structure and a `JSON Pointer`, this library also provides some 4 | features for JSON Pointers as well. 5 | 6 | Feel free to look at the API Documentation located here: 7 | https://github.com/whitlockjc/json-refs/blob/master/docs/API.md 8 | 9 | ## Installation 10 | 11 | json-refs is available for both Node.js and the browser. Installation instructions for each environment are below. 12 | 13 | ### Browser 14 | 15 | json-refs binaries for the browser are available in the `dist/` directory: 16 | 17 | * [json-refs.js](https://raw.github.com/whitlockjc/json-refs/master/dist/json-refs.js): _2,292kb_, full source source maps 18 | * [json-refs-min.js](https://raw.github.com/whitlockjc/json-refs/master/dist/json-refs-min.js): _148kb_, minified, compressed and no sourcemap 19 | 20 | Of course, these links are for the master builds so feel free to download from the release of your choice. Once you've 21 | gotten them downloaded, to use the standalone binaries, your HTML include might look like this: 22 | 23 | ``` html 24 | 25 | 26 | 27 | ``` 28 | 29 | ### Node.js 30 | 31 | Installation for Node.js applications can be done via [NPM][npm]. 32 | 33 | ``` 34 | npm install json-refs --save 35 | ``` 36 | 37 | If you plan on using the `json-refs` CLI executable, you can install json-refs globally like this: 38 | 39 | ``` 40 | npm install json-refs --global 41 | ``` 42 | 43 | After this, feel free to run `json-refs help` to see what you can do or view the CLI documentation linked above 44 | 45 | ## API Documentation 46 | 47 | The json-refs project's API documentation can be found here: https://github.com/whitlockjc/json-refs/blob/master/docs/API.md 48 | 49 | ## CLI Documentation 50 | The json-refs project's CLI documentation can be found here: https://github.com/whitlockjc/json-refs/blob/master/docs/CLI.md 51 | 52 | ## Dependencies 53 | 54 | Below is the list of projects being used by json-refs and the purpose(s) they are used for: 55 | 56 | * [graphlib][graphlib]: Used to identify circular paths _(to avoid reinventing the wheel)_ 57 | * [lodash][lodash]: JavaScript utilities _(to avoid reinventing the wheel)_ 58 | * [native-promise-only][native-promise-only]: Used to shim in [Promises][promises] support 59 | * [path-loader][path-loader]: Used to load Swagger files from the local filesystem and remote URLs 60 | 61 | ## Resolution 62 | 63 | json-refs' resolution is pretty straight forward: Find JSON Reference definitions in the source document, lookup the 64 | location being referenced and then replace the JSON Reference definition with the referenced value. During this process 65 | json-refs will also record _metadata_ that provides more information about the JSON Reference and its resolution _(or 66 | attempted resolution)_. From a performance perspective, two things must be mentioned: 67 | 68 | 1. json-refs will never process the same node in any document more than once 69 | 2. json-refs will never clone a referenced value _(JavaScript rules apply)_ 70 | 71 | ### Identifying Circulars 72 | 73 | As part of the resolution process, and the information recorded in the metadata, json-refs needs to identify the JSON 74 | Reference definitions that are circular. From a resolution perspective, if `options.resolveCirculars` is set to 75 | `false`, this information is used to avoid resolving circular JSON References. From a metadata perspective, when a JSON 76 | Reference definition is circular, it is marked as such. 77 | 78 | There are three scenarios in which a JSON Reference definition is marked as circular: 79 | 80 | **JSON Reference to Ancestor** 81 | 82 | ```yaml 83 | definitions: 84 | Person: 85 | type: object 86 | properties: 87 | family: 88 | type: array 89 | items: 90 | $ref: '#/definitions/Person' 91 | ``` 92 | 93 | Based on this example, the following are marked as circular: 94 | 95 | * `#/definitions/Person/properties/family/items` 96 | 97 | **Local-Only Circular Chain** 98 | 99 | ```yaml 100 | A: 101 | b: 102 | "$ref": "#/B" 103 | B: 104 | c: 105 | "$ref": "#/C" 106 | C: 107 | a: 108 | "$ref": "#/A" 109 | D: 110 | a: 111 | "$ref": "#/A" 112 | 113 | ``` 114 | 115 | Based on this example, the following are marked as circular: 116 | 117 | * `#/A/b` 118 | * `#/B/c` 119 | * `#/C/a` 120 | 121 | The reason `#/D/a` is **not** marked as circular is because while it references a circular, `#/D` is not itself part of 122 | the circular path that resulted in `#/A/b` being marked as circular. 123 | 124 | **Circular Chain Containing Remote Documents** 125 | 126 | `root.json` 127 | 128 | ```json 129 | { 130 | "remote": { 131 | "$ref": "./remote.json" 132 | }, 133 | "remote-with-fragment": { 134 | "$ref": "./remote.json#/definitions/Person" 135 | } 136 | } 137 | ``` 138 | 139 | `remote.json` 140 | 141 | ```json 142 | { 143 | "ancestor": { 144 | "$ref": "./root.json" 145 | }, 146 | "definitions": { 147 | "Person": { 148 | "type": "object", 149 | "properties": { 150 | "age": { 151 | "type": "integer" 152 | }, 153 | "name": { 154 | "type": "string" 155 | } 156 | } 157 | } 158 | } 159 | } 160 | ``` 161 | 162 | Based on the example above, only `#/remote/ancestor` would be marked as _circular_. _(You're right, `#/remote/ancestor` 163 | is not a JSON Reference definition location in the source document but if you look at the 164 | [JSON Reference Metadata](#json-reference-metadata) section.)_ The reason we don't mark `#/remote` as circular is 165 | because while `#/remote` is part of the circular path, not all JSON Reference definitions within `remote.json` are 166 | circular and it wouldn't make sense to stop resolution just because we point to a document that somewhere within it gets 167 | us back to where we are. So unlike local-only circulars, multi-document circulars will only mark the JSON Reference 168 | definition that points to an ancestor document that is part of the circular chain as circular. 169 | 170 | ### JSON Reference Metadata 171 | 172 | This metadata mentioned above is a map with the following structure: 173 | 174 | * **key:** A JSON Pointer relative to the root of the source document to where the resolution of a JSON Reference 175 | contributed to the resolved document 176 | * **value:** The [JsonRefs~ResolvedRefDetails][json-refs-refdetails] that contains the details of the JSON Reference 177 | definition and information about the resolution 178 | 179 | The most important part to know that might not be 100% clear is that the `key` part of the metadata is _"relative to the 180 | root of the source document."_ Instead of getting all wordy, let's use an example: 181 | 182 | `root.json` 183 | 184 | ```json 185 | { 186 | "definitions": { 187 | "Person": { 188 | "type": "object", 189 | "properties": { 190 | "address": { 191 | "$ref": "http://api.example.com/schemas/types.json#/definitions/Address" 192 | }, 193 | "age": { 194 | "$ref": "http://api.example.com/schemas/types.json#/definitions/Integer" 195 | }, 196 | "name": { 197 | "$ref": "http://api.example.com/schemas/types.json#definitions/String" 198 | } 199 | } 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | `http://api.example.com/schemas/types.json` 206 | 207 | ```json 208 | { 209 | "definitions": { 210 | "Address": { 211 | "type": "object", 212 | "properties": { 213 | "street": { 214 | "$ref": "#/definitions/String" 215 | } 216 | } 217 | }, 218 | "Integer": { 219 | "type": "integer" 220 | }, 221 | "String": { 222 | "type": "string" 223 | } 224 | }, 225 | "State": { 226 | "type": "string" 227 | } 228 | } 229 | ``` 230 | 231 | Based on the example above, the following metadata keys would be collected as part of resolving `root.json`: 232 | 233 | * `#/definitions/Person/properties/address` 234 | * `#/definitions/Person/properties/address/properties/street` 235 | * `#/definitions/Person/properties/age` 236 | * `#/definitions/Person/properties/name` 237 | 238 | The second key is the one that is most important because it shows how a a JSON Reference in a remote document rolls up 239 | to be relative to the root of the source document. Since `#/definitions/Person/properties/address` references 240 | `http://api.example.com/schemas/types.json#/definitions/Address`, all JSON Reference definitions locations beneath 241 | `#/definitions/Address` in `http://api.example.com/schemas/types.json` are joined to the JSON Reference definition 242 | location in the source document. So `#/definitions/Person/properties/address + #/properties/street` becomes 243 | `#/definitions/Person/properties/address/properties/street`. 244 | 245 | Now the metadata `value` is pretty straight forward and it includes the reference and resolution details documented in 246 | the [API documentation][json-refs-refdetails]. 247 | 248 | [graphlib]: https://github.com/cpettitt/graphlib 249 | [json-pointer-spec]: http://tools.ietf.org/html/rfc6901 250 | [json-reference-draft-spec]: http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 251 | [json-refs-refdetails]: https://github.com/whitlockjc/json-refs/blob/master/docs/API.md#module_JsonRefs..ResolvedRefDetails 252 | [lodash]: https://lodash.com 253 | [native-promise-only]: https://www.npmjs.com/package/native-promise-only 254 | [npm]: https://www.npmjs.org/ 255 | [path-loader]: https://github.com/whitlockjc/path-loader 256 | [promises]: https://www.promisejs.org/ 257 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2014 Jeremy Whitlock 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | var $ = require('gulp-load-plugins')({ 28 | rename: { 29 | 'gulp-jsdoc-to-markdown': 'jsdoc2MD' 30 | } 31 | }); 32 | var del = require('del'); 33 | var gulp = require('gulp'); 34 | var gutil = require('gulp-util'); 35 | var KarmaServer = require('karma').Server; 36 | var path = require('path'); 37 | var runSequence = require('run-sequence'); 38 | var webpack = require('webpack'); 39 | var webpackConfig = require('./webpack.config'); 40 | 41 | var runningAllTests = false; 42 | 43 | // Load promises polyfill if necessary 44 | if (typeof Promise === 'undefined') { 45 | require('native-promise-only'); 46 | } 47 | 48 | function displayCoverageReport (display) { 49 | if (display) { 50 | gulp.src([]) 51 | .pipe($.istanbul.writeReports()); 52 | } 53 | } 54 | 55 | gulp.task('clean', function (done) { 56 | del([ 57 | 'bower_components', 58 | 'coverage' 59 | ], done); 60 | }); 61 | 62 | gulp.task('docs-raw', function () { 63 | return gulp.src([ 64 | './index.js', 65 | './lib/typedefs.js' 66 | ]) 67 | .pipe($.concat('API.md')) 68 | .pipe($.jsdoc2MD({'sort-by': ['category', 'name'], 'conf': 'jsdoc.config.json'})) 69 | .pipe(gulp.dest('docs')); 70 | }); 71 | 72 | // Due to bugs in @otris/jsdoc-tsd, we need to "fix" the generated Markdown. 73 | // 74 | // * https://github.com/jsdoc2md/jsdoc-to-markdown/issues/138 75 | gulp.task('docs', ['docs-raw'], function () { 76 | return gulp.src(['docs/API.md']) 77 | .pipe($.replace('module:json-refs.UnresolvedRefDetails', 78 | '[UnresolvedRefDetails](#module_json-refs.UnresolvedRefDetails)')) 79 | .pipe(gulp.dest('docs')); 80 | }); 81 | 82 | gulp.task('dist', function (done) { 83 | webpack(webpackConfig, function (err, stats) { 84 | if (err) throw new gutil.PluginError('webpack', err); 85 | gutil.log('[webpack]', 'Bundles generated:\n' + stats.toString('minimal').split('\n').map(function (line) { 86 | return ' ' + line.replace('Child ', 'dist/').replace(':', '.js:'); 87 | }).join('\n')); 88 | done(); 89 | }); 90 | }); 91 | 92 | gulp.task('docs-ts-raw', function (done) { 93 | gulp.src([ 94 | './index.js', 95 | './lib/typedefs.js' 96 | ]) 97 | .pipe($.jsdoc3({ 98 | opts: { 99 | destination: 'index.d.ts', 100 | template: 'node_modules/@otris/jsdoc-tsd' 101 | } 102 | }, done)); 103 | }); 104 | 105 | // Due to bugs in @otris/jsdoc-tsd, we need to "fix" the generated TSD. 106 | // 107 | // * https://github.com/otris/jsdoc-tsd/issues/38 108 | // * https://github.com/otris/jsdoc-tsd/issues/39 109 | gulp.task('docs-ts', ['docs-ts-raw'], function () { 110 | return gulp.src(['index.d.ts']) 111 | .pipe($.replace('<*>', '')) 112 | .pipe($.replace('module:json-refs~', '')) 113 | .pipe($.replace('module:json-refs.', '')) 114 | .pipe($.replace('Promise.<', 'Promise<')) 115 | .pipe(gulp.dest('.')); 116 | }); 117 | 118 | gulp.task('lint', function () { 119 | return gulp.src([ 120 | 'index.js', 121 | 'lib/typedefs.js', 122 | 'test/**/*.js', 123 | '!test/browser/**/*.js', 124 | 'gulpfile.js' 125 | ]) 126 | .pipe($.eslint()) 127 | .pipe($.eslint.format('stylish')) 128 | .pipe($.eslint.failAfterError()); 129 | }); 130 | 131 | gulp.task('pre-test', function () { 132 | return gulp.src([ 133 | 'index.js', 134 | 'lib/**/*.js' 135 | ]) 136 | .pipe($.istanbul({includeUntested: true})) 137 | .pipe($.istanbul.hookRequire()); // Force `require` to return covered files 138 | }); 139 | 140 | gulp.task('test-node', ['pre-test'], function () { 141 | return gulp.src([ 142 | 'test/test-cli.js', 143 | 'test/test-json-refs.js' 144 | ]) 145 | .pipe($.mocha({ 146 | reporter: 'spec', 147 | timeout: 5000 148 | })) 149 | .on('end', function () { 150 | displayCoverageReport(!runningAllTests); 151 | }); 152 | }); 153 | 154 | gulp.task('test-browser', function () { 155 | var basePath = './test/browser/'; 156 | 157 | function cleanUp () { 158 | // Clean up just in case 159 | del.sync([ 160 | basePath + 'json-refs.js', 161 | basePath + 'json-refs-standalone.js', 162 | basePath + 'test-browser.js' 163 | ]); 164 | } 165 | 166 | 167 | return new Promise(function (resolve, reject) { 168 | cleanUp(); 169 | 170 | new KarmaServer({ 171 | configFile: path.join(__dirname, 'test/browser/karma.conf.js'), 172 | singleRun: true 173 | }, function (err) { 174 | cleanUp(); 175 | 176 | displayCoverageReport(runningAllTests); 177 | 178 | if (err) { 179 | reject(err); 180 | } else { 181 | resolve(); 182 | } 183 | }).start(); 184 | }); 185 | }); 186 | 187 | gulp.task('test', function (done) { 188 | runningAllTests = true; 189 | 190 | // Done this way to ensure that test-node runs prior to test-browser. Since both of those tasks are independent, 191 | // doing this 'The Gulp Way' isn't feasible. 192 | runSequence('test-node', 'test-browser', done); 193 | }); 194 | 195 | gulp.task('default', function (done) { 196 | // Done this way to run in series until we upgrade to Gulp 4.x+ 197 | runSequence('lint', 'test', 'dist', 'docs', 'docs-ts', done); 198 | }); 199 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Various utilities for JSON References *(http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)* and 3 | * JSON Pointers *(https://tools.ietf.org/html/rfc6901)*. 4 | */ 5 | declare module 'json-refs' { 6 | /** 7 | * Clears the internal cache of remote documents, reference details, etc. 8 | */ 9 | export function clearCache(): void; 10 | 11 | /** 12 | * Takes an array of path segments and decodes the JSON Pointer tokens in them. 13 | * @param path - The array of path segments 14 | * @returns the array of path segments with their JSON Pointer tokens decoded 15 | * @throws if the path is not an `Array` 16 | * @see 17 | */ 18 | export function decodePath(path: string[]): string[]; 19 | 20 | /** 21 | * Takes an array of path segments and encodes the special JSON Pointer characters in them. 22 | * @param path - The array of path segments 23 | * @returns the array of path segments with their JSON Pointer tokens encoded 24 | * @throws if the path is not an `Array` 25 | * @see 26 | */ 27 | export function encodePath(path: string[]): string[]; 28 | 29 | /** 30 | * Finds JSON References defined within the provided array/object. 31 | * @param obj - The structure to find JSON References within 32 | * @param options - The JsonRefs options 33 | * @returns an object whose keys are JSON Pointers 34 | * *(fragment version)* to where the JSON Reference is defined and whose values are {@link UnresolvedRefDetails}. 35 | * @throws when the input arguments fail validation or if `options.subDocPath` points to an invalid location 36 | */ 37 | export function findRefs(obj: any[] | object, options?: JsonRefsOptions): { [key: string]: (UnresolvedRefDetails|undefined) }; 38 | 39 | /** 40 | * Finds JSON References defined within the document at the provided location. 41 | * 42 | * This API is identical to {@link findRefs} except this API will retrieve a remote document and then 43 | * return the result of {@link findRefs} on the retrieved document. 44 | * @param location - The location to retrieve *(Can be relative or absolute, just make sure you look at the 45 | * {@link JsonRefsOptions|options documentation} to see how relative references are handled.)* 46 | * @param options - The JsonRefs options 47 | * @returns a promise that resolves a 48 | * {@link RetrievedRefsResults} and rejects with an `Error` when the input arguments fail validation, 49 | * when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 50 | * resource 51 | */ 52 | export function findRefsAt(location: string, options?: JsonRefsOptions): Promise; 53 | 54 | /** 55 | * Returns detailed information about the JSON Reference. 56 | * @param obj - The JSON Reference definition 57 | * @returns the detailed information 58 | */ 59 | export function getRefDetails(obj: object): UnresolvedRefDetails; 60 | 61 | /** 62 | * Returns whether the argument represents a JSON Pointer. 63 | * 64 | * A string is a JSON Pointer if the following are all true: 65 | * 66 | * * The string is of type `String` 67 | * * The string must be empty, `#` or start with a `/` or `#/` 68 | * @param ptr - The string to check 69 | * @param throwWithDetails - Whether or not to throw an `Error` with the details as to why the value 70 | * provided is invalid 71 | * @returns the result of the check 72 | * @throws when the provided value is invalid and the `throwWithDetails` argument is `true` 73 | * @see 74 | */ 75 | export function isPtr(ptr: string, throwWithDetails?: boolean): boolean; 76 | 77 | /** 78 | * Returns whether the argument represents a JSON Reference. 79 | * 80 | * An object is a JSON Reference only if the following are all true: 81 | * 82 | * * The object is of type `Object` 83 | * * The object has a `$ref` property 84 | * * The `$ref` property is a valid URI *(We do not require 100% strict URIs and will handle unescaped special 85 | * characters.)* 86 | * @param obj - The object to check 87 | * @param throwWithDetails - Whether or not to throw an `Error` with the details as to why the value 88 | * provided is invalid 89 | * @returns the result of the check 90 | * @throws when the provided value is invalid and the `throwWithDetails` argument is `true` 91 | * @see 92 | */ 93 | export function isRef(obj: object, throwWithDetails?: boolean): boolean; 94 | 95 | /** 96 | * Returns an array of path segments for the provided JSON Pointer. 97 | * @param ptr - The JSON Pointer 98 | * @returns the path segments 99 | * @throws if the provided `ptr` argument is not a JSON Pointer 100 | */ 101 | export function pathFromPtr(ptr: string): string[]; 102 | 103 | /** 104 | * Returns a JSON Pointer for the provided array of path segments. 105 | * 106 | * **Note:** If a path segment in `path` is not a `String`, it will be converted to one using `JSON.stringify`. 107 | * @param path - The array of path segments 108 | * @param hashPrefix - Whether or not create a hash-prefixed JSON Pointer 109 | * @returns the corresponding JSON Pointer 110 | * @throws if the `path` argument is not an array 111 | */ 112 | export function pathToPtr(path: string[], hashPrefix?: boolean): string; 113 | 114 | /** 115 | * Finds JSON References defined within the provided array/object and resolves them. 116 | * @param obj - The structure to find JSON References within 117 | * @param options - The JsonRefs options 118 | * @returns a promise that resolves a 119 | * {@link ResolvedRefsResults} and rejects with an `Error` when the input arguments fail validation, 120 | * when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 121 | * resource 122 | */ 123 | export function resolveRefs(obj: any[] | object, options?: JsonRefsOptions): Promise; 124 | 125 | /** 126 | * Resolves JSON References defined within the document at the provided location. 127 | * 128 | * This API is identical to {@link resolveRefs} except this API will retrieve a remote document and 129 | * then return the result of {@link resolveRefs} on the retrieved document. 130 | * @param location - The location to retrieve *(Can be relative or absolute, just make sure you look at the 131 | * {@link JsonRefsOptions|options documentation} to see how relative references are handled.)* 132 | * @param options - The JsonRefs options 133 | * @returns a promise that resolves a 134 | * {@link RetrievedResolvedRefsResults} and rejects with an `Error` when the input arguments fail 135 | * validation, when `options.subDocPath` points to an invalid location or when the location argument points to an 136 | * unloadable resource 137 | */ 138 | export function resolveRefsAt(location: string, options?: JsonRefsOptions): Promise; 139 | 140 | /** 141 | * The options used for various JsonRefs APIs. 142 | */ 143 | interface JsonRefsOptions { 144 | /** 145 | * The filter to use when gathering JSON 146 | * References *(If this value is a single string or an array of strings, the value(s) are expected to be the `type(s)` 147 | * you are interested in collecting as described in {@link getRefDetails}. If it is a function, it is 148 | * expected that the function behaves like {@link RefDetailsFilter}.)* 149 | */ 150 | filter?: string | string[] | Function; 151 | /** 152 | * Whether or not to include invalid JSON Reference details *(This will 153 | * make it so that objects that are like JSON Reference objects, as in they are an `Object` and the have a `$ref` 154 | * property, but fail validation will be included. This is very useful for when you want to know if you have invalid 155 | * JSON Reference definitions. This will not mean that APIs will process invalid JSON References but the reasons as to 156 | * why the JSON References are invalid will be included in the returned metadata.)* 157 | */ 158 | includeInvalid?: boolean; 159 | /** 160 | * The options to pass to 161 | * {@link https://github.com/whitlockjc/path-loader/blob/master/docs/API.md#module_PathLoader.load|PathLoader~load} 162 | */ 163 | loaderOptions?: object; 164 | /** 165 | * The location of the document being processed *(This property is only 166 | * useful when resolving references as it will be used to locate relative references found within the document being 167 | * resolved. If this value is relative, {@link https://github.com/whitlockjc/path-loader|path-loader} will use 168 | * `window.location.href` for the browser and `process.cwd()` for Node.js.)* 169 | */ 170 | location?: string; 171 | /** 172 | * The callback used to pre-process a JSON Reference like 173 | * object *(This is called prior to validating the JSON Reference like object and getting its details)* 174 | */ 175 | refPreProcessor?: RefPreProcessor; 176 | /** 177 | * The callback used to post-process the JSON Reference 178 | * metadata *(This is called prior filtering the references)* 179 | */ 180 | refPostProcessor?: RefPostProcessor; 181 | /** 182 | * Whether to resolve circular references 183 | */ 184 | resolveCirculars?: boolean; 185 | /** 186 | * The JSON Pointer or array of path segments to the sub document 187 | * location to search from 188 | */ 189 | subDocPath?: string | string[]; 190 | } 191 | 192 | /** 193 | * Simple function used to filter out JSON References. 194 | * @param refDetails - The JSON Reference details to test 195 | * @param path - The path to the JSON Reference 196 | * @returns whether the JSON Reference should be filtered *(out)* or not 197 | */ 198 | export type RefDetailsFilter = (refDetails: UnresolvedRefDetails, path: string[])=>boolean; 199 | 200 | /** 201 | * Simple function used to pre-process a JSON Reference like object. 202 | * @param obj - The JSON Reference like object 203 | * @param path - The path to the JSON Reference like object 204 | * @returns the processed JSON Reference like object 205 | */ 206 | export type RefPreProcessor = (obj: object, path: string[])=>object; 207 | 208 | /** 209 | * Simple function used to post-process a JSON Reference details. 210 | * @param refDetails - The JSON Reference details to test 211 | * @param path - The path to the JSON Reference 212 | * @returns the processed JSON Reference details object 213 | */ 214 | export type RefPostProcessor = (refDetails: UnresolvedRefDetails, path: string[])=>object; 215 | 216 | /** 217 | * Detailed information about resolved JSON References. 218 | */ 219 | interface ResolvedRefDetails { 220 | /** 221 | * Whether or not the JSON Reference is circular *(Will not be set if the JSON 222 | * Reference is not circular)* 223 | */ 224 | circular?: boolean; 225 | /** 226 | * The fully-qualified version of the `uri` property for 227 | * {@link UnresolvedRefDetails} but with the value being relative to the root document 228 | */ 229 | fqURI: string; 230 | /** 231 | * Whether or not the referenced value was missing or not *(Will not be set if the 232 | * referenced value is not missing)* 233 | */ 234 | missing?: boolean; 235 | /** 236 | * The referenced value *(Will not be set if the referenced value is missing)* 237 | */ 238 | value?: any; 239 | } 240 | 241 | /** 242 | * The results of resolving the JSON References of an array/object. 243 | */ 244 | interface ResolvedRefsResults { 245 | /** 246 | * An object whose keys are JSON Pointers *(fragment version)* 247 | * to where the JSON Reference is defined and whose values are {@link ResolvedRefDetails} 248 | */ 249 | refs: ResolvedRefDetails; 250 | /** 251 | * The array/object with its JSON References fully resolved 252 | */ 253 | resolved: object; 254 | } 255 | 256 | /** 257 | * An object containing the retrieved document and detailed information about its JSON References. 258 | */ 259 | interface RetrievedRefsResults { 260 | /** 261 | * The retrieved document 262 | */ 263 | value: object; 264 | } 265 | 266 | /** 267 | * An object containing the retrieved document, the document with its references resolved and detailed information 268 | * about its JSON References. 269 | */ 270 | interface RetrievedResolvedRefsResults { 271 | /** 272 | * An object whose keys are JSON Pointers *(fragment version)* 273 | * to where the JSON Reference is defined and whose values are {@link UnresolvedRefDetails} 274 | */ 275 | refs: UnresolvedRefDetails; 276 | /** 277 | * The array/object with its JSON References fully resolved 278 | */ 279 | resolved: object; 280 | /** 281 | * The retrieved document 282 | */ 283 | value: object; 284 | } 285 | 286 | /** 287 | * Detailed information about unresolved JSON References. 288 | */ 289 | interface UnresolvedRefDetails { 290 | /** 291 | * The JSON Reference definition 292 | */ 293 | def: object; 294 | /** 295 | * The error information for invalid JSON Reference definition *(Only present when the 296 | * JSON Reference definition is invalid or there was a problem retrieving a remote reference during resolution)* 297 | */ 298 | error?: string; 299 | /** 300 | * The URI portion of the JSON Reference 301 | */ 302 | uri: string; 303 | /** 304 | * Detailed information about the URI as provided by 305 | * {@link https://github.com/garycourt/uri-js|URI.parse}. 306 | */ 307 | uriDetails: object; 308 | /** 309 | * The JSON Reference type *(This value can be one of the following: `invalid`, `local`, 310 | * `relative` or `remote`.)* 311 | */ 312 | type: string; 313 | /** 314 | * The warning information *(Only present when the JSON Reference definition produces a 315 | * warning)* 316 | */ 317 | warning?: string; 318 | } 319 | 320 | } 321 | 322 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2014 Jeremy Whitlock 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | var _ = require('lodash'); 28 | var gl = require('graphlib'); 29 | var path = require('path'); 30 | var PathLoader = require('path-loader'); 31 | var qs = require('querystring'); 32 | var slash = require('slash'); 33 | var URI = require('uri-js'); 34 | 35 | var badPtrTokenRegex = /~(?:[^01]|$)/g; 36 | var remoteCache = {}; 37 | var remoteTypes = ['relative', 'remote']; 38 | var remoteUriTypes = ['absolute', 'uri']; 39 | var uriDetailsCache = {}; 40 | 41 | // Load promises polyfill if necessary 42 | /* istanbul ignore if */ 43 | if (typeof Promise === 'undefined') { 44 | require('native-promise-only'); 45 | } 46 | 47 | /* Internal Functions */ 48 | 49 | function combineQueryParams (qs1, qs2) { 50 | var combined = {}; 51 | 52 | function mergeQueryParams (obj) { 53 | _.forOwn(obj, function (val, key) { 54 | combined[key] = val; 55 | }); 56 | } 57 | 58 | mergeQueryParams(qs.parse(qs1 || '')); 59 | mergeQueryParams(qs.parse(qs2 || '')); 60 | 61 | return Object.keys(combined).length === 0 ? undefined : qs.stringify(combined); 62 | } 63 | 64 | function combineURIs (u1, u2) { 65 | // Convert Windows paths 66 | if (_.isString(u1)) { 67 | u1 = slash(u1); 68 | } 69 | 70 | if (_.isString(u2)) { 71 | u2 = slash(u2); 72 | } 73 | 74 | var u2Details = parseURI(_.isUndefined(u2) ? '' : u2); 75 | var u1Details; 76 | var combinedDetails; 77 | 78 | if (remoteUriTypes.indexOf(u2Details.reference) > -1) { 79 | combinedDetails = u2Details; 80 | } else { 81 | u1Details = _.isUndefined(u1) ? undefined : parseURI(u1); 82 | 83 | if (!_.isUndefined(u1Details)) { 84 | combinedDetails = u1Details; 85 | 86 | // Join the paths 87 | combinedDetails.path = slash(path.join(u1Details.path, u2Details.path)); 88 | 89 | // Join query parameters 90 | combinedDetails.query = combineQueryParams(u1Details.query, u2Details.query); 91 | } else { 92 | combinedDetails = u2Details; 93 | } 94 | } 95 | 96 | // Remove the fragment 97 | combinedDetails.fragment = undefined; 98 | 99 | // For relative URIs, add back the '..' since it was removed above 100 | return (remoteUriTypes.indexOf(combinedDetails.reference) === -1 && 101 | combinedDetails.path.indexOf('../') === 0 ? '../' : '') + URI.serialize(combinedDetails); 102 | } 103 | 104 | function findAncestors (obj, path) { 105 | var ancestors = []; 106 | var node; 107 | 108 | if (path.length > 0) { 109 | node = obj; 110 | 111 | path.slice(0, path.length - 1).forEach(function (seg) { 112 | if (seg in node) { 113 | node = node[seg]; 114 | 115 | ancestors.push(node); 116 | } 117 | }); 118 | } 119 | 120 | return ancestors; 121 | } 122 | 123 | function isRemote (refDetails) { 124 | return remoteTypes.indexOf(getRefType(refDetails)) > -1; 125 | } 126 | 127 | function isValid (refDetails) { 128 | return _.isUndefined(refDetails.error) && refDetails.type !== 'invalid'; 129 | } 130 | 131 | function findValue (obj, path) { 132 | var value = obj; 133 | 134 | // Using this manual approach instead of _.get since we have to decodeURI the segments 135 | path.forEach(function (seg) { 136 | if (seg in value) { 137 | value = value[seg]; 138 | } else { 139 | throw Error('JSON Pointer points to missing location: ' + pathToPtr(path)); 140 | } 141 | }); 142 | 143 | return value; 144 | } 145 | 146 | function getExtraRefKeys (ref) { 147 | return Object.keys(ref).filter(function (key) { 148 | return key !== '$ref'; 149 | }); 150 | } 151 | 152 | function getRefType (refDetails) { 153 | var type; 154 | 155 | // Convert the URI reference to one of our types 156 | switch (refDetails.uriDetails.reference) { 157 | case 'absolute': 158 | case 'uri': 159 | type = 'remote'; 160 | break; 161 | case 'same-document': 162 | type = 'local'; 163 | break; 164 | default: 165 | type = refDetails.uriDetails.reference; 166 | } 167 | 168 | return type; 169 | } 170 | 171 | function getRemoteDocument (url, options) { 172 | var cacheEntry = remoteCache[url]; 173 | var allTasks = Promise.resolve(); 174 | var loaderOptions = _.cloneDeep(options.loaderOptions || {}); 175 | 176 | if (_.isUndefined(cacheEntry)) { 177 | // If there is no content processor, default to processing the raw response as JSON 178 | if (_.isUndefined(loaderOptions.processContent)) { 179 | loaderOptions.processContent = function (res, callback) { 180 | callback(undefined, JSON.parse(res.text)); 181 | }; 182 | } 183 | 184 | // Attempt to load the resource using path-loader 185 | allTasks = PathLoader.load(decodeURI(url), loaderOptions); 186 | 187 | // Update the cache 188 | allTasks = allTasks 189 | .then(function (res) { 190 | remoteCache[url] = { 191 | value: res 192 | }; 193 | 194 | return res; 195 | }) 196 | .catch(function (err) { 197 | remoteCache[url] = { 198 | error: err 199 | }; 200 | 201 | throw err; 202 | }); 203 | } else { 204 | // Return the cached version 205 | allTasks = allTasks.then(function () { 206 | if (_.isError(cacheEntry.error)) { 207 | throw cacheEntry.error; 208 | } else { 209 | return cacheEntry.value; 210 | } 211 | }); 212 | } 213 | 214 | // Return a cloned version to avoid updating the cache 215 | allTasks = allTasks.then(function (res) { 216 | return _.cloneDeep(res); 217 | }); 218 | 219 | return allTasks; 220 | } 221 | 222 | function isRefLike (obj, throwWithDetails) { 223 | var refLike = true; 224 | 225 | try { 226 | if (!_.isPlainObject(obj)) { 227 | throw new Error('obj is not an Object'); 228 | } else if (!_.isString(obj.$ref)) { 229 | throw new Error('obj.$ref is not a String'); 230 | } 231 | } catch (err) { 232 | if (throwWithDetails) { 233 | throw err; 234 | } 235 | 236 | refLike = false; 237 | } 238 | 239 | return refLike; 240 | } 241 | 242 | function makeAbsolute (location) { 243 | if (location.indexOf('://') === -1 && !path.isAbsolute(location)) { 244 | return path.resolve(process.cwd(), location); 245 | } else { 246 | return location; 247 | } 248 | } 249 | 250 | function makeRefFilter (options) { 251 | var refFilter; 252 | var validTypes; 253 | 254 | if (_.isArray(options.filter) || _.isString(options.filter)) { 255 | validTypes = _.isString(options.filter) ? [options.filter] : options.filter; 256 | refFilter = function (refDetails) { 257 | // Check the exact type or for invalid URIs, check its original type 258 | return validTypes.indexOf(refDetails.type) > -1 || validTypes.indexOf(getRefType(refDetails)) > -1; 259 | }; 260 | } else if (_.isFunction(options.filter)) { 261 | refFilter = options.filter; 262 | } else if (_.isUndefined(options.filter)) { 263 | refFilter = function () { 264 | return true; 265 | }; 266 | } 267 | 268 | return function (refDetails, path) { 269 | return (refDetails.type !== 'invalid' || options.includeInvalid === true) && refFilter(refDetails, path); 270 | }; 271 | } 272 | 273 | function makeSubDocPath (options) { 274 | var subDocPath; 275 | 276 | if (_.isArray(options.subDocPath)) { 277 | subDocPath = options.subDocPath; 278 | } else if (_.isString(options.subDocPath)) { 279 | subDocPath = pathFromPtr(options.subDocPath); 280 | } else if (_.isUndefined(options.subDocPath)) { 281 | subDocPath = []; 282 | } 283 | 284 | return subDocPath; 285 | } 286 | 287 | function markMissing (refDetails, err) { 288 | refDetails.error = err.message; 289 | refDetails.missing = true; 290 | } 291 | 292 | function parseURI (uri) { 293 | // We decode first to avoid doubly encoding 294 | return URI.parse(uri); 295 | } 296 | 297 | function buildRefModel (document, options, metadata) { 298 | var allTasks = Promise.resolve(); 299 | var subDocPtr = pathToPtr(options.subDocPath); 300 | var absLocation = makeAbsolute(options.location); 301 | var relativeBase = path.dirname(options.location); 302 | var docDepKey = absLocation + subDocPtr; 303 | var refs; 304 | var rOptions; 305 | 306 | // Store the document in the metadata if necessary 307 | if (_.isUndefined(metadata.docs[absLocation])) { 308 | metadata.docs[absLocation] = document; 309 | } 310 | 311 | // If there are no dependencies stored for the location+subDocPath, we've never seen it before and will process it 312 | if (_.isUndefined(metadata.deps[docDepKey])) { 313 | metadata.deps[docDepKey] = {}; 314 | 315 | // Find the references based on the options 316 | refs = findRefs(document, options); 317 | 318 | // Iterate over the references and process 319 | _.forOwn(refs, function (refDetails, refPtr) { 320 | var refKey = makeAbsolute(options.location) + refPtr; 321 | var refdKey = refDetails.refdId = decodeURIComponent(makeAbsolute(isRemote(refDetails) ? 322 | combineURIs(relativeBase, refDetails.uri) : 323 | options.location) + '#' + 324 | (refDetails.uri.indexOf('#') > -1 ? 325 | refDetails.uri.split('#')[1] : 326 | '')); 327 | 328 | // Record reference metadata 329 | metadata.refs[refKey] = refDetails; 330 | 331 | // Do not process invalid references 332 | if (!isValid(refDetails)) { 333 | return; 334 | } 335 | 336 | // Record the fully-qualified URI 337 | refDetails.fqURI = refdKey; 338 | 339 | // Record dependency (relative to the document's sub-document path) 340 | metadata.deps[docDepKey][refPtr === subDocPtr ? '#' : refPtr.replace(subDocPtr + '/', '#/')] = refdKey; 341 | 342 | // Do not process directly-circular references (to an ancestor or self) 343 | if (refKey.indexOf(refdKey + '/') === 0 || refKey === refdKey) { 344 | refDetails.circular = true; 345 | 346 | return; 347 | } 348 | 349 | // Prepare the options for subsequent processDocument calls 350 | rOptions = _.cloneDeep(options); 351 | 352 | rOptions.subDocPath = _.isUndefined(refDetails.uriDetails.fragment) ? 353 | [] : 354 | pathFromPtr(decodeURIComponent(refDetails.uriDetails.fragment)); 355 | 356 | // Resolve the reference 357 | if (isRemote(refDetails)) { 358 | // Delete filter.options because all remote references should be fully resolved 359 | delete rOptions.filter; 360 | // The new location being referenced 361 | rOptions.location = refdKey.split('#')[0]; 362 | 363 | allTasks = allTasks 364 | .then(function (nMetadata, nOptions) { 365 | return function () { 366 | var rAbsLocation = makeAbsolute(nOptions.location); 367 | var rDoc = nMetadata.docs[rAbsLocation]; 368 | 369 | if (_.isUndefined(rDoc)) { 370 | // We have no cache so we must retrieve the document 371 | return getRemoteDocument(rAbsLocation, nOptions) 372 | .catch(function (err) { 373 | // Store the response in the document cache 374 | nMetadata.docs[rAbsLocation] = err; 375 | 376 | // Return the error to allow the subsequent `then` to handle both errors and successes 377 | return err; 378 | }); 379 | } else { 380 | // We have already retrieved (or attempted to) the document and should use the cached version in the 381 | // metadata since it could already be processed some. 382 | return Promise.resolve() 383 | .then(function () { 384 | return rDoc; 385 | }); 386 | } 387 | }; 388 | }(metadata, rOptions)); 389 | } else { 390 | allTasks = allTasks 391 | .then(function () { 392 | return document; 393 | }); 394 | } 395 | 396 | // Process the remote document or the referenced portion of the local document 397 | allTasks = allTasks 398 | .then(function (nMetadata, nOptions, nRefDetails) { 399 | return function (doc) { 400 | if (_.isError(doc)) { 401 | markMissing(nRefDetails, doc); 402 | } else { 403 | // Wrapped in a try/catch since findRefs throws 404 | try { 405 | return buildRefModel(doc, nOptions, nMetadata) 406 | .catch(function (err) { 407 | markMissing(nRefDetails, err); 408 | }); 409 | } catch (err) { 410 | markMissing(nRefDetails, err); 411 | } 412 | } 413 | }; 414 | }(metadata, rOptions, refDetails)); 415 | }); 416 | } 417 | 418 | return allTasks; 419 | } 420 | 421 | function setValue (obj, refPath, value) { 422 | findValue(obj, refPath.slice(0, refPath.length - 1))[refPath[refPath.length - 1]] = value; 423 | } 424 | 425 | function walk (ancestors, node, path, fn) { 426 | var processChildren = true; 427 | 428 | function walkItem (item, segment) { 429 | path.push(segment); 430 | walk(ancestors, item, path, fn); 431 | path.pop(); 432 | } 433 | 434 | // Call the iteratee 435 | if (_.isFunction(fn)) { 436 | processChildren = fn(ancestors, node, path); 437 | } 438 | 439 | // We do not process circular objects again 440 | if (ancestors.indexOf(node) === -1) { 441 | ancestors.push(node); 442 | 443 | if (processChildren !== false) { 444 | if (_.isArray(node)) { 445 | node.forEach(function (member, index) { 446 | walkItem(member, index.toString()); 447 | }); 448 | } else if (_.isObject(node)) { 449 | _.forOwn(node, function (cNode, key) { 450 | walkItem(cNode, key); 451 | }); 452 | } 453 | } 454 | 455 | ancestors.pop(); 456 | } 457 | } 458 | 459 | function validateOptions (options, obj) { 460 | var locationParts; 461 | var shouldDecode; 462 | 463 | if (_.isUndefined(options)) { 464 | // Default to an empty options object 465 | options = {}; 466 | } else { 467 | // Clone the options so we do not alter the ones passed in 468 | options = _.cloneDeep(options); 469 | } 470 | 471 | if (!_.isObject(options)) { 472 | throw new TypeError('options must be an Object'); 473 | } else if (!_.isUndefined(options.resolveCirculars) && 474 | !_.isBoolean(options.resolveCirculars)) { 475 | throw new TypeError('options.resolveCirculars must be a Boolean'); 476 | } else if (!_.isUndefined(options.filter) && 477 | !_.isArray(options.filter) && 478 | !_.isFunction(options.filter) && 479 | !_.isString(options.filter)) { 480 | throw new TypeError('options.filter must be an Array, a Function of a String'); 481 | } else if (!_.isUndefined(options.includeInvalid) && 482 | !_.isBoolean(options.includeInvalid)) { 483 | throw new TypeError('options.includeInvalid must be a Boolean'); 484 | } else if (!_.isUndefined(options.location) && 485 | !_.isString(options.location)) { 486 | throw new TypeError('options.location must be a String'); 487 | } else if (!_.isUndefined(options.refPreProcessor) && 488 | !_.isFunction(options.refPreProcessor)) { 489 | throw new TypeError('options.refPreProcessor must be a Function'); 490 | } else if (!_.isUndefined(options.refPostProcessor) && 491 | !_.isFunction(options.refPostProcessor)) { 492 | throw new TypeError('options.refPostProcessor must be a Function'); 493 | } else if (!_.isUndefined(options.subDocPath) && 494 | !_.isArray(options.subDocPath) && 495 | !isPtr(options.subDocPath)) { 496 | // If a pointer is provided, throw an error if it's not the proper type 497 | throw new TypeError('options.subDocPath must be an Array of path segments or a valid JSON Pointer'); 498 | } 499 | 500 | // Default to false for allowing circulars 501 | if (_.isUndefined(options.resolveCirculars)) { 502 | options.resolveCirculars = false; 503 | } 504 | 505 | options.filter = makeRefFilter(options); 506 | 507 | // options.location is not officially supported yet but will be when Issue 88 is complete 508 | if (_.isUndefined(options.location)) { 509 | options.location = makeAbsolute('./root.json'); 510 | } 511 | 512 | locationParts = options.location.split('#'); 513 | 514 | // If options.location contains a fragment, turn it into an options.subDocPath 515 | if (locationParts.length > 1) { 516 | options.subDocPath = '#' + locationParts[1]; 517 | } 518 | 519 | shouldDecode = decodeURI(options.location) === options.location; 520 | 521 | // Just to be safe, remove any accidental fragment as it would break things 522 | options.location = combineURIs(options.location, undefined); 523 | 524 | // If the location was not encoded, meke sure it's not when we get it back (Issue #138) 525 | if (shouldDecode) { 526 | options.location = decodeURI(options.location); 527 | } 528 | 529 | // Set the subDocPath to avoid everyone else having to compute it 530 | options.subDocPath = makeSubDocPath(options); 531 | 532 | if (!_.isUndefined(obj)) { 533 | try { 534 | findValue(obj, options.subDocPath); 535 | } catch (err) { 536 | err.message = err.message.replace('JSON Pointer', 'options.subDocPath'); 537 | 538 | throw err; 539 | } 540 | } 541 | 542 | return options; 543 | } 544 | 545 | function decodePath (path) { 546 | if (!_.isArray(path)) { 547 | throw new TypeError('path must be an array'); 548 | } 549 | 550 | return path.map(function (seg) { 551 | if (!_.isString(seg)) { 552 | seg = JSON.stringify(seg); 553 | } 554 | 555 | return seg.replace(/~1/g, '/').replace(/~0/g, '~'); 556 | }); 557 | } 558 | 559 | function encodePath (path) { 560 | if (!_.isArray(path)) { 561 | throw new TypeError('path must be an array'); 562 | } 563 | 564 | return path.map(function (seg) { 565 | if (!_.isString(seg)) { 566 | seg = JSON.stringify(seg); 567 | } 568 | 569 | return seg.replace(/~/g, '~0').replace(/\//g, '~1'); 570 | }); 571 | } 572 | 573 | function findRefs (obj, options) { 574 | var refs = {}; 575 | 576 | // Validate the provided document 577 | if (!_.isArray(obj) && !_.isObject(obj)) { 578 | throw new TypeError('obj must be an Array or an Object'); 579 | } 580 | 581 | // Validate options 582 | options = validateOptions(options, obj); 583 | 584 | // Walk the document (or sub document) and find all JSON References 585 | walk(findAncestors(obj, options.subDocPath), 586 | findValue(obj, options.subDocPath), 587 | _.cloneDeep(options.subDocPath), 588 | function (ancestors, node, path) { 589 | var processChildren = true; 590 | var refDetails; 591 | var refPtr; 592 | 593 | if (isRefLike(node)) { 594 | // Pre-process the node when necessary 595 | if (!_.isUndefined(options.refPreProcessor)) { 596 | node = options.refPreProcessor(_.cloneDeep(node), path); 597 | } 598 | 599 | refDetails = getRefDetails(node); 600 | 601 | // Post-process the reference details 602 | if (!_.isUndefined(options.refPostProcessor)) { 603 | refDetails = options.refPostProcessor(refDetails, path); 604 | } 605 | 606 | if (options.filter(refDetails, path)) { 607 | refPtr = pathToPtr(path); 608 | 609 | refs[refPtr] = refDetails; 610 | } 611 | 612 | // Whenever a JSON Reference has extra children, its children should not be processed. 613 | // See: http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3 614 | if (getExtraRefKeys(node).length > 0) { 615 | processChildren = false; 616 | } 617 | } 618 | 619 | return processChildren; 620 | }); 621 | 622 | return refs; 623 | } 624 | 625 | function findRefsAt (location, options) { 626 | var allTasks = Promise.resolve(); 627 | 628 | allTasks = allTasks 629 | .then(function () { 630 | // Validate the provided location 631 | if (!_.isString(location)) { 632 | throw new TypeError('location must be a string'); 633 | } 634 | 635 | if (_.isUndefined(options)) { 636 | options = {}; 637 | } 638 | 639 | if (_.isObject(options)) { 640 | // Add the location to the options for processing/validation 641 | options.location = location; 642 | } 643 | 644 | // Validate options 645 | options = validateOptions(options); 646 | 647 | return getRemoteDocument(options.location, options); 648 | }) 649 | .then(function (res) { 650 | var cacheEntry = _.cloneDeep(remoteCache[options.location]); 651 | var cOptions = _.cloneDeep(options); 652 | 653 | if (_.isUndefined(cacheEntry.refs)) { 654 | // Do not filter any references so the cache is complete 655 | delete cOptions.filter; 656 | delete cOptions.subDocPath; 657 | 658 | cOptions.includeInvalid = true; 659 | 660 | remoteCache[options.location].refs = findRefs(res, cOptions); 661 | } 662 | 663 | // Add the filter options back 664 | if (!_.isUndefined(options.filter)) { 665 | cOptions.filter = options.filter; 666 | } 667 | 668 | // This will use the cache so don't worry about calling it twice 669 | return { 670 | refs: findRefs(res, cOptions), 671 | value: res 672 | }; 673 | }); 674 | 675 | return allTasks; 676 | } 677 | 678 | function getRefDetails (obj) { 679 | var details = { 680 | def: obj 681 | }; 682 | var cacheKey; 683 | var extraKeys; 684 | var uriDetails; 685 | 686 | try { 687 | // This will throw so the result doesn't matter 688 | isRefLike(obj, true); 689 | 690 | cacheKey = obj.$ref; 691 | uriDetails = uriDetailsCache[cacheKey]; 692 | 693 | if (_.isUndefined(uriDetails)) { 694 | uriDetails = uriDetailsCache[cacheKey] = parseURI(cacheKey); 695 | } 696 | 697 | details.uri = cacheKey; 698 | details.uriDetails = uriDetails; 699 | 700 | if (_.isUndefined(uriDetails.error)) { 701 | details.type = getRefType(details); 702 | 703 | // Validate the JSON Pointer 704 | try { 705 | if (['#', '/'].indexOf(cacheKey[0]) > -1) { 706 | isPtr(cacheKey, true); 707 | } else if (cacheKey.indexOf('#') > -1) { 708 | isPtr(uriDetails.fragment, true); 709 | } 710 | } catch (err) { 711 | details.error = err.message; 712 | details.type = 'invalid'; 713 | } 714 | } else { 715 | details.error = details.uriDetails.error; 716 | details.type = 'invalid'; 717 | } 718 | 719 | // Identify warning 720 | extraKeys = getExtraRefKeys(obj); 721 | 722 | if (extraKeys.length > 0) { 723 | details.warning = 'Extra JSON Reference properties will be ignored: ' + extraKeys.join(', '); 724 | } 725 | } catch (err) { 726 | details.error = err.message; 727 | details.type = 'invalid'; 728 | } 729 | 730 | return details; 731 | } 732 | 733 | function isPtr (ptr, throwWithDetails) { 734 | var valid = true; 735 | var firstChar; 736 | 737 | try { 738 | if (_.isString(ptr)) { 739 | if (ptr !== '') { 740 | firstChar = ptr.charAt(0); 741 | 742 | if (['#', '/'].indexOf(firstChar) === -1) { 743 | throw new Error('ptr must start with a / or #/'); 744 | } else if (firstChar === '#' && ptr !== '#' && ptr.charAt(1) !== '/') { 745 | throw new Error('ptr must start with a / or #/'); 746 | } else if (ptr.match(badPtrTokenRegex)) { 747 | throw new Error('ptr has invalid token(s)'); 748 | } 749 | } 750 | } else { 751 | throw new Error('ptr is not a String'); 752 | } 753 | } catch (err) { 754 | if (throwWithDetails === true) { 755 | throw err; 756 | } 757 | 758 | valid = false; 759 | } 760 | 761 | return valid; 762 | } 763 | 764 | function isRef (obj, throwWithDetails) { 765 | return isRefLike(obj, throwWithDetails) && getRefDetails(obj).type !== 'invalid'; 766 | } 767 | 768 | function pathFromPtr (ptr) { 769 | try { 770 | isPtr(ptr, true); 771 | } catch (err) { 772 | throw new Error('ptr must be a JSON Pointer: ' + err.message); 773 | } 774 | 775 | var segments = ptr.split('/'); 776 | 777 | // Remove the first segment 778 | segments.shift(); 779 | 780 | return decodePath(segments); 781 | } 782 | 783 | function pathToPtr (path, hashPrefix) { 784 | if (!_.isArray(path)) { 785 | throw new Error('path must be an Array'); 786 | } 787 | 788 | // Encode each segment and return 789 | return (hashPrefix !== false ? '#' : '') + (path.length > 0 ? '/' : '') + encodePath(path).join('/'); 790 | } 791 | 792 | function resolveRefs (obj, options) { 793 | var allTasks = Promise.resolve(); 794 | 795 | allTasks = allTasks 796 | .then(function () { 797 | // Validate the provided document 798 | if (!_.isArray(obj) && !_.isObject(obj)) { 799 | throw new TypeError('obj must be an Array or an Object'); 800 | } 801 | 802 | // Validate options 803 | options = validateOptions(options, obj); 804 | 805 | // Clone the input so we do not alter it 806 | obj = _.cloneDeep(obj); 807 | }) 808 | .then(function () { 809 | var metadata = { 810 | deps: {}, // To avoid processing the same refernece twice, and for circular reference identification 811 | docs: {}, // Cache to avoid processing the same document more than once 812 | refs: {} // Reference locations and their metadata 813 | }; 814 | 815 | return buildRefModel(obj, options, metadata) 816 | .then(function () { 817 | return metadata; 818 | }); 819 | }) 820 | .then(function (results) { 821 | var allRefs = {}; 822 | var circularPaths = []; 823 | var circulars = []; 824 | var depGraph = new gl.Graph(); 825 | var fullLocation = makeAbsolute(options.location); 826 | var refsRoot = fullLocation + pathToPtr(options.subDocPath); 827 | var relativeBase = path.dirname(fullLocation); 828 | 829 | // Identify circulars 830 | 831 | // Add nodes first 832 | Object.keys(results.deps).forEach(function (node) { 833 | depGraph.setNode(node); 834 | }); 835 | 836 | // Add edges 837 | _.forOwn(results.deps, function (props, node) { 838 | _.forOwn(props, function (dep) { 839 | depGraph.setEdge(node, dep); 840 | }); 841 | }); 842 | 843 | circularPaths = gl.alg.findCycles(depGraph); 844 | 845 | // Create a unique list of circulars 846 | circularPaths.forEach(function (path) { 847 | path.forEach(function (seg) { 848 | if (circulars.indexOf(seg) === -1) { 849 | circulars.push(seg); 850 | } 851 | }); 852 | }); 853 | 854 | // Identify circulars 855 | _.forOwn(results.deps, function (props, node) { 856 | _.forOwn(props, function (dep, prop) { 857 | var isCircular = false; 858 | var refPtr = node + prop.slice(1); 859 | var refDetails = results.refs[node + prop.slice(1)]; 860 | var remote = isRemote(refDetails); 861 | var pathIndex; 862 | 863 | if (circulars.indexOf(dep) > -1) { 864 | // Figure out if the circular is part of a circular chain or just a reference to a circular 865 | circularPaths.forEach(function (path) { 866 | // Short circuit 867 | if (isCircular) { 868 | return; 869 | } 870 | 871 | pathIndex = path.indexOf(dep); 872 | 873 | if (pathIndex > -1) { 874 | // Check each path segment to see if the reference location is beneath one of its segments 875 | path.forEach(function (seg) { 876 | // Short circuit 877 | if (isCircular) { 878 | return; 879 | } 880 | 881 | if (refPtr.indexOf(seg + '/') === 0) { 882 | // If the reference is local, mark it as circular but if it's a remote reference, only mark it 883 | // circular if the matching path is the last path segment or its match is not to a document root 884 | if (!remote || pathIndex === path.length - 1 || dep[dep.length - 1] !== '#') { 885 | isCircular = true; 886 | } 887 | } 888 | }); 889 | } 890 | }); 891 | } 892 | 893 | if (isCircular) { 894 | // Update all references and reference details 895 | refDetails.circular = true; 896 | } 897 | }); 898 | }); 899 | 900 | // Resolve the references in reverse order since the current order is top-down 901 | _.forOwn(Object.keys(results.deps).reverse(), function (parentPtr) { 902 | var deps = results.deps[parentPtr]; 903 | var pPtrParts = parentPtr.split('#'); 904 | var pDocument = results.docs[pPtrParts[0]]; 905 | var pPtrPath = pathFromPtr(pPtrParts[1]); 906 | 907 | _.forOwn(deps, function (dep, prop) { 908 | var depParts = splitFragment(dep); 909 | var dDocument = results.docs[depParts[0]]; 910 | var dPtrPath = pPtrPath.concat(pathFromPtr(prop)); 911 | var refDetails = results.refs[pPtrParts[0] + pathToPtr(dPtrPath)]; 912 | 913 | // Resolve reference if valid 914 | if (_.isUndefined(refDetails.error) && _.isUndefined(refDetails.missing)) { 915 | if (!options.resolveCirculars && refDetails.circular) { 916 | refDetails.value = _.cloneDeep(refDetails.def); 917 | } else { 918 | try { 919 | refDetails.value = findValue(dDocument, pathFromPtr(depParts[1])); 920 | } catch (err) { 921 | markMissing(refDetails, err); 922 | 923 | return; 924 | } 925 | 926 | // If the reference is at the root of the document, replace the document in the cache. Otherwise, replace 927 | // the value in the appropriate location in the document cache. 928 | if (pPtrParts[1] === '' && prop === '#') { 929 | results.docs[pPtrParts[0]] = refDetails.value; 930 | } else { 931 | setValue(pDocument, dPtrPath, refDetails.value); 932 | } 933 | } 934 | } 935 | }); 936 | }); 937 | 938 | function walkRefs (root, refPtr, refPath) { 939 | var refPtrParts = refPtr.split('#'); 940 | var refDetails = results.refs[refPtr]; 941 | var refDeps; 942 | 943 | // Record the reference (relative to the root document unless the reference is in the root document) 944 | allRefs[refPtrParts[0] === options.location ? 945 | '#' + refPtrParts[1] : 946 | pathToPtr(options.subDocPath.concat(refPath))] = refDetails; 947 | 948 | // Do not walk invalid references 949 | if (refDetails.circular || !isValid(refDetails)) { 950 | // Sanitize errors 951 | if (!refDetails.circular && refDetails.error) { 952 | // The way we use findRefs now results in an error that doesn't match the expectation 953 | refDetails.error = refDetails.error.replace('options.subDocPath', 'JSON Pointer'); 954 | 955 | // Update the error to use the appropriate JSON Pointer 956 | if (refDetails.error.indexOf('#') > -1) { 957 | refDetails.error = refDetails.error.replace(refDetails.uri.substr(refDetails.uri.indexOf('#')), 958 | refDetails.uri); 959 | } 960 | 961 | // Report errors opening files as JSON Pointer errors 962 | if (refDetails.error.indexOf('ENOENT:') === 0 || refDetails.error.indexOf('Not Found') === 0) { 963 | refDetails.error = 'JSON Pointer points to missing location: ' + refDetails.uri; 964 | } 965 | } 966 | 967 | return; 968 | } 969 | 970 | refDeps = results.deps[refDetails.refdId]; 971 | 972 | if (refDetails.refdId.indexOf(root) !== 0) { 973 | Object.keys(refDeps).forEach(function (prop) { 974 | walkRefs(refDetails.refdId, refDetails.refdId + prop.substr(1), refPath.concat(pathFromPtr(prop))); 975 | }); 976 | } 977 | } 978 | 979 | // For performance reasons, we only process a document (or sub document) and each reference once ever. This means 980 | // that if we want to provide the full picture as to what paths in the resolved document were created as a result 981 | // of a reference, we have to take our fully-qualified reference locations and expand them to be all local based 982 | // on the original document. 983 | Object.keys(results.refs).forEach(function (refPtr) { 984 | var refDetails = results.refs[refPtr]; 985 | var fqURISegments; 986 | var uriSegments; 987 | 988 | // Make all fully-qualified reference URIs relative to the document root (if necessary). This step is done here 989 | // for performance reasons instead of below when the official sanitization process runs. 990 | if (refDetails.type !== 'invalid') { 991 | // Remove the trailing hash from document root references if they weren't in the original URI 992 | if (refDetails.fqURI[refDetails.fqURI.length - 1] === '#' && 993 | refDetails.uri[refDetails.uri.length - 1] !== '#') { 994 | refDetails.fqURI = refDetails.fqURI.substr(0, refDetails.fqURI.length - 1); 995 | } 996 | 997 | fqURISegments = refDetails.fqURI.split('/'); 998 | uriSegments = refDetails.uri.split('/'); 999 | 1000 | // The fully-qualified URI is unencoded so to keep the original formatting of the URI (encoded vs. unencoded), 1001 | // we need to replace each URI segment in reverse order. 1002 | _.times(uriSegments.length - 1, function (time) { 1003 | var nSeg = uriSegments[uriSegments.length - time - 1]; 1004 | var pSeg = uriSegments[uriSegments.length - time]; 1005 | var fqSegIndex = fqURISegments.length - time - 1; 1006 | 1007 | if (nSeg === '.' || nSeg === '..' || pSeg === '..') { 1008 | return; 1009 | } 1010 | 1011 | fqURISegments[fqSegIndex] = nSeg; 1012 | }); 1013 | 1014 | refDetails.fqURI = fqURISegments.join('/'); 1015 | 1016 | // Make the fully-qualified URIs relative to the document root 1017 | if (refDetails.fqURI.indexOf(fullLocation) === 0) { 1018 | refDetails.fqURI = refDetails.fqURI.replace(fullLocation, ''); 1019 | } else if (refDetails.fqURI.indexOf(relativeBase) === 0) { 1020 | refDetails.fqURI = refDetails.fqURI.replace(relativeBase, ''); 1021 | } 1022 | 1023 | if (refDetails.fqURI[0] === '/') { 1024 | refDetails.fqURI = '.' + refDetails.fqURI; 1025 | } 1026 | } 1027 | 1028 | // We only want to process references found at or beneath the provided document and sub-document path 1029 | if (refPtr.indexOf(refsRoot) !== 0) { 1030 | return; 1031 | } 1032 | 1033 | walkRefs(refsRoot, refPtr, pathFromPtr(refPtr.substr(refsRoot.length))); 1034 | }); 1035 | 1036 | // Sanitize the reference details 1037 | _.forOwn(allRefs, function (refDetails, refPtr) { 1038 | // Delete the reference id used for dependency tracking and circular identification 1039 | delete refDetails.refdId; 1040 | 1041 | // For locally-circular references, update the $ref to be fully qualified (Issue #175) 1042 | if (refDetails.circular && refDetails.type === 'local') { 1043 | refDetails.value.$ref = refDetails.fqURI; 1044 | 1045 | setValue(results.docs[fullLocation], pathFromPtr(refPtr), refDetails.value); 1046 | } 1047 | 1048 | // To avoid the error message being URI encoded/decoded by mistake, replace the current JSON Pointer with the 1049 | // value in the JSON Reference definition. 1050 | if (refDetails.missing) { 1051 | refDetails.error = refDetails.error.split(': ')[0] + ': ' + refDetails.def.$ref; 1052 | } 1053 | }); 1054 | 1055 | return { 1056 | refs: allRefs, 1057 | resolved: results.docs[fullLocation] 1058 | }; 1059 | }); 1060 | 1061 | return allTasks; 1062 | } 1063 | 1064 | function resolveRefsAt (location, options) { 1065 | var allTasks = Promise.resolve(); 1066 | 1067 | allTasks = allTasks 1068 | .then(function () { 1069 | // Validate the provided location 1070 | if (!_.isString(location)) { 1071 | throw new TypeError('location must be a string'); 1072 | } 1073 | 1074 | if (_.isUndefined(options)) { 1075 | options = {}; 1076 | } 1077 | 1078 | if (_.isObject(options)) { 1079 | // Add the location to the options for processing/validation 1080 | options.location = location; 1081 | } 1082 | 1083 | // Validate options 1084 | options = validateOptions(options); 1085 | 1086 | return getRemoteDocument(options.location, options); 1087 | }) 1088 | .then(function (res) { 1089 | return resolveRefs(res, options) 1090 | .then(function (res2) { 1091 | return { 1092 | refs: res2.refs, 1093 | resolved: res2.resolved, 1094 | value: res 1095 | }; 1096 | }); 1097 | }); 1098 | 1099 | return allTasks; 1100 | } 1101 | 1102 | // splits a fragment from a URI using the first hash found 1103 | function splitFragment(uri) { 1104 | var hash = uri.indexOf('#'); 1105 | if (hash < 0) { 1106 | return [uri]; 1107 | } 1108 | var parts = []; 1109 | parts.push(uri.substring(0, hash)); 1110 | parts.push(uri.substring(hash + 1)); 1111 | return parts; 1112 | } 1113 | 1114 | /** 1115 | * Various utilities for JSON References *(http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)* and 1116 | * JSON Pointers *(https://tools.ietf.org/html/rfc6901)*. 1117 | * 1118 | * @module json-refs 1119 | */ 1120 | 1121 | /** 1122 | * A number of functions exported below are used within the exported functions. Typically, I would use a function 1123 | * declaration _(with documenation)_ above and then just export a reference to the function but due to a bug in JSDoc 1124 | * (https://github.com/jsdoc3/jsdoc/issues/679), this breaks the generated API documentation and TypeScript 1125 | * declarations. So that's why each `module.exports` below basically just wraps a call to the function declaration. 1126 | */ 1127 | 1128 | /** 1129 | * Clears the internal cache of remote documents, reference details, etc. 1130 | */ 1131 | module.exports.clearCache = function () { 1132 | remoteCache = {}; 1133 | }; 1134 | 1135 | /** 1136 | * Takes an array of path segments and decodes the JSON Pointer tokens in them. 1137 | * 1138 | * @param {string[]} path - The array of path segments 1139 | * 1140 | * @returns {string[]} the array of path segments with their JSON Pointer tokens decoded 1141 | * 1142 | * @throws {Error} if the path is not an `Array` 1143 | * 1144 | * @see {@link https://tools.ietf.org/html/rfc6901#section-3} 1145 | */ 1146 | module.exports.decodePath = function (path) { 1147 | return decodePath(path); 1148 | }; 1149 | 1150 | /** 1151 | * Takes an array of path segments and encodes the special JSON Pointer characters in them. 1152 | * 1153 | * @param {string[]} path - The array of path segments 1154 | * 1155 | * @returns {string[]} the array of path segments with their JSON Pointer tokens encoded 1156 | * 1157 | * @throws {Error} if the path is not an `Array` 1158 | * 1159 | * @see {@link https://tools.ietf.org/html/rfc6901#section-3} 1160 | */ 1161 | module.exports.encodePath = function (path) { 1162 | return encodePath(path); 1163 | }; 1164 | 1165 | /** 1166 | * Finds JSON References defined within the provided array/object. 1167 | * 1168 | * @param {array|object} obj - The structure to find JSON References within 1169 | * @param {module:json-refs.JsonRefsOptions} [options] - The JsonRefs options 1170 | * 1171 | * @returns {Object.} an object whose keys are JSON Pointers 1172 | * *(fragment version)* to where the JSON Reference is defined and whose values are {@link UnresolvedRefDetails}. 1173 | * 1174 | * @throws {Error} when the input arguments fail validation or if `options.subDocPath` points to an invalid location 1175 | * 1176 | * @example 1177 | * // Finding all valid references 1178 | * var allRefs = JsonRefs.findRefs(obj); 1179 | * // Finding all remote references 1180 | * var remoteRefs = JsonRefs.findRefs(obj, {filter: ['relative', 'remote']}); 1181 | * // Finding all invalid references 1182 | * var invalidRefs = JsonRefs.findRefs(obj, {filter: 'invalid', includeInvalid: true}); 1183 | */ 1184 | module.exports.findRefs = function (obj, options) { 1185 | return findRefs(obj, options); 1186 | }; 1187 | 1188 | /** 1189 | * Finds JSON References defined within the document at the provided location. 1190 | * 1191 | * This API is identical to {@link findRefs} except this API will retrieve a remote document and then 1192 | * return the result of {@link findRefs} on the retrieved document. 1193 | * 1194 | * @param {string} location - The location to retrieve *(Can be relative or absolute, just make sure you look at the 1195 | * {@link module:json-refs.JsonRefsOptions|options documentation} to see how relative references are handled.)* 1196 | * @param {module:json-refs.JsonRefsOptions} [options] - The JsonRefs options 1197 | * 1198 | * @returns {Promise} a promise that resolves a 1199 | * {@link module:json-refs.RetrievedRefsResults} and rejects with an `Error` when the input arguments fail validation, 1200 | * when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 1201 | * resource 1202 | * 1203 | * @example 1204 | * // Example that only resolves references within a sub document 1205 | * JsonRefs.findRefsAt('http://petstore.swagger.io/v2/swagger.json', { 1206 | * subDocPath: '#/definitions' 1207 | * }) 1208 | * .then(function (res) { 1209 | * // Do something with the response 1210 | * // 1211 | * // res.refs: JSON Reference locations and details 1212 | * // res.value: The retrieved document 1213 | * }, function (err) { 1214 | * console.log(err.stack); 1215 | * }); 1216 | */ 1217 | module.exports.findRefsAt = function (location, options) { 1218 | return findRefsAt(location, options); 1219 | }; 1220 | 1221 | /** 1222 | * Returns detailed information about the JSON Reference. 1223 | * 1224 | * @param {object} obj - The JSON Reference definition 1225 | * 1226 | * @returns {module:json-refs.UnresolvedRefDetails} the detailed information 1227 | */ 1228 | module.exports.getRefDetails = function (obj) { 1229 | return getRefDetails(obj); 1230 | }; 1231 | 1232 | /** 1233 | * Returns whether the argument represents a JSON Pointer. 1234 | * 1235 | * A string is a JSON Pointer if the following are all true: 1236 | * 1237 | * * The string is of type `String` 1238 | * * The string must be empty, `#` or start with a `/` or `#/` 1239 | * 1240 | * @param {string} ptr - The string to check 1241 | * @param {boolean} [throwWithDetails=false] - Whether or not to throw an `Error` with the details as to why the value 1242 | * provided is invalid 1243 | * 1244 | * @returns {boolean} the result of the check 1245 | * 1246 | * @throws {error} when the provided value is invalid and the `throwWithDetails` argument is `true` 1247 | * 1248 | * @see {@link https://tools.ietf.org/html/rfc6901#section-3} 1249 | * 1250 | * @example 1251 | * // Separating the different ways to invoke isPtr for demonstration purposes 1252 | * if (isPtr(str)) { 1253 | * // Handle a valid JSON Pointer 1254 | * } else { 1255 | * // Get the reason as to why the value is not a JSON Pointer so you can fix/report it 1256 | * try { 1257 | * isPtr(str, true); 1258 | * } catch (err) { 1259 | * // The error message contains the details as to why the provided value is not a JSON Pointer 1260 | * } 1261 | * } 1262 | */ 1263 | module.exports.isPtr = function (ptr, throwWithDetails) { 1264 | return isPtr(ptr, throwWithDetails); 1265 | }; 1266 | 1267 | /** 1268 | * Returns whether the argument represents a JSON Reference. 1269 | * 1270 | * An object is a JSON Reference only if the following are all true: 1271 | * 1272 | * * The object is of type `Object` 1273 | * * The object has a `$ref` property 1274 | * * The `$ref` property is a valid URI *(We do not require 100% strict URIs and will handle unescaped special 1275 | * characters.)* 1276 | * 1277 | * @param {object} obj - The object to check 1278 | * @param {boolean} [throwWithDetails=false] - Whether or not to throw an `Error` with the details as to why the value 1279 | * provided is invalid 1280 | * 1281 | * @returns {boolean} the result of the check 1282 | * 1283 | * @throws {error} when the provided value is invalid and the `throwWithDetails` argument is `true` 1284 | * 1285 | * @see {@link http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3} 1286 | * 1287 | * @example 1288 | * // Separating the different ways to invoke isRef for demonstration purposes 1289 | * if (isRef(obj)) { 1290 | * // Handle a valid JSON Reference 1291 | * } else { 1292 | * // Get the reason as to why the value is not a JSON Reference so you can fix/report it 1293 | * try { 1294 | * isRef(str, true); 1295 | * } catch (err) { 1296 | * // The error message contains the details as to why the provided value is not a JSON Reference 1297 | * } 1298 | * } 1299 | */ 1300 | module.exports.isRef = function (obj, throwWithDetails) { 1301 | return isRef(obj, throwWithDetails); 1302 | }; 1303 | 1304 | /** 1305 | * Returns an array of path segments for the provided JSON Pointer. 1306 | * 1307 | * @param {string} ptr - The JSON Pointer 1308 | * 1309 | * @returns {string[]} the path segments 1310 | * 1311 | * @throws {Error} if the provided `ptr` argument is not a JSON Pointer 1312 | */ 1313 | module.exports.pathFromPtr = function (ptr) { 1314 | return pathFromPtr(ptr); 1315 | }; 1316 | 1317 | /** 1318 | * Returns a JSON Pointer for the provided array of path segments. 1319 | * 1320 | * **Note:** If a path segment in `path` is not a `String`, it will be converted to one using `JSON.stringify`. 1321 | * 1322 | * @param {string[]} path - The array of path segments 1323 | * @param {boolean} [hashPrefix=true] - Whether or not create a hash-prefixed JSON Pointer 1324 | * 1325 | * @returns {string} the corresponding JSON Pointer 1326 | * 1327 | * @throws {Error} if the `path` argument is not an array 1328 | */ 1329 | module.exports.pathToPtr = function (path, hashPrefix) { 1330 | return pathToPtr(path, hashPrefix); 1331 | }; 1332 | 1333 | /** 1334 | * Finds JSON References defined within the provided array/object and resolves them. 1335 | * 1336 | * @param {array|object} obj - The structure to find JSON References within 1337 | * @param {module:json-refs.JsonRefsOptions} [options] - The JsonRefs options 1338 | * 1339 | * @returns {Promise} a promise that resolves a 1340 | * {@link module:json-refs.ResolvedRefsResults} and rejects with an `Error` when the input arguments fail validation, 1341 | * when `options.subDocPath` points to an invalid location or when the location argument points to an unloadable 1342 | * resource 1343 | * 1344 | * @example 1345 | * // Example that only resolves relative and remote references 1346 | * JsonRefs.resolveRefs(swaggerObj, { 1347 | * filter: ['relative', 'remote'] 1348 | * }) 1349 | * .then(function (res) { 1350 | * // Do something with the response 1351 | * // 1352 | * // res.refs: JSON Reference locations and details 1353 | * // res.resolved: The document with the appropriate JSON References resolved 1354 | * }, function (err) { 1355 | * console.log(err.stack); 1356 | * }); 1357 | */ 1358 | module.exports.resolveRefs = function (obj, options) { 1359 | return resolveRefs(obj, options); 1360 | }; 1361 | 1362 | /** 1363 | * Resolves JSON References defined within the document at the provided location. 1364 | * 1365 | * This API is identical to {@link module:json-refs.resolveRefs} except this API will retrieve a remote document and 1366 | * then return the result of {@link module:json-refs.resolveRefs} on the retrieved document. 1367 | * 1368 | * @param {string} location - The location to retrieve *(Can be relative or absolute, just make sure you look at the 1369 | * {@link module:json-refs.JsonRefsOptions|options documentation} to see how relative references are handled.)* 1370 | * @param {module:json-refs.JsonRefsOptions} [options] - The JsonRefs options 1371 | * 1372 | * @returns {Promise} a promise that resolves a 1373 | * {@link module:json-refs.RetrievedResolvedRefsResults} and rejects with an `Error` when the input arguments fail 1374 | * validation, when `options.subDocPath` points to an invalid location or when the location argument points to an 1375 | * unloadable resource 1376 | * 1377 | * @example 1378 | * // Example that loads a JSON document (No options.loaderOptions.processContent required) and resolves all references 1379 | * JsonRefs.resolveRefsAt('./swagger.json') 1380 | * .then(function (res) { 1381 | * // Do something with the response 1382 | * // 1383 | * // res.refs: JSON Reference locations and details 1384 | * // res.resolved: The document with the appropriate JSON References resolved 1385 | * // res.value: The retrieved document 1386 | * }, function (err) { 1387 | * console.log(err.stack); 1388 | * }); 1389 | */ 1390 | module.exports.resolveRefsAt = function (location, options) { 1391 | return resolveRefsAt(location, options); 1392 | }; 1393 | -------------------------------------------------------------------------------- /jsdoc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | "recurseDepth": 10, 4 | "source": { 5 | "includePattern": ".+\\.js(doc|x)?$" 6 | }, 7 | "sourceType": "module", 8 | "tags": { 9 | "allowUnknownTags": true, 10 | "dictionaries": ["jsdoc","closure"] 11 | }, 12 | "templates": { 13 | "cleverLinks": false, 14 | "monospaceLinks": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/typedefs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all type definitions that are purely for documentation purposes. 3 | */ 4 | 5 | /** 6 | * The options used for various JsonRefs APIs. 7 | * 8 | * @typedef {object} JsonRefsOptions 9 | * 10 | * @property {string|string[]|function} [filter=function () {return true;}] - The filter to use when gathering JSON 11 | * References *(If this value is a single string or an array of strings, the value(s) are expected to be the `type(s)` 12 | * you are interested in collecting as described in {@link module:json-refs.getRefDetails}. If it is a function, it is 13 | * expected that the function behaves like {@link module:json-refs.RefDetailsFilter}.)* 14 | * @property {boolean} [includeInvalid=false] - Whether or not to include invalid JSON Reference details *(This will 15 | * make it so that objects that are like JSON Reference objects, as in they are an `Object` and the have a `$ref` 16 | * property, but fail validation will be included. This is very useful for when you want to know if you have invalid 17 | * JSON Reference definitions. This will not mean that APIs will process invalid JSON References but the reasons as to 18 | * why the JSON References are invalid will be included in the returned metadata.)* 19 | * @property {object} [loaderOptions] - The options to pass to 20 | * {@link https://github.com/whitlockjc/path-loader/blob/master/docs/API.md#module_PathLoader.load|PathLoader~load} 21 | * @property {string} [location=root.json] - The location of the document being processed *(This property is only 22 | * useful when resolving references as it will be used to locate relative references found within the document being 23 | * resolved. If this value is relative, {@link https://github.com/whitlockjc/path-loader|path-loader} will use 24 | * `window.location.href` for the browser and `process.cwd()` for Node.js.)* 25 | * @property {module:json-refs.RefPreProcessor} [refPreProcessor] - The callback used to pre-process a JSON Reference like 26 | * object *(This is called prior to validating the JSON Reference like object and getting its details)* 27 | * @property {module:json-refs.RefPostProcessor} [refPostProcessor] - The callback used to post-process the JSON Reference 28 | * metadata *(This is called prior filtering the references)* 29 | * @property {boolean} [resolveCirculars=false] - Whether to resolve circular references 30 | * @property {string|string[]} [subDocPath=[]] - The JSON Pointer or array of path segments to the sub document 31 | * location to search from 32 | * 33 | * @memberof module:json-refs 34 | */ 35 | 36 | /** 37 | * Simple function used to filter out JSON References. 38 | * 39 | * @typedef {function} RefDetailsFilter 40 | * 41 | * @param {module:json-refs.UnresolvedRefDetails} refDetails - The JSON Reference details to test 42 | * @param {string[]} path - The path to the JSON Reference 43 | * 44 | * @returns {boolean} whether the JSON Reference should be filtered *(out)* or not 45 | * 46 | * @memberof module:json-refs 47 | */ 48 | 49 | /** 50 | * Simple function used to pre-process a JSON Reference like object. 51 | * 52 | * @typedef {function} RefPreProcessor 53 | * 54 | * @param {object} obj - The JSON Reference like object 55 | * @param {string[]} path - The path to the JSON Reference like object 56 | * 57 | * @returns {object} the processed JSON Reference like object 58 | * 59 | * @memberof module:json-refs 60 | */ 61 | 62 | /** 63 | * Simple function used to post-process a JSON Reference details. 64 | * 65 | * @typedef {function} RefPostProcessor 66 | * 67 | * @param {module:json-refs.UnresolvedRefDetails} refDetails - The JSON Reference details to test 68 | * @param {string[]} path - The path to the JSON Reference 69 | * 70 | * @returns {object} the processed JSON Reference details object 71 | * 72 | * @memberof module:json-refs 73 | */ 74 | 75 | /** 76 | * Detailed information about resolved JSON References. 77 | * 78 | * @typedef {module:json-refs.UnresolvedRefDetails} ResolvedRefDetails 79 | * 80 | * @property {boolean} [circular] - Whether or not the JSON Reference is circular *(Will not be set if the JSON 81 | * Reference is not circular)* 82 | * @property {string} fqURI - The fully-qualified version of the `uri` property for 83 | * {@link module:json-refs.UnresolvedRefDetails} but with the value being relative to the root document 84 | * @property {boolean} [missing] - Whether or not the referenced value was missing or not *(Will not be set if the 85 | * referenced value is not missing)* 86 | * @property {*} [value] - The referenced value *(Will not be set if the referenced value is missing)* 87 | * 88 | * @memberof module:json-refs 89 | */ 90 | 91 | /** 92 | * The results of resolving the JSON References of an array/object. 93 | * 94 | * @typedef {object} ResolvedRefsResults 95 | * 96 | * @property {module:json-refs.ResolvedRefDetails} refs - An object whose keys are JSON Pointers *(fragment version)* 97 | * to where the JSON Reference is defined and whose values are {@link module:json-refs.ResolvedRefDetails} 98 | * @property {object} resolved - The array/object with its JSON References fully resolved 99 | * 100 | * @memberof module:json-refs 101 | */ 102 | 103 | /** 104 | * An object containing the retrieved document and detailed information about its JSON References. 105 | * 106 | * @typedef {module:json-refs.ResolvedRefsResults} RetrievedRefsResults 107 | * 108 | * @property {object} value - The retrieved document 109 | * 110 | * @memberof module:json-refs 111 | */ 112 | 113 | /** 114 | * An object containing the retrieved document, the document with its references resolved and detailed information 115 | * about its JSON References. 116 | * 117 | * @typedef {object} RetrievedResolvedRefsResults 118 | * 119 | * @property {module:json-refs.UnresolvedRefDetails} refs - An object whose keys are JSON Pointers *(fragment version)* 120 | * to where the JSON Reference is defined and whose values are {@link module:json-refs.UnresolvedRefDetails} 121 | * @property {object} resolved - The array/object with its JSON References fully resolved 122 | * @property {object} value - The retrieved document 123 | * 124 | * @memberof module:json-refs 125 | */ 126 | 127 | /** 128 | * Detailed information about unresolved JSON References. 129 | * 130 | * @typedef {object} UnresolvedRefDetails 131 | * 132 | * @property {object} def - The JSON Reference definition 133 | * @property {string} [error] - The error information for invalid JSON Reference definition *(Only present when the 134 | * JSON Reference definition is invalid or there was a problem retrieving a remote reference during resolution)* 135 | * @property {string} uri - The URI portion of the JSON Reference 136 | * @property {object} uriDetails - Detailed information about the URI as provided by 137 | * {@link https://github.com/garycourt/uri-js|URI.parse}. 138 | * @property {string} type - The JSON Reference type *(This value can be one of the following: `invalid`, `local`, 139 | * `relative` or `remote`.)* 140 | * @property {string} [warning] - The warning information *(Only present when the JSON Reference definition produces a 141 | * warning)* 142 | * 143 | * @memberof module:json-refs 144 | */ 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-refs", 3 | "version": "3.0.15", 4 | "description": "Various utilities for JSON References (http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03).", 5 | "main": "index.js", 6 | "scripts": { 7 | "gulp": "gulp", 8 | "test": "gulp test" 9 | }, 10 | "author": { 11 | "name": "Jeremy Whitlock", 12 | "email": "jwhitlock@apache.org", 13 | "url": "https://github.com/whitlockjc" 14 | }, 15 | "contributors": [], 16 | "engines": { 17 | "node": ">=0.8" 18 | }, 19 | "bin": { 20 | "json-refs": "./bin/json-refs" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/whitlockjc/json-refs/issues" 24 | }, 25 | "files": [ 26 | "bin", 27 | "dist", 28 | "index.d.ts", 29 | "index.js" 30 | ], 31 | "types": "./index.d.ts", 32 | "homepage": "https://github.com/whitlockjc/json-refs", 33 | "license": "MIT", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/whitlockjc/json-refs.git" 37 | }, 38 | "keywords": [ 39 | "json" 40 | ], 41 | "devDependencies": { 42 | "@babel/core": "^7.8.4", 43 | "@babel/preset-env": "^7.8.4", 44 | "@otris/jsdoc-tsd": "^1.0.4", 45 | "babel-loader": "^8.0.6", 46 | "brfs": "~2.0.2", 47 | "connect": "^3.7.0", 48 | "del": "^5.1.0", 49 | "gulp": "~3.9.1", 50 | "gulp-concat": "^2.6.1", 51 | "gulp-eslint": "^6.0.0", 52 | "gulp-istanbul": "~1.1.3", 53 | "gulp-jsdoc-to-markdown": "^1.2.2", 54 | "gulp-jsdoc3": "^2.0.0", 55 | "gulp-load-plugins": "^2.0.2", 56 | "gulp-mocha": "~3.0.1", 57 | "gulp-replace": "^1.0.0", 58 | "jsdoc": "^3.5.5", 59 | "karma": "^4.4.1", 60 | "karma-mocha": "^1.3.0", 61 | "karma-mocha-reporter": "^2.2.5", 62 | "karma-phantomjs-launcher": "^1.0.4", 63 | "karma-webpack": "^4.0.2", 64 | "mocha": "^7.0.1", 65 | "phantomjs-prebuilt": "^2.1.16", 66 | "run-sequence": "^2.2.1", 67 | "transform-loader": "^0.2.4", 68 | "webpack": "^4.41.6" 69 | }, 70 | "dependencies": { 71 | "commander": "~4.1.1", 72 | "graphlib": "^2.1.8", 73 | "js-yaml": "^3.13.1", 74 | "lodash": "^4.17.15", 75 | "native-promise-only": "^0.8.1", 76 | "path-loader": "^1.0.10", 77 | "slash": "^3.0.0", 78 | "uri-js": "^4.2.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/browser/documents/circular-child.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | A: 3 | type: object 4 | properties: 5 | children: 6 | type: array 7 | items: 8 | $ref: './circular-root.yaml#/components/schemas/B' 9 | B: 10 | type: object 11 | properties: 12 | parent: 13 | $ref: './circular-root.yaml#/components/schemas/A' 14 | -------------------------------------------------------------------------------- /test/browser/documents/circular-local.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | root: 3 | $ref: '#' 4 | self: 5 | $ref: '#/definitions/self' 6 | -------------------------------------------------------------------------------- /test/browser/documents/circular-root.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Test 4 | version: 0.0.1 5 | description: | 6 | testing circular reference 7 | 8 | components: 9 | schemas: 10 | A: 11 | $ref: './circular-child.yaml#/definitions/A' 12 | B: 13 | $ref: './circular-child.yaml#/definitions/B' 14 | 15 | paths: 16 | '/test': 17 | get: 18 | responses: 19 | '200': 20 | description: test 21 | content: 22 | application/json: 23 | schema: 24 | type: object 25 | properties: 26 | A: 27 | $ref: '#/components/schemas/A' 28 | -------------------------------------------------------------------------------- /test/browser/documents/nested/test-nested-1.yaml: -------------------------------------------------------------------------------- 1 | name: documents/nested/test-nested-1.yaml 2 | ancestor: 3 | $ref: ../test-document-1.yaml 4 | local: 5 | $ref: '#/name' 6 | missing: 7 | $ref: '#/some/missing/path' 8 | -------------------------------------------------------------------------------- /test/browser/documents/nested/test-nested.yaml: -------------------------------------------------------------------------------- 1 | name: documents/nested/test-nested.yaml 2 | child: 3 | $ref: ./test-nested-1.yaml 4 | local: 5 | $ref: '#/name' 6 | missing: 7 | $ref: '#/some/missing/path' 8 | -------------------------------------------------------------------------------- /test/browser/documents/test-document-1.yaml: -------------------------------------------------------------------------------- 1 | name: documents/test-document-1.yaml 2 | nested: 3 | $ref: ./nested/test-nested.yaml 4 | local: 5 | $ref: '#/name' 6 | missing: 7 | $ref: '#/some/missing/path' 8 | -------------------------------------------------------------------------------- /test/browser/documents/test-document-same.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | SameName: 3 | type: object 4 | properties: 5 | given: 6 | type: string 7 | SameNameContain: 8 | type: object 9 | properties: 10 | age: 11 | type: integer 12 | name: 13 | $ref: '#/definitions/SameName' 14 | -------------------------------------------------------------------------------- /test/browser/documents/test-document.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "description": "Various utilities for JSON References and JSON Pointers.", 4 | "name": "json-refs" 5 | }, 6 | "array": [ 7 | { 8 | "$ref": "#/project/name" 9 | }, 10 | { 11 | "$ref": "#/project/description" 12 | } 13 | ], 14 | "nonURIEncoded": { 15 | "$ref": "#/foo bar" 16 | }, 17 | "nonURIEncodedMissing": { 18 | "$ref": "#/foo bar missing" 19 | }, 20 | "uriEncoded1": { 21 | "$ref": "#/foo%20bar" 22 | }, 23 | "uriEncoded1Missing": { 24 | "$ref": "#/foo%20bar%20missing" 25 | }, 26 | "uriEncoded2": { 27 | "$ref": "#/foo%2520bar" 28 | }, 29 | "uriEncoded2Missing": { 30 | "$ref": "#/foo%2520bar%2520missing" 31 | }, 32 | "foo bar": "foo bar", 33 | "foo%20bar": "foo%20bar", 34 | "circular": { 35 | "root": { 36 | "$ref": "#" 37 | }, 38 | "ancestor": { 39 | "$ref": "#/circular" 40 | }, 41 | "User": { 42 | "type": "object", 43 | "properties": { 44 | "status": { 45 | "$ref": "#/circular/Status" 46 | } 47 | } 48 | }, 49 | "Status": { 50 | "type": "object", 51 | "properties": { 52 | "user": { 53 | "$ref": "#/circular/User" 54 | }, 55 | "message": { 56 | "$ref": "#/circular/Message" 57 | } 58 | } 59 | }, 60 | "Message": { 61 | "type": "object", 62 | "properties": { 63 | "author": { 64 | "$ref": "#/circular/User" 65 | } 66 | } 67 | }, 68 | "StatusWrapper": { 69 | "type": "object", 70 | "properties": { 71 | "status": { 72 | "$ref": "#/circular/Status" 73 | } 74 | } 75 | } 76 | }, 77 | "definitions": { 78 | "HumanName": { 79 | "type": "object", 80 | "properties": { 81 | "given": { 82 | "type": "string" 83 | }, 84 | "family": { 85 | "type": "string" 86 | } 87 | } 88 | }, 89 | "Person": { 90 | "type": "object", 91 | "properties": { 92 | "age": { 93 | "type": "integer" 94 | }, 95 | "name": { 96 | "$ref": "#/definitions/HumanName" 97 | } 98 | } 99 | } 100 | }, 101 | "invalid": { 102 | "$ref": "http://:8080" 103 | }, 104 | "local": { 105 | "$ref": "#/project/name" 106 | }, 107 | "missing": { 108 | "$ref": "#/some/missing/path" 109 | }, 110 | "remote": { 111 | "absolute": { 112 | "$ref": "https://rawgit.com/whitlockjc/json-refs/master/package.json" 113 | }, 114 | "absolute-with-hash": { 115 | "$ref": "https://rawgit.com/whitlockjc/json-refs/master/package.json#/name" 116 | }, 117 | "relative": { 118 | "$ref": "./nested/test-nested.yaml" 119 | }, 120 | "relative-missing": { 121 | "$ref": "./missing.yaml" 122 | }, 123 | "relative-with-hash": { 124 | "$ref": "./nested/test-nested.yaml#/name" 125 | }, 126 | "relative-with-hash2": { 127 | "$ref": "./test-types.yaml#/definitions/Person" 128 | }, 129 | "relative-with-hash3": { 130 | "$ref": "./test-types.yaml#/missing" 131 | }, 132 | "relative-with-inline-relative-path": { 133 | "$ref": "./nested/../test-types.yaml#/definitions/Integer" 134 | } 135 | }, 136 | "warning": { 137 | "$ref": "#/project/name", 138 | "ignored": { 139 | "$ref": "#/project/name" 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /test/browser/documents/test-document.yaml: -------------------------------------------------------------------------------- 1 | # Simple details that could be referenced 2 | project: 3 | description: Various utilities for JSON References and JSON Pointers. 4 | name: json-refs 5 | 6 | # An array of references to make sure document walking works for arrays 7 | array: 8 | - $ref: '#/project/name' 9 | - $ref: '#/project/description' 10 | 11 | # Not a URI encoded reference 12 | nonURIEncoded: 13 | $ref: '#/foo bar' 14 | 15 | nonURIEncodedMissing: 16 | $ref: '#/foo bar missing' 17 | 18 | # URI Encoded reference 19 | uriEncoded1: 20 | $ref: '#/foo%20bar' 21 | 22 | uriEncoded1Missing: 23 | $ref: '#/foo%20bar%20missing' 24 | 25 | # Another URI encoded reference 26 | uriEncoded2: 27 | $ref: '#/foo%2520bar' 28 | 29 | uriEncoded2Missing: 30 | $ref: '#/foo%2520bar%2520missing' 31 | 32 | # Values that require URI encoding to reference properly 33 | foo bar: foo bar 34 | foo%20bar: foo%20bar 35 | 36 | # Various circular references 37 | circular: 38 | # Reference to the root of the document 39 | root: 40 | $ref: '#' 41 | # Reference to an ancestor 42 | ancestor: 43 | $ref: '#/circular' 44 | # Reference to another object that references this one 45 | User: 46 | type: object 47 | properties: 48 | status: 49 | $ref: '#/circular/Status' 50 | # Reference to another object that references this one 51 | Status: 52 | type: object 53 | properties: 54 | user: 55 | $ref: '#/circular/User' 56 | message: 57 | $ref: '#/circular/Message' 58 | # Reference to another object that itself has an indirect reference to this one 59 | Message: 60 | type: object 61 | properties: 62 | author: 63 | $ref: '#/circular/User' 64 | # Reference to another object that itself has an indirect reference to itself 65 | StatusWrapper: 66 | type: object 67 | properties: 68 | status: 69 | $ref: '#/circular/Status' 70 | 71 | definitions: 72 | HumanName: 73 | type: object 74 | properties: 75 | given: 76 | type: string 77 | family: 78 | type: string 79 | Person: 80 | type: object 81 | properties: 82 | age: 83 | type: integer 84 | name: 85 | $ref: '#/definitions/HumanName' 86 | 87 | # Invalid reference 88 | invalid: 89 | $ref: 'http://:8080' 90 | 91 | # Local reference 92 | local: 93 | $ref: '#/project/name' 94 | 95 | missing: 96 | $ref: '#/some/missing/path' 97 | 98 | # Remote references 99 | remote: 100 | absolute: 101 | $ref: 'https://rawgit.com/whitlockjc/json-refs/master/package.json' 102 | absolute-with-hash: 103 | $ref: 'https://rawgit.com/whitlockjc/json-refs/master/package.json#/name' 104 | relative: 105 | $ref: './nested/test-nested.yaml' 106 | relative-missing: 107 | $ref: './missing.yaml' 108 | relative-with-hash: 109 | $ref: './nested/test-nested.yaml#/name' 110 | relative-with-hash2: 111 | $ref: './test-types.yaml#/definitions/Person' 112 | relative-with-hash3: 113 | $ref: './test-types.yaml#/missing' 114 | relative-with-inline-relative-path: 115 | $ref: './nested/../test-types.yaml#/definitions/Integer' 116 | 117 | # A reference with extra properties that should produce a warning 118 | warning: 119 | $ref: '#/project/name' 120 | # This should not be processed as it should be ignored 121 | ignored: 122 | $ref: '#/project/name' 123 | -------------------------------------------------------------------------------- /test/browser/documents/test-types.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | Integer: 3 | type: integer 4 | String: 5 | type: string 6 | Address: 7 | type: object 8 | properties: 9 | name: 10 | $ref: '#/definitions/String' 11 | street: 12 | type: array 13 | items: 14 | $ref: '#/definitions/String' 15 | city: 16 | $ref: '#/definitions/String' 17 | state: 18 | $ref: '#/definitions/String' 19 | Person: 20 | type: object 21 | properties: 22 | addresses: 23 | type: array 24 | items: 25 | $ref: '#/definitions/Address' 26 | name: 27 | $ref: '#/definitions/String' 28 | age: 29 | $ref: '#/definitions/Integer' 30 | family: 31 | type: array 32 | items: 33 | $ref: '#/definitions/Person' 34 | -------------------------------------------------------------------------------- /test/browser/documents/{id}/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jeremy" 3 | } 4 | -------------------------------------------------------------------------------- /test/browser/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* Karma configuration for standalone build */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = function (config) { 6 | console.log(); 7 | console.log('Browser Tests'); 8 | console.log(); 9 | 10 | config.set({ 11 | autoWatch: false, 12 | basePath: '..', 13 | browsers: ['PhantomJS'], 14 | frameworks: ['mocha'], 15 | reporters: ['mocha'], 16 | singleRun: true, 17 | files: [ 18 | {pattern: 'test-json-refs.js', watch: false}, 19 | {pattern: 'browser/documents/**/*', watched: false, included: false} 20 | ], 21 | client: { 22 | mocha: { 23 | reporter: 'html', 24 | timeout: 10000, 25 | ui: 'bdd' 26 | } 27 | }, 28 | plugins: [ 29 | 'karma-mocha', 30 | 'karma-mocha-reporter', 31 | 'karma-phantomjs-launcher', 32 | 'karma-webpack' 33 | ], 34 | preprocessors: { 35 | 'test-json-refs.js': ['webpack'] 36 | }, 37 | webpack: { 38 | mode: 'development', 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | loader: 'transform-loader?brfs' 44 | }, 45 | { 46 | test: /\.js$/, 47 | use: { 48 | loader: 'babel-loader', 49 | options: { 50 | presets: ['@babel/env'] 51 | } 52 | } 53 | } 54 | ] 55 | }, 56 | node: { 57 | fs: 'empty' 58 | } 59 | }, 60 | webpackMiddleware: { 61 | stats: 'errors-only' 62 | } 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /test/test-cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, mocha */ 2 | 3 | /* 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2014 Jeremy Whitlock 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | 'use strict'; 28 | 29 | var _ = require('lodash'); 30 | var assert = require('assert'); 31 | var cp = require('child_process'); 32 | var http = require('http'); 33 | var JsonRefs = require('..'); 34 | var path = require('path'); 35 | var pkg = require('../package.json'); 36 | var YAML = require('js-yaml'); 37 | 38 | var jsonRefsOptions = { 39 | loaderOptions: { 40 | processContent: function (res, callback) { 41 | callback(undefined, YAML.safeLoad(res.text)); 42 | } 43 | } 44 | }; 45 | 46 | var globalHelp = [ 47 | 'Usage: json-refs [options] [command]', 48 | '', 49 | 'Options:', 50 | ' -V, --version output the version number', 51 | ' -h, --help output usage information', 52 | '', 53 | 'Commands:', 54 | ' help [command] Display help information', 55 | ' resolve [options] Prints document at location with its JSON References resolved', 56 | '', 57 | ].join('\n'); 58 | 59 | var helpHelp = [ 60 | 'Usage: json-refs help [options] [command]', 61 | '', 62 | 'Display help information', 63 | '', 64 | 'Options:', 65 | ' -h, --help output usage information', 66 | '' 67 | ].join('\n'); 68 | 69 | var resolveHelp = [ 70 | 'Usage: json-refs resolve [options] ', 71 | '', 72 | 'Prints document at location with its JSON References resolved', 73 | '', 74 | 'Options:', 75 | ' -f, --force Do not fail when the document has invalid JSON References', 76 | ' -H, --header
The header to use when retrieving a remote document (default: [])', 77 | ' -I, --filter The type of JSON References to resolved (default: [])', 78 | ' -w, --warnings-as-errors Treat warnings as errors', 79 | ' -y, --yaml Output as YAML', 80 | ' -h, --help output usage information', 81 | '' 82 | ].join('\n'); 83 | 84 | var testDocumentLocationYaml = path.join(__dirname, 'browser', 'documents', 'test-document.yaml'); 85 | var testDocumentLocationJson = path.join(__dirname, 'browser', 'documents', 'test-document.json'); 86 | 87 | function executeJsonRefs (args, done, cwd) { 88 | var options; 89 | 90 | // Add Node args 91 | args.unshift('node', path.resolve(path.join(__dirname, '..', 'bin', 'json-refs'))); 92 | 93 | if (typeof cwd !== 'undefined') { 94 | options = { 95 | cwd: cwd 96 | }; 97 | } 98 | 99 | cp.exec(args.join(' '), options, function (err, stdout, stderr) { 100 | done(stderr, stdout); 101 | }); 102 | }; 103 | 104 | describe('json-refs CLI', function () { 105 | it('--help flag', function (done) { 106 | executeJsonRefs(['--help'], function (stderr, stdout) { 107 | assert.equal(stderr, ''); 108 | assert.equal(stdout, globalHelp); 109 | 110 | done(); 111 | }); 112 | }); 113 | 114 | it('--version flag', function (done) { 115 | executeJsonRefs(['--version'], function (stderr, stdout) { 116 | assert.equal(stderr, ''); 117 | assert.equal(stdout, pkg.version + '\n'); 118 | 119 | done(); 120 | }); 121 | }); 122 | 123 | it('invalid command', function (done) { 124 | executeJsonRefs(['invalid'], function (stderr, stdout) { 125 | assert.equal(stderr, ''); 126 | assert.equal(stdout, 'json-refs does not support the invalid command.\n' + globalHelp); 127 | 128 | done(); 129 | }); 130 | }); 131 | 132 | it('no command', function (done) { 133 | executeJsonRefs([], function (stderr, stdout) { 134 | assert.equal(stderr, ''); 135 | assert.equal(stdout, globalHelp); 136 | 137 | done(); 138 | }); 139 | }); 140 | 141 | describe('help command', function () { 142 | it('--help flag', function (done) { 143 | executeJsonRefs(['help', '--help'], function (stderr, stdout) { 144 | assert.equal(stderr, ''); 145 | assert.equal(stdout, helpHelp); 146 | 147 | done(); 148 | }); 149 | }); 150 | 151 | it('no sub-command', function (done) { 152 | executeJsonRefs(['help'], function (stderr, stdout) { 153 | assert.equal(stderr, ''); 154 | assert.equal(stdout, globalHelp); 155 | 156 | done(); 157 | }); 158 | }); 159 | 160 | it('help sub-command', function (done) { 161 | executeJsonRefs(['help', 'help'], function (stderr, stdout) { 162 | assert.equal(stderr, ''); 163 | assert.equal(stdout, helpHelp); 164 | 165 | done(); 166 | }); 167 | }); 168 | 169 | it('resolve sub-command', function (done) { 170 | executeJsonRefs(['help', 'resolve'], function (stderr, stdout) { 171 | assert.equal(stderr, ''); 172 | assert.equal(stdout, resolveHelp); 173 | 174 | done(); 175 | }); 176 | }); 177 | }); 178 | 179 | describe('resolve command', function () { 180 | it('--help flag', function (done) { 181 | executeJsonRefs(['resolve', '--help'], function (stderr, stdout) { 182 | assert.equal(stderr, ''); 183 | assert.equal(stdout, resolveHelp); 184 | 185 | done(); 186 | }); 187 | }); 188 | 189 | it('no arguments', function (done) { 190 | executeJsonRefs(['resolve'], function (stderr, stdout) { 191 | assert.equal(stderr, [ 192 | 'error: missing required argument \'location\'', 193 | '', 194 | ].join('\n')); 195 | assert.equal(stdout, ''); 196 | 197 | done(); 198 | }); 199 | }); 200 | 201 | describe('invalid location', function () { 202 | it('missing file location', function (done) { 203 | var filePath = './missing.yaml'; 204 | 205 | executeJsonRefs(['resolve', filePath], function (stderr, stdout) { 206 | assert.ok(stderr.indexOf('error: ENOENT') > -1); 207 | assert.ok(stderr.indexOf('open \'' + path.resolve(filePath) + '\'') > -1); 208 | assert.equal(stdout, ''); 209 | 210 | done(); 211 | }); 212 | }); 213 | 214 | it('missing http location', function (done) { 215 | var location = 'https://rawgit.com/whitlockjc/json-refs/master/test/browser/documents/missing.yaml'; 216 | 217 | executeJsonRefs(['resolve', location], function (stderr, stdout) { 218 | assert.equal(stderr, [ 219 | '', 220 | ' error: Not Found', 221 | '', 222 | '' 223 | ].join('\n')); 224 | assert.equal(stdout, ''); 225 | 226 | done(); 227 | }); 228 | }); 229 | }); 230 | 231 | describe('valid location', function () { 232 | var httpServer; 233 | 234 | before(function (done) { 235 | httpServer = http.createServer(function (req, res) { 236 | // Echo back the provided headers 237 | ['Header-One', 'Header-Two'].forEach(function (header) { 238 | if (req.headers[header.toLowerCase()]) { 239 | res.setHeader(header, req.headers[header.toLowerCase()]); 240 | } 241 | }); 242 | 243 | res.setHeader('Content-Type', 'application/json'); 244 | 245 | res.end(JSON.stringify(pkg)); 246 | }); 247 | 248 | httpServer.listen(1337, done); 249 | }); 250 | 251 | after(function (done) { 252 | httpServer.close(done); 253 | }); 254 | 255 | it('no options', function (done) { 256 | this.timeout(10000); 257 | 258 | executeJsonRefs(['resolve', testDocumentLocationYaml], function (stderr, stdout) { 259 | assert.equal(stdout, ''); 260 | 261 | assert.equal(stderr, [ 262 | '', 263 | ' error: Document has invalid references:', 264 | '', 265 | ' #/invalid: HTTP URIs must have a host.', 266 | ' #/missing: JSON Pointer points to missing location: #/some/missing/path', 267 | ' #/nonURIEncodedMissing: JSON Pointer points to missing location: #/foo bar missing', 268 | ' #/remote/relative-missing: JSON Pointer points to missing location: ./missing.yaml', 269 | ' #/remote/relative-with-hash3: JSON Pointer points to missing location: ./test-types.yaml#/missing', 270 | ' #/remote/relative/child/ancestor/missing: JSON Pointer points to missing location: #/some/missing/path', 271 | ' #/remote/relative/child/missing: JSON Pointer points to missing location: #/some/missing/path', 272 | ' #/remote/relative/missing: JSON Pointer points to missing location: #/some/missing/path', 273 | ' #/uriEncoded1Missing: JSON Pointer points to missing location: #/foo%20bar%20missing', 274 | ' #/uriEncoded2Missing: JSON Pointer points to missing location: #/foo%2520bar%2520missing', 275 | '', 276 | '' 277 | ].join('\n')); 278 | 279 | done(); 280 | }); 281 | }); 282 | 283 | it('--filter option(s)', function (done) { 284 | this.timeout(10000); 285 | 286 | var cliArgs = [ 287 | 'resolve', 288 | testDocumentLocationJson, 289 | '--filter', 'relative', 290 | '-I', 'remote', 291 | '--force' 292 | ]; 293 | var cOptions = _.cloneDeep(jsonRefsOptions); 294 | 295 | cOptions.filter = ['relative', 'remote']; 296 | 297 | executeJsonRefs(cliArgs, function (stderr, stdout) { 298 | assert.equal(stderr, ''); 299 | 300 | JsonRefs.resolveRefsAt(testDocumentLocationYaml, cOptions) 301 | .then(function (results) { 302 | assert.equal(stdout, JSON.stringify(results.resolved, null, 2) + '\n'); 303 | }) 304 | .then(done, done); 305 | }); 306 | }); 307 | 308 | it('--force option', function (done) { 309 | this.timeout(10000); 310 | 311 | executeJsonRefs(['resolve', testDocumentLocationJson, '--force'], function (stderr, stdout) { 312 | assert.equal(stderr, ''); 313 | 314 | JsonRefs.resolveRefsAt(testDocumentLocationYaml, jsonRefsOptions) 315 | .then(function (results) { 316 | assert.equal(stdout, JSON.stringify(results.resolved, null, 2) + '\n'); 317 | }) 318 | .then(done, done); 319 | }); 320 | }); 321 | 322 | it('--header option(s)', function (done) { 323 | this.timeout(10000); 324 | 325 | var cliArgs = [ 326 | 'resolve', 327 | 'http://localhost:1337', 328 | '--header', '"MyHeader: MyValue"', 329 | '-H', '"MyOtherHeader: MyOtherValue"' 330 | ]; 331 | var cOptions = _.cloneDeep(jsonRefsOptions); 332 | 333 | cOptions.loaderOptions.processContent = function (res, callback) { 334 | try { 335 | assert.equal(res.headers['header-one'], 'Value One'); 336 | assert.equal(res.headers['header-two'], 'Value Two'); 337 | 338 | callback(undefined, JSON.parse(res.text)); 339 | } catch (err) { 340 | callback(err); 341 | } 342 | }; 343 | 344 | executeJsonRefs(cliArgs, function (stderr, stdout) { 345 | assert.equal(stderr, ''); 346 | 347 | assert.deepEqual(stdout, JSON.stringify(pkg, null, 2) + '\n'); 348 | 349 | done(); 350 | }); 351 | }); 352 | 353 | it('--warnings-as-errors option', function (done) { 354 | this.timeout(10000); 355 | 356 | executeJsonRefs(['resolve', testDocumentLocationYaml, '--warnings-as-errors'], function (stderr, stdout) { 357 | assert.equal(stdout, ''); 358 | 359 | assert.equal(stderr, [ 360 | '', 361 | ' error: Document has invalid references:', 362 | '', 363 | ' #/invalid: HTTP URIs must have a host.', 364 | ' #/missing: JSON Pointer points to missing location: #/some/missing/path', 365 | ' #/nonURIEncodedMissing: JSON Pointer points to missing location: #/foo bar missing', 366 | ' #/remote/relative-missing: JSON Pointer points to missing location: ./missing.yaml', 367 | ' #/remote/relative-with-hash3: JSON Pointer points to missing location: ./test-types.yaml#/missing', 368 | ' #/remote/relative/child/ancestor/missing: JSON Pointer points to missing location: #/some/missing/path', 369 | ' #/remote/relative/child/missing: JSON Pointer points to missing location: #/some/missing/path', 370 | ' #/remote/relative/missing: JSON Pointer points to missing location: #/some/missing/path', 371 | ' #/uriEncoded1Missing: JSON Pointer points to missing location: #/foo%20bar%20missing', 372 | ' #/uriEncoded2Missing: JSON Pointer points to missing location: #/foo%2520bar%2520missing', 373 | ' #/warning: Extra JSON Reference properties will be ignored: ignored', 374 | '', 375 | '' 376 | ].join('\n')); 377 | 378 | done(); 379 | }); 380 | }); 381 | 382 | it('--yaml option', function (done) { 383 | this.timeout(10000); 384 | 385 | executeJsonRefs(['resolve', testDocumentLocationJson, '-fy'], function (stderr, stdout) { 386 | assert.equal(stderr, ''); 387 | 388 | JsonRefs.resolveRefsAt(testDocumentLocationYaml, jsonRefsOptions) 389 | .then(function (results) { 390 | assert.equal(stdout, YAML.safeDump(results.resolved, {noRefs: true}) + '\n'); 391 | }) 392 | .then(done, done); 393 | }); 394 | }); 395 | }); 396 | 397 | describe('issues', function () { 398 | describe('Issue 104', function () { 399 | it('should handle locations with a fragment', function (done) { 400 | var subDocPath = '#/array'; 401 | 402 | this.timeout(10000); 403 | 404 | executeJsonRefs(['resolve', testDocumentLocationYaml + subDocPath], function (stderr, stdout) { 405 | var cOptions = _.cloneDeep(jsonRefsOptions); 406 | 407 | assert.equal(stderr, ''); 408 | 409 | cOptions.subDocPath = subDocPath; 410 | 411 | JsonRefs.resolveRefsAt(testDocumentLocationYaml, cOptions) 412 | .then(function (results) { 413 | assert.equal(stdout, JSON.stringify(results.resolved, null, 2) + '\n'); 414 | }) 415 | .then(done, done); 416 | }); 417 | }); 418 | }); 419 | 420 | describe('Issue #67', function () { 421 | // Since the ancestor is a circular, the fact it is marked as such and not marked as missing is enough of a test 422 | var expectedOutput = [ 423 | '', 424 | ' error: Document has invalid references:', 425 | '', 426 | ' #/ancestor/missing: JSON Pointer points to missing location: #/some/missing/path', 427 | ' #/ancestor/nested/missing: JSON Pointer points to missing location: #/some/missing/path', 428 | ' #/missing: JSON Pointer points to missing location: #/some/missing/path', 429 | '', 430 | '', 431 | ].join('\n'); 432 | 433 | it('relative references to ancestor of process.cwd()', function (done) { 434 | this.timeout(10000); 435 | 436 | executeJsonRefs(['resolve', './test-nested-1.yaml'], function (stderr, stdout) { 437 | assert.equal(stderr, expectedOutput); 438 | assert.equal(stdout, ''); 439 | 440 | done(); 441 | }, path.join(__dirname, 'browser', 'documents', 'nested')); 442 | }); 443 | 444 | it('relative references to child of process.cwd()', function (done) { 445 | this.timeout(10000); 446 | 447 | executeJsonRefs(['resolve', '../test/browser/documents/nested/test-nested-1.yaml'], function (stderr, stdout) { 448 | assert.equal(stderr, expectedOutput); 449 | assert.equal(stdout, ''); 450 | 451 | done(); 452 | }, __dirname); 453 | }); 454 | }); 455 | describe('Issue #66', function () { 456 | it('json-refs resolve output the same language that was input', function (done) { 457 | this.timeout(10000); 458 | 459 | executeJsonRefs(['resolve', testDocumentLocationYaml, '-f'], function (stderr, stdout) { 460 | assert.equal(stderr, ''); 461 | 462 | JsonRefs.resolveRefsAt(testDocumentLocationYaml, jsonRefsOptions) 463 | .then(function (results) { 464 | assert.equal(stdout, YAML.safeDump(results.resolved, {noRefs: true}) + '\n'); 465 | }) 466 | .then(done, done); 467 | }); 468 | }); 469 | }); 470 | }); 471 | }); 472 | }); 473 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = [{ 6 | devtool: 'inline-source-map', 7 | entry: './index.js', 8 | mode: 'development', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | use: { 14 | loader: 'babel-loader', 15 | options: { 16 | presets: [ 17 | ['@babel/env', { 18 | targets: 'cover 100%' 19 | }] 20 | ] 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | name: 'json-refs', 27 | optimization: { 28 | minimize: false 29 | }, 30 | output: { 31 | path: path.resolve(__dirname, 'dist'), 32 | filename: 'json-refs.js', 33 | library: 'JsonRefs' 34 | } 35 | }, { 36 | entry: './index.js', 37 | mode: 'production', 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.js$/, 42 | use: { 43 | loader: 'babel-loader', 44 | options: { 45 | presets: [ 46 | ['@babel/env', { 47 | targets: 'cover 100%' 48 | }] 49 | ] 50 | } 51 | } 52 | } 53 | ] 54 | }, 55 | name: 'json-refs-min', 56 | optimization: { 57 | minimize: true 58 | }, 59 | output: { 60 | path: path.resolve(__dirname, 'dist'), 61 | filename: 'json-refs-min.js', 62 | library: 'JsonRefs' 63 | } 64 | }]; 65 | --------------------------------------------------------------------------------