Skip to content

Commit 4cea068

Browse files
committed
feat(proxy): resolve references
1 parent 1b30584 commit 4cea068

File tree

2 files changed

+147
-78
lines changed

2 files changed

+147
-78
lines changed

lib/schemaProxy.js

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ const symbols = {
1414
filename: Symbol('filename'),
1515
id: Symbol('id'),
1616
titles: Symbol('titles'),
17+
resolve: Symbol('resolve'),
1718
};
1819

19-
const handler = ({ root = '', filename = '.', schemas, parent}) => {
20+
const handler = ({
21+
root = '', filename = '.', schemas, parent,
22+
}) => {
2023
const meta = {};
2124

2225
meta[symbols.pointer] = () => root;
@@ -31,7 +34,7 @@ const handler = ({ root = '', filename = '.', schemas, parent}) => {
3134
return parent[symbols.id];
3235
}
3336
return undefined;
34-
}
37+
};
3538
meta[symbols.titles] = (target) => {
3639
if (parent) {
3740
// if we can determine the parent titles
@@ -40,7 +43,18 @@ const handler = ({ root = '', filename = '.', schemas, parent}) => {
4043
}
4144
// otherwise, it's just our own
4245
return [target.title];
43-
}
46+
};
47+
48+
meta[symbols.resolve] = (target, prop, receiver) => (path) => {
49+
const [head, ...tail] = typeof path === 'string' ? path.split('/') : path;
50+
if ((head === '' || head === undefined) && tail.length === 0) {
51+
return receiver;
52+
} else if (head === '' || head === undefined) {
53+
return receiver[symbols.resolve](tail);
54+
} else {
55+
return receiver[head][symbols.resolve](tail);
56+
}
57+
};
4458

4559
return {
4660
ownKeys: target => Reflect.ownKeys(target),
@@ -52,12 +66,31 @@ const handler = ({ root = '', filename = '.', schemas, parent}) => {
5266

5367
const retval = Reflect.get(target, prop, receiver);
5468
if (typeof retval === 'object') {
55-
//console.log('making new proxy from', target, prop, 'receiver', receiver[symbols.id]);
56-
const subschema = new Proxy(retval, handler({
57-
root: `${root}/${prop}`,
69+
if (retval.$ref) {
70+
const [uri, pointer] = retval.$ref.split('#');
71+
const basedoc = uri || receiver[symbols.id];
72+
if (schemas.known[basedoc]) {
73+
const referenced = schemas.known[basedoc][symbols.resolve](pointer);
74+
// inject the referenced schema into the current schema
75+
Object.assign(retval, referenced);
76+
} else {
77+
console.error('cannot resolve', basedoc);
78+
}
79+
}
80+
81+
// console.log('making new proxy from', target, prop, 'receiver', receiver[symbols.id]);
82+
const subschema = new Proxy(retval, handler({
83+
root: `${root}/${prop}`,
5884
parent: receiver,
59-
filename,
60-
schemas }));
85+
filename,
86+
schemas,
87+
}));
88+
89+
if (subschema.$id) {
90+
// stow away the schema for lookup
91+
// eslint-disable-next-line no-param-reassign
92+
schemas.known[subschema.$id] = subschema;
93+
}
6194
return subschema;
6295
}
6396
return retval;
@@ -66,18 +99,22 @@ const handler = ({ root = '', filename = '.', schemas, parent}) => {
6699
};
67100

68101
module.exports = {
69-
...symbols
70-
}
102+
...symbols,
103+
};
71104

72105
module.exports.loader = () => {
73106
const schemas = {
74107
loaded: [],
75-
known: {}
108+
known: {},
76109
};
77110

78111
return (schema, filename) => {
79-
const proxied = new Proxy(schema, handler({filename, schemas}));
112+
const proxied = new Proxy(schema, handler({ filename, schemas }));
80113
schemas.loaded.push(proxied);
114+
if (proxied.$id) {
115+
// stow away the schema for lookup
116+
schemas.known[proxied.$id] = proxied;
117+
}
81118
return proxied;
82119
};
83-
}
120+
};

test/schemaProxy.test.js

Lines changed: 97 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,148 @@
1+
/*
2+
* Copyright 2019 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
112
/* eslint-env mocha */
213

3-
const assert = require("assert");
4-
const { loader, pointer, filename, id, titles } = require("../lib/schemaProxy");
14+
const assert = require('assert');
15+
const {
16+
loader, pointer, filename, id, titles, resolve,
17+
} = require('../lib/schemaProxy');
518

