Skip to content

Commit f409c32

Browse files
committed
add shared schema across multiple root resolution
1 parent edc388c commit f409c32

File tree

7 files changed

+239
-26
lines changed

7 files changed

+239
-26
lines changed

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
[![CI](https://github.com/Eomm/json-schema-resolver/workflows/ci/badge.svg)](https://github.com/Eomm/json-schema-resolver/actions?query=workflow%3Aci)
44
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
55

6-
Resolve all `$refs` in your [JSON schema](https://json-schema.org/specification.html)!
6+
Resolve all `$refs` in your [JSON schema](https://json-schema.org/specification.html)!
7+
This module will resolve the `$ref` keyword against the `externalSchemas` you will provide.
8+
If a reference is missing, it will not throw any error.
79

10+
The `$ref` will be modified to point to a local reference `#/definitions/<generated key>`.
11+
Moreover, the `definitions` keyword will be decorated with the external schemas to get only
12+
one JSON schema resolved as output.
813

914
## Install
1015

@@ -14,7 +19,7 @@ npm install json-schema-resolver
1419

1520
This plugin support Node.js >= 10
1621

17-
## Usage
22+
## Usage: resolve one schema against external schemas
1823

1924
```js
2025
const RefResolver = require('json-schema-resolver')
@@ -83,6 +88,39 @@ const singleSchema = ref.resolve(inputSchema, { externalSchemas: [addresSchema]
8388
}
8489
```
8590

91+
## Usage: resolve multiple schemas against external shared schemas
92+
93+
When you have multiple schemas to resolve against a collection of shared schema you need to use this
94+
module with little changes.
95+
96+
This is needed to have all the same definitions path (`#/definitions/<generated key>`) across all the
97+
root schemas
98+
99+
```js
100+
const ref = RefResolver({
101+
clone: true, // Clone the input schema without changing it. Default: false
102+
applicationUri: 'my-application.org', // You need to provide an unique URI to resolve relative `$id`s
103+
externalSchemas: [addresSchema] // The schemas provided at the creation of the resolver, will be used evvery time `.resolve` will be called
104+
})
105+
106+
const inputSchema = {
107+
$id: 'http://example.com/SimplePerson',
108+
type: 'object',
109+
properties: {
110+
name: { type: 'string' },
111+
address: { $ref: 'relativeAddress#' },
112+
houses: { type: 'array', items: { $ref: 'relativeAddress#' } }
113+
}
114+
}
115+
116+
// the resolved schema DOES NOT have definitions added
117+
const singleSchema = ref.resolve(inputSchema)
118+
const anotherResolvedSchema = ref.resolve(input_2_Schema)
119+
120+
// to get the definition you need only to call:
121+
const sharedDefinitions = ref.definitions()
122+
```
123+
86124
## License
87125

88126
Licensed under [MIT](./LICENSE).

ref-resolver.js

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ const targetCfg = {
3333
// logic: https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.B.1
3434
function jsonSchemaResolver (options) {
3535
const ee = new EventEmitter()
36-
const { clone, target } = Object.assign({}, defaultOpts, options)
37-
38-
if (!targetSupported.includes(target)) {
39-
throw new Error(`Unsupported JSON schema version ${target}`)
40-
}
36+
const { clone, target, applicationUri, externalSchemas: rootExternalSchemas } = Object.assign({}, defaultOpts, options)
4137

4238
const allIds = new Map()
4339
let rolling = 0
@@ -46,26 +42,47 @@ function jsonSchemaResolver (options) {
4642
const allRefs = []
4743
ee.on('$ref', collectRefs)
4844

45+
if (!targetSupported.includes(target)) {
46+
throw new Error(`Unsupported JSON schema version ${target}`)
47+
}
48+
49+
let defaultUri
50+
if (applicationUri) {
51+
defaultUri = getRootUri(applicationUri)
52+
53+
if (rootExternalSchemas) {
54+
for (const es of rootExternalSchemas) { mapIds(ee, defaultUri, es) }
55+
debug('Processed root external schemas')
56+
}
57+
} else if (rootExternalSchemas) {
58+
throw new Error('If you set root externalSchema, the applicationUri option is needed')
59+
}
60+
4961
return {
50-
resolve
62+
resolve,
63+
definitions () {
64+
const defKey = targetCfg[target].def
65+
const x = { [defKey]: {} }
66+
allIds.forEach((json, baseUri) => {
67+
x[defKey][json[kRefToDef]] = json
68+
})
69+
return x
70+
}
5171
}
5272

5373
function resolve (rootSchema, opts) {
5474
const { externalSchemas } = opts || {}
5575

56-
allIds.clear()
76+
if (!rootExternalSchemas) {
77+
allIds.clear()
78+
}
5779
allRefs.length = 0
5880

5981
if (clone) {
6082
rootSchema = cloner(rootSchema)
6183
}
6284

63-
// If present, the value for this keyword MUST be a string, and MUST
64-
// represent a valid URI-reference [RFC3986]. This value SHOULD be
65-
// normalized, and SHOULD NOT be an empty fragment <#> or an empty
66-
// string <>.
67-
const appUri = URI.parse(rootSchema.$id || 'application.uri')
68-
appUri.fragment = undefined // remove fragment
85+
const appUri = defaultUri || getRootUri(rootSchema.$id)
6986
debug('Found app URI %o', appUri)
7087

7188
if (externalSchemas) {
@@ -96,16 +113,19 @@ function jsonSchemaResolver (options) {
96113
json.$ref = `#/definitions/${evaluatedJson[kRefToDef]}`
97114
})
98115

99-
const defKey = targetCfg[target].def
100-
allIds.forEach((json, baseUri) => {
101-
if (json[kConsumed] === true) {
102-
if (!rootSchema[defKey]) {
103-
rootSchema[defKey] = {}
116+
if (externalSchemas) {
117+
// only if user sets external schema add it to the definitions
118+
const defKey = targetCfg[target].def
119+
allIds.forEach((json, baseUri) => {
120+
if (json[kConsumed] === true) {
121+
if (!rootSchema[defKey]) {
122+
rootSchema[defKey] = {}
123+
}
124+
125+
rootSchema[defKey][json[kRefToDef]] = json
104126
}
105-
106-
rootSchema[defKey][json[kRefToDef]] = json
107-
}
108-
})
127+
})
128+
}
109129

110130
return rootSchema
111131
}
@@ -193,4 +213,14 @@ function mapIds (ee, baseUri, json) {
193213
}
194214
}
195215

216+
function getRootUri (strUri = 'application.uri') {
217+
// If present, the value for this keyword MUST be a string, and MUST
218+
// represent a valid URI-reference [RFC3986]. This value SHOULD be
219+
// normalized, and SHOULD NOT be an empty fragment <#> or an empty
220+
// string <>.
221+
const uri = URI.parse(strUri)
222+
uri.fragment = undefined
223+
return uri
224+
}
225+
196226
module.exports = jsonSchemaResolver

test/example.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,74 @@ test('readme example', t => {
6565
}
6666
})
6767
})
68+
69+
test('readme example #2', t => {
70+
t.plan(2)
71+
72+
const addresSchema = {
73+
$id: 'relativeAddress',
74+
type: 'object',
75+
properties: {
76+
zip: { type: 'string' },
77+
city: { type: 'string' }
78+
}
79+
}
80+
81+
const ref = RefResolver({
82+
clone: true, // Clone the input schema without changing it. Default: false
83+
applicationUri: 'my-application.org', // You need to provide an unique URI to resolve relative `$id`s
84+
externalSchemas: [addresSchema] // The schemas provided at the creation of the resolver, will be used evvery time `.resolve` will be called
85+
})
86+
87+
const inputSchema = {
88+
$id: 'http://example.com/SimplePerson',
89+
type: 'object',
90+
properties: {
91+
name: { type: 'string' },
92+
address: { $ref: 'relativeAddress#' },
93+
houses: { type: 'array', items: { $ref: 'relativeAddress#' } }
94+
}
95+
}
96+
97+
const singleSchema = ref.resolve(inputSchema)
98+
// the resolved schema DOES NOT have definitions added
99+
100+
// to get the definition you need only to call:
101+
const sharedDefinitions = ref.definitions()
102+
103+
t.deepEqual(sharedDefinitions, {
104+
definitions: {
105+
'def-0': {
106+
$id: 'relativeAddress',
107+
type: 'object',
108+
properties: {
109+
zip: {
110+
type: 'string'
111+
},
112+
city: {
113+
type: 'string'
114+
}
115+
}
116+
}
117+
}
118+
})
119+
120+
t.deepEqual(singleSchema, {
121+
$id: 'my-application.org',
122+
type: 'object',
123+
properties: {
124+
name: {
125+
type: 'string'
126+
},
127+
address: {
128+
$ref: '#/definitions/def-0'
129+
},
130+
houses: {
131+
type: 'array',
132+
items: {
133+
$ref: '#/definitions/def-0'
134+
}
135+
}
136+
}
137+
})
138+
})

test/ref-resolver.test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ const save = (out) => require('fs').writeFileSync(`./out-${Date.now()}.json`, JS
1212
// https://json-schema.org/draft/2019-09/json-schema-core.html#idExamples
1313

1414
test('wrong params', t => {
15-
t.plan(1)
15+
t.plan(2)
1616
t.throws(() => RefResolver({ target: 'draft-1000' }))
17+
t.throws(() => RefResolver({ externalSchemas: [] }), 'need application uri')
1718
})
1819

1920
test('$ref to root', t => {
@@ -159,7 +160,7 @@ test('dont resolve external schema missing', t => {
159160

160161
test('dont resolve external schema missing #2', t => {
161162
t.plan(1)
162-
const schema = factory('absoluteId-asoluteRef')
163+
const schema = factory('absoluteId-absoluteRef')
163164

164165
const resolver = RefResolver({ clone: true })
165166
const out = resolver.resolve(schema)

test/ref-root.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const clone = require('rfdc')({ proto: true, circles: false })
5+
6+
const RefResolver = require('../ref-resolver')
7+
const factory = require('./schema-factory')
8+
9+
// eslint-disable-next-line
10+
const save = (out) => require('fs').writeFileSync(`./out-${Date.now()}.json`, JSON.stringify(out, null, 2))
11+
12+
test('application uri priority', t => {
13+
t.plan(3)
14+
15+
const schema = {
16+
$id: 'http://example.com/SimplePerson',
17+
type: 'object',
18+
properties: { name: { type: 'string' } }
19+
}
20+
21+
const opts = {
22+
applicationUri: 'one-single-uri.to'
23+
}
24+
25+
const resolver = RefResolver(opts)
26+
27+
resolver.resolve(schema)
28+
t.notEqual(schema.$id, 'http://example.com/SimplePerson')
29+
t.equal(schema.$id, 'one-single-uri.to')
30+
31+
const externalDef = resolver.definitions()
32+
t.deepEqual(externalDef.definitions, {})
33+
})
34+
35+
test('multiple resolve over same externals', t => {
36+
t.plan(7)
37+
38+
const schema1 = factory('absoluteId-externalRef')
39+
const originalSchema1 = clone(schema1)
40+
41+
const schema2 = factory('absoluteId-externalRef2')
42+
const originalSchema2 = clone(schema2)
43+
44+
const opts = {
45+
applicationUri: 'one-single-uri.to',
46+
externalSchemas: [
47+
factory('relativeId-noRef')
48+
]
49+
}
50+
const resolver = RefResolver(opts)
51+
52+
const out1 = resolver.resolve(schema1)
53+
t.notMatch(schema1, originalSchema1, 'the refs has been changed')
54+
t.deepEquals(out1, schema1)
55+
t.notOk(schema1.definitions, 'definition has not been added')
56+
57+
const out2 = resolver.resolve(schema2)
58+
t.notMatch(schema2, originalSchema2, 'the refs has been changed')
59+
t.deepEquals(out2, schema2)
60+
t.notOk(schema2.definitions, 'definition has not been added')
61+
62+
const externalDef = resolver.definitions()
63+
t.deepEqual(externalDef.definitions['def-0'], factory('relativeId-noRef'))
64+
})
File renamed without changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
$id: 'http://example.com/ComplexPerson',
3+
type: 'object',
4+
properties: {
5+
surname: { type: 'string' },
6+
garage: { $ref: 'relativeAddress#' },
7+
votes: { type: 'integer', minimum: 1 }
8+
}
9+
}

0 commit comments

Comments
 (0)