protobufjs-no-cli
Version:
Protocol Buffers for JavaScript. Finally.
701 lines (664 loc) • 20.2 kB
JavaScript
/*?
// --- Scope ----------------------
// Lang : Language expressions
// Tokenizer : DotProto Tokenizer
*/
/**
* Constructs a new Parser.
* @exports ProtoBuf.DotProto.Parser
* @class prototype parser
* @param {string} source Source
* @constructor
*/
var Parser = function(source) {
/**
* Tokenizer.
* @type {!ProtoBuf.DotProto.Tokenizer}
* @expose
*/
this.tn = new Tokenizer(source);
/**
* Whether parsing proto3 or not.
* @type {boolean}
*/
this.proto3 = false;
};
/**
* @alias ProtoBuf.DotProto.Parser.prototype
* @inner
*/
var ParserPrototype = Parser.prototype;
/**
* Parses the source.
* @returns {!Object}
* @throws {Error} If the source cannot be parsed
* @expose
*/
ParserPrototype.parse = function() {
var topLevel = {
"name": "[ROOT]", // temporary
"package": null,
"messages": [],
"enums": [],
"imports": [],
"options": {},
"services": []
// "syntax": undefined
};
var token,
head = true,
weak;
try {
while (token = this.tn.next()) {
switch (token) {
case 'package':
if (!head || topLevel["package"] !== null)
throw Error("unexpected 'package'");
token = this.tn.next();
if (!Lang.TYPEREF.test(token))
throw Error("illegal package name: " + token);
this.tn.skip(";");
topLevel["package"] = token;
break;
case 'import':
if (!head)
throw Error("unexpected 'import'");
token = this.tn.peek();
if (token === "public" || (weak = token === "weak")) // token ignored
this.tn.next();
token = this._readString();
this.tn.skip(";");
if (!weak) // import ignored
topLevel["imports"].push(token);
break;
case 'syntax':
if (!head)
throw Error("unexpected 'syntax'");
this.tn.skip("=");
if ((topLevel["syntax"] = this._readString()) === "proto3")
this.proto3 = true;
this.tn.skip(";");
break;
case 'message':
this._parseMessage(topLevel, null);
head = false;
break;
case 'enum':
this._parseEnum(topLevel);
head = false;
break;
case 'option':
this._parseOption(topLevel);
break;
case 'service':
this._parseService(topLevel);
break;
case 'extend':
this._parseExtend(topLevel);
break;
default:
throw Error("unexpected '" + token + "'");
}
}
} catch (e) {
e.message = "Parse error at line "+this.tn.line+": " + e.message;
throw e;
}
delete topLevel["name"];
return topLevel;
};
/**
* Parses the specified source.
* @returns {!Object}
* @throws {Error} If the source cannot be parsed
* @expose
*/
Parser.parse = function(source) {
return new Parser(source).parse();
};
// ----- Conversion ------
/**
* Converts a numerical string to an id.
* @param {string} value
* @param {boolean=} mayBeNegative
* @returns {number}
* @inner
*/
function mkId(value, mayBeNegative) {
var id = -1,
sign = 1;
if (value.charAt(0) == '-') {
sign = -1;
value = value.substring(1);
}
if (Lang.NUMBER_DEC.test(value))
id = parseInt(value);
else if (Lang.NUMBER_HEX.test(value))
id = parseInt(value.substring(2), 16);
else if (Lang.NUMBER_OCT.test(value))
id = parseInt(value.substring(1), 8);
else
throw Error("illegal id value: " + (sign < 0 ? '-' : '') + value);
id = (sign*id)|0; // Force to 32bit
if (!mayBeNegative && id < 0)
throw Error("illegal id value: " + (sign < 0 ? '-' : '') + value);
return id;
}
/**
* Converts a numerical string to a number.
* @param {string} val
* @returns {number}
* @inner
*/
function mkNumber(val) {
var sign = 1;
if (val.charAt(0) == '-') {
sign = -1;
val = val.substring(1);
}
if (Lang.NUMBER_DEC.test(val))
return sign * parseInt(val, 10);
else if (Lang.NUMBER_HEX.test(val))
return sign * parseInt(val.substring(2), 16);
else if (Lang.NUMBER_OCT.test(val))
return sign * parseInt(val.substring(1), 8);
else if (val === 'inf')
return sign * Infinity;
else if (val === 'nan')
return NaN;
else if (Lang.NUMBER_FLT.test(val))
return sign * parseFloat(val);
throw Error("illegal number value: " + (sign < 0 ? '-' : '') + val);
}
// ----- Reading ------
/**
* Reads a string.
* @returns {string}
* @private
*/
ParserPrototype._readString = function() {
var value = "",
token,
delim;
do {
delim = this.tn.next();
if (delim !== "'" && delim !== '"')
throw Error("illegal string delimiter: "+delim);
value += this.tn.next();
this.tn.skip(delim);
token = this.tn.peek();
} while (token === '"' || token === '"'); // multi line?
return value;
};
/**
* Reads a value.
* @param {boolean=} mayBeTypeRef
* @returns {number|boolean|string}
* @private
*/
ParserPrototype._readValue = function(mayBeTypeRef) {
var token = this.tn.peek(),
value;
if (token === '"' || token === "'")
return this._readString();
this.tn.next();
if (Lang.NUMBER.test(token))
return mkNumber(token);
if (Lang.BOOL.test(token))
return (token.toLowerCase() === 'true');
if (mayBeTypeRef && Lang.TYPEREF.test(token))
return token;
throw Error("illegal value: "+token);
};
// ----- Parsing constructs -----
/**
* Parses a namespace option.
* @param {!Object} parent Parent definition
* @param {boolean=} isList
* @private
*/
ParserPrototype._parseOption = function(parent, isList) {
var token = this.tn.next(),
custom = false;
if (token === '(') {
custom = true;
token = this.tn.next();
}
if (!Lang.TYPEREF.test(token))
// we can allow options of the form google.protobuf.* since they will just get ignored anyways
// if (!/google\.protobuf\./.test(token)) // FIXME: Why should that not be a valid typeref?
throw Error("illegal option name: "+token);
var name = token;
if (custom) { // (my_method_option).foo, (my_method_option), some_method_option, (foo.my_option).bar
this.tn.skip(')');
name = '('+name+')';
token = this.tn.peek();
if (Lang.FQTYPEREF.test(token)) {
name += token;
this.tn.next();
}
}
this.tn.skip('=');
this._parseOptionValue(parent, name);
if (!isList)
this.tn.skip(";");
};
/**
* Sets an option on the specified options object.
* @param {!Object.<string,*>} options
* @param {string} name
* @param {string|number|boolean} value
* @inner
*/
function setOption(options, name, value) {
if (typeof options[name] === 'undefined')
options[name] = value;
else {
if (!Array.isArray(options[name]))
options[name] = [ options[name] ];
options[name].push(value);
}
}
/**
* Parses an option value.
* @param {!Object} parent
* @param {string} name
* @private
*/
ParserPrototype._parseOptionValue = function(parent, name) {
var token = this.tn.peek();
if (token !== '{') { // Plain value
setOption(parent["options"], name, this._readValue(true));
} else { // Aggregate options
this.tn.skip("{");
while ((token = this.tn.next()) !== '}') {
if (!Lang.NAME.test(token))
throw Error("illegal option name: " + name + "." + token);
if (this.tn.omit(":"))
setOption(parent["options"], name + "." + token, this._readValue(true));
else
this._parseOptionValue(parent, name + "." + token);
}
}
};
/**
* Parses a service definition.
* @param {!Object} parent Parent definition
* @private
*/
ParserPrototype._parseService = function(parent) {
var token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal service name at line "+this.tn.line+": "+token);
var name = token;
var svc = {
"name": name,
"rpc": {},
"options": {}
};
this.tn.skip("{");
while ((token = this.tn.next()) !== '}') {
if (token === "option")
this._parseOption(svc);
else if (token === 'rpc')
this._parseServiceRPC(svc);
else
throw Error("illegal service token: "+token);
}
this.tn.omit(";");
parent["services"].push(svc);
};
/**
* Parses a RPC service definition of the form ['rpc', name, (request), 'returns', (response)].
* @param {!Object} svc Service definition
* @private
*/
ParserPrototype._parseServiceRPC = function(svc) {
var type = "rpc",
token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal rpc service method name: "+token);
var name = token;
var method = {
"request": null,
"response": null,
"request_stream": false,
"response_stream": false,
"options": {}
};
this.tn.skip("(");
token = this.tn.next();
if (token.toLowerCase() === "stream") {
method["request_stream"] = true;
token = this.tn.next();
}
if (!Lang.TYPEREF.test(token))
throw Error("illegal rpc service request type: "+token);
method["request"] = token;
this.tn.skip(")");
token = this.tn.next();
if (token.toLowerCase() !== "returns")
throw Error("illegal rpc service request type delimiter: "+token);
this.tn.skip("(");
token = this.tn.next();
if (token.toLowerCase() === "stream") {
method["response_stream"] = true;
token = this.tn.next();
}
method["response"] = token;
this.tn.skip(")");
token = this.tn.peek();
if (token === '{') {
this.tn.next();
while ((token = this.tn.next()) !== '}') {
if (token === 'option')
this._parseOption(method);
else
throw Error("illegal rpc service token: " + token);
}
this.tn.omit(";");
} else
this.tn.skip(";");
if (typeof svc[type] === 'undefined')
svc[type] = {};
svc[type][name] = method;
};
/**
* Parses a message definition.
* @param {!Object} parent Parent definition
* @param {!Object=} fld Field definition if this is a group
* @returns {!Object}
* @private
*/
ParserPrototype._parseMessage = function(parent, fld) {
var isGroup = !!fld,
token = this.tn.next();
var msg = {
"name": "",
"fields": [],
"enums": [],
"messages": [],
"options": {},
"services": [],
"oneofs": {}
// "extensions": undefined
};
if (!Lang.NAME.test(token))
throw Error("illegal "+(isGroup ? "group" : "message")+" name: "+token);
msg["name"] = token;
if (isGroup) {
this.tn.skip("=");
fld["id"] = mkId(this.tn.next());
msg["isGroup"] = true;
}
token = this.tn.peek();
if (token === '[' && fld)
this._parseFieldOptions(fld);
this.tn.skip("{");
while ((token = this.tn.next()) !== '}') {
if (Lang.RULE.test(token))
this._parseMessageField(msg, token);
else if (token === "oneof")
this._parseMessageOneOf(msg);
else if (token === "enum")
this._parseEnum(msg);
else if (token === "message")
this._parseMessage(msg);
else if (token === "option")
this._parseOption(msg);
else if (token === "service")
this._parseService(msg);
else if (token === "extensions")
if (msg.hasOwnProperty("extensions")) {
msg["extensions"] = msg["extensions"].concat(this._parseExtensionRanges())
} else {
msg["extensions"] = this._parseExtensionRanges();
}
else if (token === "reserved")
this._parseIgnored(); // TODO
else if (token === "extend")
this._parseExtend(msg);
else if (Lang.TYPEREF.test(token)) {
if (!this.proto3)
throw Error("illegal field rule: "+token);
this._parseMessageField(msg, "optional", token);
} else
throw Error("illegal message token: "+token);
}
this.tn.omit(";");
parent["messages"].push(msg);
return msg;
};
/**
* Parses an ignored statement.
* @private
*/
ParserPrototype._parseIgnored = function() {
while (this.tn.peek() !== ';')
this.tn.next();
this.tn.skip(";");
};
/**
* Parses a message field.
* @param {!Object} msg Message definition
* @param {string} rule Field rule
* @param {string=} type Field type if already known (never known for maps)
* @returns {!Object} Field descriptor
* @private
*/
ParserPrototype._parseMessageField = function(msg, rule, type) {
if (!Lang.RULE.test(rule))
throw Error("illegal message field rule: "+rule);
var fld = {
"rule": rule,
"type": "",
"name": "",
"options": {},
"id": 0
};
var token;
if (rule === "map") {
if (type)
throw Error("illegal type: " + type);
this.tn.skip('<');
token = this.tn.next();
if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token))
throw Error("illegal message field type: " + token);
fld["keytype"] = token;
this.tn.skip(',');
token = this.tn.next();
if (!Lang.TYPE.test(token) && !Lang.TYPEREF.test(token))
throw Error("illegal message field: " + token);
fld["type"] = token;
this.tn.skip('>');
token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal message field name: " + token);
fld["name"] = token;
this.tn.skip("=");
fld["id"] = mkId(this.tn.next());
token = this.tn.peek();
if (token === '[')
this._parseFieldOptions(fld);
this.tn.skip(";");
} else {
type = typeof type !== 'undefined' ? type : this.tn.next();
if (type === "group") {
// "A [legacy] group simply combines a nested message type and a field into a single declaration. In your
// code, you can treat this message just as if it had a Result type field called result (the latter name is
// converted to lower-case so that it does not conflict with the former)."
var grp = this._parseMessage(msg, fld);
if (!/^[A-Z]/.test(grp["name"]))
throw Error('illegal group name: '+grp["name"]);
fld["type"] = grp["name"];
fld["name"] = grp["name"].toLowerCase();
this.tn.omit(";");
} else {
if (!Lang.TYPE.test(type) && !Lang.TYPEREF.test(type))
throw Error("illegal message field type: " + type);
fld["type"] = type;
token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal message field name: " + token);
fld["name"] = token;
this.tn.skip("=");
fld["id"] = mkId(this.tn.next());
token = this.tn.peek();
if (token === "[")
this._parseFieldOptions(fld);
this.tn.skip(";");
}
}
msg["fields"].push(fld);
return fld;
};
/**
* Parses a message oneof.
* @param {!Object} msg Message definition
* @private
*/
ParserPrototype._parseMessageOneOf = function(msg) {
var token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal oneof name: "+token);
var name = token,
fld;
var fields = [];
this.tn.skip("{");
while ((token = this.tn.next()) !== "}") {
fld = this._parseMessageField(msg, "optional", token);
fld["oneof"] = name;
fields.push(fld["id"]);
}
this.tn.omit(";");
msg["oneofs"][name] = fields;
};
/**
* Parses a set of field option definitions.
* @param {!Object} fld Field definition
* @private
*/
ParserPrototype._parseFieldOptions = function(fld) {
this.tn.skip("[");
var token,
first = true;
while ((token = this.tn.peek()) !== ']') {
if (!first)
this.tn.skip(",");
this._parseOption(fld, true);
first = false;
}
this.tn.next();
};
/**
* Parses an enum.
* @param {!Object} msg Message definition
* @private
*/
ParserPrototype._parseEnum = function(msg) {
var enm = {
"name": "",
"values": [],
"options": {}
};
var token = this.tn.next();
if (!Lang.NAME.test(token))
throw Error("illegal name: "+token);
enm["name"] = token;
this.tn.skip("{");
while ((token = this.tn.next()) !== '}') {
if (token === "option")
this._parseOption(enm);
else {
if (!Lang.NAME.test(token))
throw Error("illegal name: "+token);
this.tn.skip("=");
var val = {
"name": token,
"id": mkId(this.tn.next(), true)
};
token = this.tn.peek();
if (token === "[")
this._parseFieldOptions({ "options": {} });
this.tn.skip(";");
enm["values"].push(val);
}
}
this.tn.omit(";");
msg["enums"].push(enm);
};
/**
* Parses extension / reserved ranges.
* @returns {!Array.<!Array.<number>>}
* @private
*/
ParserPrototype._parseExtensionRanges = function() {
var ranges = [];
var token,
range,
value;
do {
range = [];
while (true) {
token = this.tn.next();
switch (token) {
case "min":
value = ProtoBuf.ID_MIN;
break;
case "max":
value = ProtoBuf.ID_MAX;
break;
default:
value = mkNumber(token);
break;
}
range.push(value);
if (range.length === 2)
break;
if (this.tn.peek() !== "to") {
range.push(value);
break;
}
this.tn.next();
}
ranges.push(range);
} while (this.tn.omit(","));
this.tn.skip(";");
return ranges;
};
/**
* Parses an extend block.
* @param {!Object} parent Parent object
* @private
*/
ParserPrototype._parseExtend = function(parent) {
var token = this.tn.next();
if (!Lang.TYPEREF.test(token))
throw Error("illegal extend reference: "+token);
var ext = {
"ref": token,
"fields": []
};
this.tn.skip("{");
while ((token = this.tn.next()) !== '}') {
if (Lang.RULE.test(token))
this._parseMessageField(ext, token);
else if (Lang.TYPEREF.test(token)) {
if (!this.proto3)
throw Error("illegal field rule: "+token);
this._parseMessageField(ext, "optional", token);
} else
throw Error("illegal extend token: "+token);
}
this.tn.omit(";");
parent["messages"].push(ext);
return ext;
};
// ----- General -----
/**
* Returns a string representation of this parser.
* @returns {string}
*/
ParserPrototype.toString = function() {
return "Parser at line "+this.tn.line;
};