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