| "use strict"; |
| module.exports = Root; |
| |
| // extends Namespace |
| var Namespace = require("./namespace"); |
| ((Root.prototype = Object.create(Namespace.prototype)).constructor = Root).className = "Root"; |
| |
| var Field = require("./field"), |
| Enum = require("./enum"), |
| OneOf = require("./oneof"), |
| util = require("./util"); |
| |
| var Type, // cyclic |
| parse, // might be excluded |
| common; // " |
| |
| /** |
| * Constructs a new root namespace instance. |
| * @classdesc Root namespace wrapping all types, enums, services, sub-namespaces etc. that belong together. |
| * @extends NamespaceBase |
| * @constructor |
| * @param {Object.<string,*>} [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 namespace descriptor into a root namespace. |
| * @param {INamespace} json Nameespace descriptor |
| * @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(); |
| if (json.options) |
| root.setOptions(json.options); |
| return root.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|null} Resolved path to `target` or `null` to skip the file |
| */ |
| Root.prototype.resolvePath = util.path.resolve; |
| |
| /** |
| * Fetch content from file path or url |
| * This method exists so you can override it with your own logic. |
| * @function |
| * @param {string} path File path or url |
| * @param {FetchCallback} callback Callback function |
| * @returns {undefined} |
| */ |
| Root.prototype.fetch = util.fetch; |
| |
| // A symbol-like function to safely signal synchronous loading |
| /* istanbul ignore next */ |
| function SYNC() {} // eslint-disable-line no-empty-function |
| |
| /** |
| * 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 {IParseOptions} options Parse options |
| * @param {LoadCallback} callback Callback function |
| * @returns {undefined} |
| */ |
| Root.prototype.load = function load(filename, options, callback) { |
| if (typeof options === "function") { |
| callback = options; |
| options = undefined; |
| } |
| var self = this; |
| if (!callback) |
| return util.asPromise(load, self, filename, options); |
| |
| var sync = callback === SYNC; // undocumented |
| |
| // Finishes loading by calling the callback (exactly once) |
| function finish(err, root) { |
| /* istanbul ignore if */ |
| if (!callback) |
| return; |
| var cb = callback; |
| callback = null; |
| if (sync) |
| throw err; |
| cb(err, root); |
| } |
| |
| // Bundled definition existence checking |
| function getBundledFileName(filename) { |
| var idx = filename.lastIndexOf("google/protobuf/"); |
| if (idx > -1) { |
| var altname = filename.substring(idx); |
| if (altname in common) return altname; |
| } |
| return null; |
| } |
| |
| // 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 { |
| parse.filename = filename; |
| var parsed = parse(source, self, options), |
| resolved, |
| i = 0; |
| if (parsed.imports) |
| for (; i < parsed.imports.length; ++i) |
| if (resolved = getBundledFileName(parsed.imports[i]) || self.resolvePath(filename, parsed.imports[i])) |
| fetch(resolved); |
| if (parsed.weakImports) |
| for (i = 0; i < parsed.weakImports.length; ++i) |
| if (resolved = getBundledFileName(parsed.weakImports[i]) || self.resolvePath(filename, parsed.weakImports[i])) |
| fetch(resolved, true); |
| } |
| } catch (err) { |
| finish(err); |
| } |
| if (!sync && !queued) |
| finish(null, self); // only once anyway |
| } |
| |
| // Fetches a single file |
| function fetch(filename, weak) { |
| |
| // Skip if already loaded / attempted |
| if (self.files.indexOf(filename) > -1) |
| return; |
| self.files.push(filename); |
| |
| // Shortcut bundled definitions |
| if (filename in common) { |
| if (sync) |
| process(filename, common[filename]); |
| else { |
| ++queued; |
| setTimeout(function() { |
| --queued; |
| process(filename, common[filename]); |
| }); |
| } |
| return; |
| } |
| |
| // Otherwise fetch from disk or network |
| if (sync) { |
| var source; |
| try { |
| source = util.fs.readFileSync(filename).toString("utf8"); |
| } catch (err) { |
| if (!weak) |
| finish(err); |
| return; |
| } |
| process(filename, source); |
| } else { |
| ++queued; |
| self.fetch(filename, function(err, source) { |
| --queued; |
| /* istanbul ignore if */ |
| if (!callback) |
| return; // terminated meanwhile |
| if (err) { |
| /* istanbul ignore else */ |
| if (!weak) |
| finish(err); |
| else if (!queued) // can't be covered reliably |
| finish(null, self); |
| 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 ]; |
| for (var i = 0, resolved; i < filename.length; ++i) |
| if (resolved = self.resolvePath("", filename[i])) |
| fetch(resolved); |
| |
| if (sync) |
| return self; |
| if (!queued) |
| finish(null, self); |
| return undefined; |
| }; |
| // function load(filename:string, options:IParseOptions, callback:LoadCallback):undefined |
| |
| /** |
| * Loads one or multiple .proto or preprocessed .json files into this root namespace and calls the callback. |
| * @function Root#load |
| * @param {string|string[]} filename Names of one or multiple files to load |
| * @param {LoadCallback} callback Callback function |
| * @returns {undefined} |
| * @variation 2 |
| */ |
| // function load(filename:string, callback:LoadCallback):undefined |
| |
| /** |
| * Loads one or multiple .proto or preprocessed .json files into this root namespace and returns a promise. |
| * @function Root#load |
| * @param {string|string[]} filename Names of one or multiple files to load |
| * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted. |
| * @returns {Promise<Root>} Promise |
| * @variation 3 |
| */ |
| // function load(filename:string, [options:IParseOptions]):Promise<Root> |
| |
| /** |
| * Synchronously loads one or multiple .proto or preprocessed .json files into this root namespace (node only). |
| * @function Root#loadSync |
| * @param {string|string[]} filename Names of one or multiple files to load |
| * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted. |
| * @returns {Root} Root namespace |
| * @throws {Error} If synchronous fetching is not supported (i.e. in browsers) or if a file's syntax is invalid |
| */ |
| Root.prototype.loadSync = function loadSync(filename, options) { |
| if (!util.isNode) |
| throw Error("not supported"); |
| return this.load(filename, options, SYNC); |
| }; |
| |
| /** |
| * @override |
| */ |
| Root.prototype.resolveAll = function resolveAll() { |
| if (this.deferred.length) |
| throw Error("unresolvable extensions: " + this.deferred.map(function(field) { |
| return "'extend " + field.extend + "' in " + field.parent.fullName; |
| }).join(", ")); |
| return Namespace.prototype.resolveAll.call(this); |
| }; |
| |
| // only uppercased (and thus conflict-free) children are exposed, see below |
| var exposeRe = /^[A-Z]/; |
| |
| /** |
| * Handles a deferred declaring extension field by creating a sister field to represent it within its extended type. |
| * @param {Root} root Root instance |
| * @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 tryHandleExtension(root, field) { |
| var extendedType = field.parent.lookup(field.extend); |
| if (extendedType) { |
| var sisterField = new Field(field.fullName, 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 |
| */ |
| Root.prototype._handleAdd = function _handleAdd(object) { |
| if (object instanceof Field) { |
| |
| if (/* an extension field (implies not part of a oneof) */ object.extend !== undefined && /* not already handled */ !object.extensionField) |
| if (!tryHandleExtension(this, object)) |
| this.deferred.push(object); |
| |
| } else if (object instanceof Enum) { |
| |
| if (exposeRe.test(object.name)) |
| object.parent[object.name] = object.values; // expose enum values as property of its parent |
| |
| } else if (!(object instanceof OneOf)) /* everything else is a namespace */ { |
| |
| if (object instanceof Type) // Try to handle any deferred extensions |
| for (var i = 0; i < this.deferred.length;) |
| if (tryHandleExtension(this, this.deferred[i])) |
| this.deferred.splice(i, 1); |
| else |
| ++i; |
| for (var j = 0; j < /* initializes */ object.nestedArray.length; ++j) // recurse into the namespace |
| this._handleAdd(object._nestedArray[j]); |
| if (exposeRe.test(object.name)) |
| object.parent[object.name] = object; // expose namespace as property of its parent |
| } |
| |
| // The above also adds uppercased (and thus conflict-free) nested types, services and enums as |
| // properties of namespaces just like static code does. This allows using a .d.ts generated for |
| // a static module with reflection-based solutions where the condition is met. |
| }; |
| |
| /** |
| * Called when any object is removed from this root or its sub-namespaces. |
| * @param {ReflectionObject} object Object removed |
| * @returns {undefined} |
| * @private |
| */ |
| Root.prototype._handleRemove = function _handleRemove(object) { |
| if (object instanceof Field) { |
| |
| if (/* an extension field */ object.extend !== undefined) { |
| if (/* already handled */ object.extensionField) { // remove its sister field |
| object.extensionField.parent.remove(object.extensionField); |
| object.extensionField = null; |
| } else { // cancel the extension |
| var index = this.deferred.indexOf(object); |
| /* istanbul ignore else */ |
| if (index > -1) |
| this.deferred.splice(index, 1); |
| } |
| } |
| |
| } else if (object instanceof Enum) { |
| |
| if (exposeRe.test(object.name)) |
| delete object.parent[object.name]; // unexpose enum values |
| |
| } else if (object instanceof Namespace) { |
| |
| for (var i = 0; i < /* initializes */ object.nestedArray.length; ++i) // recurse into the namespace |
| this._handleRemove(object._nestedArray[i]); |
| |
| if (exposeRe.test(object.name)) |
| delete object.parent[object.name]; // unexpose namespaces |
| |
| } |
| }; |
| |
| // Sets up cyclic dependencies (called in index-light) |
| Root._configure = function(Type_, parse_, common_) { |
| Type = Type_; |
| parse = parse_; |
| common = common_; |
| }; |