619
const example = {
7-
"meta:license": [
8-
"Copyright 2017 Adobe Systems Incorporated. All rights reserved.",
20+
'meta:license': [
21+
'Copyright 2017 Adobe Systems Incorporated. All rights reserved.',
922
"This file is licensed to you under the Apache License, Version 2.0 (the 'License');",
10-
"you may not use this file except in compliance with the License. You may obtain a copy",
11-
"of the License at http://www.apache.org/licenses/LICENSE-2.0"
23+
'you may not use this file except in compliance with the License. You may obtain a copy',
24+
'of the License at http://www.apache.org/licenses/LICENSE-2.0',
1225
],
13-
$schema: "http://json-schema.org/draft-06/schema#",
14-
$id: "https://example.com/schemas/example",
15-
title: "Example",
16-
type: "object",
26+
$schema: 'http://json-schema.org/draft-06/schema#',
27+
$id: 'https://example.com/schemas/example',
28+
title: 'Example',
29+
type: 'object',
1730
description:
18-
"This is an example schema with examples. Too many examples? There can never be too many examples!",
31+
'This is an example schema with examples. Too many examples? There can never be too many examples!',
1932
properties: {
2033
foo: {
21-
type: "string",
22-
description: "A simple string.",
23-
examples: ["bar"],
24-
version: "1.0.0",
25-
testProperty: "test"
34+
type: 'string',
35+
description: 'A simple string.',
36+
examples: ['bar'],
37+
version: '1.0.0',
38+
testProperty: 'test',
2639
},
2740
bar: {
28-
type: "string",
29-
description: "A simple string.",
30-
examples: ["bar", "baz"],
31-
version: "1.0.0",
32-
testProperty: "test"
41+
type: 'string',
42+
description: 'A simple string.',
43+
examples: ['bar', 'baz'],
44+
version: '1.0.0',
45+
testProperty: 'test',
3346
},
3447
zip: {
35-
type: "object",
36-
title: 'An object'
48+
type: 'object',
49+
title: 'An object',
3750
},
3851
baz: {
39-
"anyOf": [
40-
{ "$ref": "#/properties/foo"},
41-
{ "$ref": "#/properties/bar"}
42-
]
43-
}
44-
}
52+
anyOf: [
53+
{ $ref: '#/properties/foo' },
54+
{ $ref: '#/properties/bar' },
55+
],
56+
},
57+
},
4558
};
4659

4760
const referencing = {
48-
$schema: "http://json-schema.org/draft-06/schema#",
49-
$id: "https://example.com/schemas/referencing",
61+
$schema: 'http://json-schema.org/draft-06/schema#',
62+
$id: 'https://example.com/schemas/referencing',
5063
properties: {
51-
$ref: 'https://example.com/schemas/referencing#/properties'
52-
}
53-
}
54-
55-
describe("Testing Schema Proxy", () => {
64+
$ref: 'https://example.com/schemas/example#/properties',
65+
zap: {
66+
type: 'boolean',
67+
},
68+
},
69+
};
5670

