; | |
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; | |
if (sync) | |
throw err; | |
var cb = callback; | |
callback = null; | |
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) { | |
filename = getBundledFileName(filename) || filename; | |
// 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); | |
//do not allow to extend same field twice to prevent the error | |
if (extendedType.get(sisterField.name)) { | |
return true; | |
} | |
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_; | |
}; | |