/
root.js
248 lines (226 loc) · 7.85 KB
/
root.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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"use strict";
module.exports = Root;
var Namespace = require("./namespace");
/** @alias Root.prototype */
var RootPrototype = Namespace.extend(Root);
var Field = require("./field"),
util = require("./util"),
common = require("./common");
/**
* Constructs a new root namespace.
* @classdesc Root namespace wrapping all types, enums, services, sub-namespaces etc. that belong together.
* @extends Namespace
* @constructor
* @param {Object} [options] Top level options
*/
function Root(options) {
Namespace.call(this, "", options);
/**
* Deferred extension fields.
* @type {Field[]}
*/
this.deferred = [];
/**
* Resolved file names of loaded files.
* @type {string[]}
*/
this.files = [];
}
/**
* Loads a JSON definition into a root namespace.
* @param {*} json JSON definition
* @param {Root} [root] Root namespace, defaults to create a new one if omitted
* @returns {Root} Root namespace
*/
Root.fromJSON = function fromJSON(json, root) {
if (!root)
root = new Root();
return root.setOptions(json.options).addJSON(json.nested);
};
/**
* Resolves the path of an imported file, relative to the importing origin.
* This method exists so you can override it with your own logic in case your imports are scattered over multiple directories.
* @function
* @param {string} origin The file name of the importing file
* @param {string} target The file name being imported
* @returns {string} Resolved path to `target`
*/
RootPrototype.resolvePath = util.resolvePath;
/**
* Loads one or multiple .proto or preprocessed .json files into this root namespace and calls the callback.
* @param {string|string[]} filename Names of one or multiple files to load
* @param {function(?Error, Root=)} callback Node-style callback function
* @returns {undefined}
* @throws {TypeError} If arguments are invalid
*/
RootPrototype.load = function load(filename, callback) {
var self = this;
if (!callback)
return util.asPromise(load, self, filename);
// Finishes loading by calling the callback (exactly once)
function finish(err, root) {
if (!callback)
return;
var cb = callback;
callback = null;
cb(err, root);
}
// Processes a single file
function process(filename, source) {
try {
if (util.isString(source) && source.charAt(0) === "{")
source = JSON.parse(source);
if (!util.isString(source))
self.setOptions(source.options).addJSON(source.nested);
else {
var parsed = require("./parse")(source, self);
if (parsed.imports)
parsed.imports.forEach(function(name) {
fetch(self.resolvePath(filename, name));
});
if (parsed.weakImports)
parsed.weakImports.forEach(function(name) {
fetch(self.resolvePath(filename, name), true);
});
}
} catch (err) {
finish(err);
return;
}
if (!queued)
finish(null, self);
}
// Fetches a single file
function fetch(filename, weak) {
// Strip path if this file references a bundled definition
var idx = filename.indexOf("google/protobuf/");
if (idx > -1) {
var altname = filename.substring(idx);
if (altname in common)
filename = altname;
}
// Skip if already loaded
if (self.files.indexOf(filename) > -1)
return;
self.files.push(filename);
// Shortcut bundled definitions
if (filename in common) {
++queued;
setTimeout(function() {
--queued;
process(filename, common[filename]);
});
return;
}
// Otherwise fetch from disk or network
++queued;
util.fetch(filename, function(err, source) {
--queued;
if (!callback)
return; // terminated meanwhile
if (err) {
if (!weak)
finish(err);
return;
}
process(filename, source);
});
}
var queued = 0;
// Assembling the root namespace doesn't require working type
// references anymore, so we can load everything in parallel
if (util.isString(filename))
filename = [ filename ];
filename.forEach(function(filename) {
fetch(self.resolvePath("", filename));
});
if (!queued)
finish(null);
return undefined;
};
// function load(filename:string, callback:function):undefined
/**
* Loads one or multiple .proto or preprocessed .json files into this root namespace and returns a promise.
* @name Root#load
* @function
* @param {string|string[]} filename Names of one or multiple files to load
* @returns {Promise<Root>} Promise
* @throws {TypeError} If arguments are invalid
* @variation 2
*/
// function load(filename:string):Promise<Root>
/**
* Handles a deferred declaring extension field by creating a sister field to represent it within its extended type.
* @param {Field} field Declaring extension field witin the declaring type
* @returns {boolean} `true` if successfully added to the extended type, `false` otherwise
* @inner
* @ignore
*/
function handleExtension(field) {
var extendedType = field.parent.lookup(field.extend);
if (extendedType) {
var sisterField = new Field(field.getFullName(), field.id, field.type, field.rule, undefined, field.options);
sisterField.declaringField = field;
field.extensionField = sisterField;
extendedType.add(sisterField);
return true;
}
return false;
}
/**
* Called when any object is added to this root or its sub-namespaces.
* @param {ReflectionObject} object Object added
* @returns {undefined}
* @private
*/
RootPrototype._handleAdd = function handleAdd(object) {
// Try to handle any deferred extensions
var newDeferred = this.deferred.slice();
this.deferred = []; // because the loop calls handleAdd
var i = 0;
while (i < newDeferred.length)
if (handleExtension(newDeferred[i]))
newDeferred.splice(i, 1);
else
++i;
this.deferred = newDeferred;
// Handle new declaring extension fields without a sister field yet
if (object instanceof Field && object.extend !== undefined && !object.extensionField && !handleExtension(object) && this.deferred.indexOf(object) < 0)
this.deferred.push(object);
else if (object instanceof Namespace) {
var nested = object.getNestedArray();
for (i = 0; i < nested.length; ++i) // recurse into the namespace
this._handleAdd(nested[i]);
}
};
/**
* Called when any object is removed from this root or its sub-namespaces.
* @param {ReflectionObject} object Object removed
* @returns {undefined}
* @private
*/
RootPrototype._handleRemove = function handleRemove(object) {
if (object instanceof Field) {
// If a deferred declaring extension field, cancel the extension
if (object.extend !== undefined && !object.extensionField) {
var index = this.deferred.indexOf(object);
if (index > -1)
this.deferred.splice(index, 1);
}
// If a declaring extension field with a sister field, remove its sister field
if (object.extensionField) {
object.extensionField.parent.remove(object.extensionField);
object.extensionField = null;
}
} else if (object instanceof Namespace) {
var nested = object.getNestedArray();
for (var i = 0; i < nested.length; ++i) // recurse into the namespace
this._handleRemove(nested[i]);
}
};
/**
* @override
*/
RootPrototype.toString = function toString() {
return this.constructor.name;
};