-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
195 lines (182 loc) · 6.2 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/* eslint-env node */
/* eslint no-sync: 0 */
"use strict";
// Include external dependencies
var fs = require('fs');
var path = require('path');
var yaml = require('js-yaml');
var clone = require('clone');
// Include local modules
// Setup
/**
* Generate a schema from a .yaml file
*
* References are resolved using the component before the '#' in a '$ref'
* property, using that component as the file name, searching the directory of
* the .yaml file generating the schema. Subschemas definitions are compiled
* into the top level schema, preventing circular references and nested and
* duplicated schemas
*/
class Generator {
/**
* Constructor
*
* @param {object} schema JSON schema object
* @param {object|null} topLevelSchema Schema to compile references into
* @param {string} subpath Path to current schema from top level
* @param {string} schemaDir Directory to find schema files in
*/
constructor(schema, topLevelSchema, subpath, schemaDir) {
this.schemaDir = schemaDir;
// Keep a list of references and their replacements
this.references = {};
// Clone it so the original object isn't modified (useful if used elsewhere)
this.compiled = clone(schema);
// If no top level schema specified, this is the top level
this.topLevelSchema = this.compiled;
if (topLevelSchema) {
this.topLevelSchema = topLevelSchema;
}
this.references[schema.shortName] = '#';
if (subpath) {
this.references[schema.shortName] += subpath;
}
// preserve uncompiled schema?
this.original = schema;
}
resolveReferences(callback) {
recursiveMap(this.compiled, [], (stack, key, value) => {
// 'this' bound to most recently called context. arrow function
// preserves it for when called outside the class
this.compile(stack, key, value);
});
//log({compiled: this.compiled});
callback(null, this.compiled);
}
/**
* Resolve references to external schemas and compile them into the top
* level schema
*
* @param {array} stack
* @param {string} key
* @param {mixed} value
*/
compile(stack, key, value) {
if (key === "$ref") {
let reference = parseReference(value);
if (reference.object === '#') {
// references self. don't allow this, force absolute references
let e = new Error("Found reference to self at '" + stack.join(".") + "'. Use object id prefix for all references");
e.detail = reference;
throw e;
} else if (this.references[reference.object]) {
// Reference is already compiled into main schema
// Replace reference to external schema with one to local definition
this.replaceReference(stack, reference);
} else {
// Fetch reference and compile into main schema
let data = fs.readFileSync(path.join(this.schemaDir, reference.object + ".yaml"));
let schema = yaml.safeLoad(data);
//this.compiled.definitions[reference.object] = schema; //TODO replace with compiled schema
let generator = new Generator(schema, this.topLevelSchema, "/definitions/" + schema.shortName, this.schemaDir);
generator.resolveReferences((err, compiled) => {
// Add compiled schema to definitions of top level schema
if (!this.topLevelSchema.definitions) {
this.topLevelSchema.definitions = {};
}
// Remove subschema's id, as it is now part of the compiled schema
delete compiled.id;
this.topLevelSchema.definitions[reference.object] = compiled;
// Store new path in list of resolved references
this.references[schema.shortName] = "#/definitions/" + schema.shortName;
// Replace reference to external schema with one to local definition
this.replaceReference(stack, reference);
});
}
}
}
/**
* Replace string reference to external schema with one pointing to local definition
*
* @param {string[]} stack Properties path to reference
* @param {object} reference Parsed reference
*/
replaceReference(stack, reference) {
let updatable = this.compiled;
for (let i = 0; i < stack.length; i++) {
updatable = updatable[stack[i]];
}
//updatable.$ref = "#/definitions/" + reference.object;
updatable.$ref = this.references[reference.object];
if (reference.path) {
updatable.$ref += reference.path;
}
}
}
/**
* Send each key/value pair to a function
*
* @param {object} object Object to recurse
* @param {array} stack Path to current node in object
* @param {function} fn Function to send key/values to
*/
function recursiveMap(object, stack, fn) {
for (let i in object) {
if (object.hasOwnProperty(i)) {
if (typeof object[i] === "object") {
let newStack = clone(stack);
newStack.push(i);
recursiveMap(object[i], newStack, fn);
} else {
fn(stack, i, object[i]);
}
}
}
}
/**
* Parse a reference into component parts and properties
*
* @param {string} value
*
* @returns {object}
*/
function parseReference(value) {
var parsed = {
original: value
};
var parts = value.split('#');
parsed.parts = parts;
if (parts[0] === "" && parts.length === 2) {
// Reference self
parsed.object = "#";
parsed.path = parts[1];
} else if (parts.length === 1) {
// Reference external top level object
parsed.object = parts[0];
} else if (parts.length === 2) {
// Reference external object's sub-object
parsed.object = parts[0];
parsed.path = parts[1];
} else {
// More than one '#' found. This shouldn't happen if yaml files are valid
throw new Error("More than one '#' found in reference value: " + value);
}
return parsed;
}
//function log() {
//console.dir.call(null, ...arguments, {depth: null});
//}
// Public
module.exports = {
generate: function generate(fileName, callback) {
fs.readFile(fileName, function onFileRead(err, data) {
if (err) {
return callback(err);
}
var schema = yaml.safeLoad(data);
var schemaDir = path.dirname(fileName);
var generator = new Generator(schema, null, null, schemaDir);
return generator.resolveReferences(callback);
});
}
};