57-
it("Schema Proxy creates a JSON schema", () => {
71+
describe('Testing Schema Proxy', () => {
72+
it('Schema Proxy creates a JSON schema', () => {
5873
const proxied = loader()(example, 'example.schema.json');
5974

60-
assert.equal(proxied.title, "Example");
61-
assert.equal(proxied.properties.foo.type, "string");
75+
assert.equal(proxied.title, 'Example');
76+
assert.equal(proxied.properties.foo.type, 'string');
6277
});
6378

64-
it("Schema Proxy loads multiple JSON schemas", () => {
79+
it('Schema Proxy loads multiple JSON schemas', () => {
6580
const myloader = loader();
6681
const proxied1 = myloader(example, 'example.schema.json');
6782
const proxied2 = myloader(referencing, 'referencing.schema.json');
6883

69-
assert.equal(proxied1.title, "Example");
70-
assert.equal(proxied2.$id, "https://example.com/schemas/referencing");
71-
84+
assert.equal(proxied1.title, 'Example');
85+
assert.equal(proxied2.$id, 'https://example.com/schemas/referencing');
7286
});
7387

74-
it("Schema Proxy creates a JSON schema with Pointers", () => {
88+
it('Schema Proxy creates a JSON schema with Pointers', () => {
7589
const proxied = loader()(example, 'example.schema.json');
7690

77-
assert.equal(proxied[pointer], "");
78-
assert.equal(proxied.properties[pointer], "/properties");
79-
assert.equal(proxied.properties.foo[pointer], "/properties/foo");
80-
assert.equal(proxied['meta:license'][pointer], "/meta:license");
81-
assert.equal(proxied.properties.baz.anyOf[0][pointer], "/properties/baz/anyOf/0");
91+
assert.equal(proxied[pointer], '');
92+
assert.equal(proxied.properties[pointer], '/properties');
93+
assert.equal(proxied.properties.foo[pointer], '/properties/foo');
94+
assert.equal(proxied['meta:license'][pointer], '/meta:license');
95+
assert.equal(proxied.properties.baz.anyOf[0][pointer], '/properties/baz/anyOf/0');
8296
});
8397

84-
it("Schema Proxy creates a JSON schema with ID References", () => {
98+
it('Schema Proxy creates a JSON schema with ID References', () => {
8599
const proxied = loader()(example, 'example.schema.json');
86100

87-
assert.equal(proxied[id], "https://example.com/schemas/example");
88-
assert.equal(proxied.properties[pointer], "/properties");
101+
assert.equal(proxied[id], 'https://example.com/schemas/example');
102+
assert.equal(proxied.properties[pointer], '/properties');
89103

90-
assert.equal(proxied.properties[id], "https://example.com/schemas/example");
91-
assert.equal(proxied.properties[pointer], "/properties");
104+
assert.equal(proxied.properties[id], 'https://example.com/schemas/example');
105+
assert.equal(proxied.properties[pointer], '/properties');
92106

93-
assert.equal(proxied.properties.foo[id], "https://example.com/schemas/example");
94-
assert.equal(proxied['meta:license'][id], "https://example.com/schemas/example");
95-
assert.equal(proxied.properties.baz.anyOf[0][id], "https://example.com/schemas/example");
107+
assert.equal(proxied.properties.foo[id], 'https://example.com/schemas/example');
108+
assert.equal(proxied['meta:license'][id], 'https://example.com/schemas/example');
109+
assert.equal(proxied.properties.baz.anyOf[0][id], 'https://example.com/schemas/example');
96110
});
97111

98-
it("Schema Proxy creates a JSON schema with Filename References", () => {
112+
it('Schema Proxy creates a JSON schema with Filename References', () => {
99113
const proxied = loader()(example, 'example.schema.json');
100114

101-
assert.equal(proxied[filename], "example.schema.json");
102-
assert.equal(proxied.properties[filename], "example.schema.json");
103-
assert.equal(proxied.properties.foo[filename], "example.schema.json");
104-
assert.equal(proxied['meta:license'][filename], "example.schema.json");
105-
assert.equal(proxied.properties.baz.anyOf[0][filename], "example.schema.json");
115+
assert.equal(proxied[filename], 'example.schema.json');
116+
assert.equal(proxied.properties[filename], 'example.schema.json');
117+
assert.equal(proxied.properties.foo[filename], 'example.schema.json');
118+
assert.equal(proxied['meta:license'][filename], 'example.schema.json');
119+
assert.equal(proxied.properties.baz.anyOf[0][filename], 'example.schema.json');
106120
});
107121

108-
it("Schema Proxy creates a JSON schema with title References", () => {
122+
it('Schema Proxy creates a JSON schema with title References', () => {
109123
const proxied = loader()(example, 'example.schema.json');
110124

111125
assert.deepStrictEqual(proxied[titles], ['Example']);
112126
assert.deepStrictEqual(proxied.properties.zip[titles], ['Example', undefined, 'An object']);
113127
});
114128

115-
129+
it('Schema proxy resolves JSON Pointers', () => {
130+
const myloader = loader();
131+
const proxied1 = myloader(example, 'example.schema.json');
132+
133+
assert.deepStrictEqual(proxied1.properties, proxied1[resolve]('/properties'));
134+
assert.deepStrictEqual(proxied1.properties.foo, proxied1[resolve]('/properties/foo'));
135+
assert.deepStrictEqual(proxied1.properties.foo, proxied1.properties[resolve]('/foo'));
136+
});
137+
138+
it('Schema proxy resolves Reference Pointers', () => {
139+
const myloader = loader();
140+
myloader(example, 'example.schema.json');
141+
const proxied2 = myloader(referencing, 'referencing.schema.json');
142+
143+
assert.deepStrictEqual(new Set(Object.keys(proxied2.properties)), new Set([
144+
'$ref', 'zap', // the two properties from the original declaration
145+
'foo', 'bar', 'zip', 'baz', // plus all the referenced properties
146+
]));
147+
});
116148
});

0 commit comments

Comments
 (0)