| "use strict"; |
| module.exports = parse; |
| |
| parse.filename = null; |
| parse.defaults = { keepCase: false }; |
| |
| var tokenize = require("./tokenize"), |
| Root = require("./root"), |
| Type = require("./type"), |
| Field = require("./field"), |
| MapField = require("./mapfield"), |
| OneOf = require("./oneof"), |
| Enum = require("./enum"), |
| Service = require("./service"), |
| Method = require("./method"), |
| types = require("./types"), |
| util = require("./util"); |
| |
| var base10Re = /^[1-9][0-9]*$/, |
| base10NegRe = /^-?[1-9][0-9]*$/, |
| base16Re = /^0[x][0-9a-fA-F]+$/, |
| base16NegRe = /^-?0[x][0-9a-fA-F]+$/, |
| base8Re = /^0[0-7]+$/, |
| base8NegRe = /^-?0[0-7]+$/, |
| numberRe = /^(?![eE])[0-9]*(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?$/, |
| nameRe = /^[a-zA-Z_][a-zA-Z_0-9]*$/, |
| typeRefRe = /^(?:\.?[a-zA-Z_][a-zA-Z_0-9]*)(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*$/, |
| fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/; |
| |
| /** |
| * Result object returned from {@link parse}. |
| * @interface IParserResult |
| * @property {string|undefined} package Package name, if declared |
| * @property {string[]|undefined} imports Imports, if any |
| * @property {string[]|undefined} weakImports Weak imports, if any |
| * @property {string|undefined} syntax Syntax, if specified (either `"proto2"` or `"proto3"`) |
| * @property {Root} root Populated root instance |
| */ |
| |
| /** |
| * Options modifying the behavior of {@link parse}. |
| * @interface IParseOptions |
| * @property {boolean} [keepCase=false] Keeps field casing instead of converting to camel case |
| * @property {boolean} [alternateCommentMode=false] Recognize double-slash comments in addition to doc-block comments. |
| * @property {boolean} [preferTrailingComment=false] Use trailing comment when both leading comment and trailing comment exist. |
| */ |
| |
| /** |
| * Options modifying the behavior of JSON serialization. |
| * @interface IToJSONOptions |
| * @property {boolean} [keepComments=false] Serializes comments. |
| */ |
| |
| /** |
| * Parses the given .proto source and returns an object with the parsed contents. |
| * @param {string} source Source contents |
| * @param {Root} root Root to populate |
| * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted. |
| * @returns {IParserResult} Parser result |
| * @property {string} filename=null Currently processing file name for error reporting, if known |
| * @property {IParseOptions} defaults Default {@link IParseOptions} |
| */ |
| function parse(source, root, options) { |
| /* eslint-disable callback-return */ |
| if (!(root instanceof Root)) { |
| options = root; |
| root = new Root(); |
| } |
| if (!options) |
| options = parse.defaults; |
| |
| var preferTrailingComment = options.preferTrailingComment || false; |
| var tn = tokenize(source, options.alternateCommentMode || false), |
| next = tn.next, |
| push = tn.push, |
| peek = tn.peek, |
| skip = tn.skip, |
| cmnt = tn.cmnt; |
| |
| var head = true, |
| pkg, |
| imports, |
| weakImports, |
| syntax, |
| isProto3 = false; |
| |
| var ptr = root; |
| |
| var applyCase = options.keepCase ? function(name) { return name; } : util.camelCase; |
| |
| /* istanbul ignore next */ |
| function illegal(token, name, insideTryCatch) { |
| var filename = parse.filename; |
| if (!insideTryCatch) |
| parse.filename = null; |
| return Error("illegal " + (name || "token") + " '" + token + "' (" + (filename ? filename + ", " : "") + "line " + tn.line + ")"); |
| } |
| |
| function readString() { |
| var values = [], |
| token; |
| do { |
| /* istanbul ignore if */ |
| if ((token = next()) !== "\"" && token !== "'") |
| throw illegal(token); |
| |
| values.push(next()); |
| skip(token); |
| token = peek(); |
| } while (token === "\"" || token === "'"); |
| return values.join(""); |
| } |
| |
| function readValue(acceptTypeRef) { |
| var token = next(); |
| switch (token) { |
| case "'": |
| case "\"": |
| push(token); |
| return readString(); |
| case "true": case "TRUE": |
| return true; |
| case "false": case "FALSE": |
| return false; |
| } |
| try { |
| return parseNumber(token, /* insideTryCatch */ true); |
| } catch (e) { |
| |
| /* istanbul ignore else */ |
| if (acceptTypeRef && typeRefRe.test(token)) |
| return token; |
| |
| /* istanbul ignore next */ |
| throw illegal(token, "value"); |
| } |
| } |
| |
| function readRanges(target, acceptStrings) { |
| var token, start; |
| do { |
| if (acceptStrings && ((token = peek()) === "\"" || token === "'")) |
| target.push(readString()); |
| else |
| target.push([ start = parseId(next()), skip("to", true) ? parseId(next()) : start ]); |
| } while (skip(",", true)); |
| skip(";"); |
| } |
| |
| function parseNumber(token, insideTryCatch) { |
| var sign = 1; |
| if (token.charAt(0) === "-") { |
| sign = -1; |
| token = token.substring(1); |
| } |
| switch (token) { |
| case "inf": case "INF": case "Inf": |
| return sign * Infinity; |
| case "nan": case "NAN": case "Nan": case "NaN": |
| return NaN; |
| case "0": |
| return 0; |
| } |
| if (base10Re.test(token)) |
| return sign * parseInt(token, 10); |
| if (base16Re.test(token)) |
| return sign * parseInt(token, 16); |
| if (base8Re.test(token)) |
| return sign * parseInt(token, 8); |
| |
| /* istanbul ignore else */ |
| if (numberRe.test(token)) |
| return sign * parseFloat(token); |
| |
| /* istanbul ignore next */ |
| throw illegal(token, "number", insideTryCatch); |
| } |
| |
| function parseId(token, acceptNegative) { |
| switch (token) { |
| case "max": case "MAX": case "Max": |
| return 536870911; |
| case "0": |
| return 0; |
| } |
| |
| /* istanbul ignore if */ |
| if (!acceptNegative && token.charAt(0) === "-") |
| throw illegal(token, "id"); |
| |
| if (base10NegRe.test(token)) |
| return parseInt(token, 10); |
| if (base16NegRe.test(token)) |
| return parseInt(token, 16); |
| |
| /* istanbul ignore else */ |
| if (base8NegRe.test(token)) |
| return parseInt(token, 8); |
| |
| /* istanbul ignore next */ |
| throw illegal(token, "id"); |
| } |
| |
| function parsePackage() { |
| |
| /* istanbul ignore if */ |
| if (pkg !== undefined) |
| throw illegal("package"); |
| |
| pkg = next(); |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(pkg)) |
| throw illegal(pkg, "name"); |
| |
| ptr = ptr.define(pkg); |
| skip(";"); |
| } |
| |
| function parseImport() { |
| var token = peek(); |
| var whichImports; |
| switch (token) { |
| case "weak": |
| whichImports = weakImports || (weakImports = []); |
| next(); |
| break; |
| case "public": |
| next(); |
| // eslint-disable-line no-fallthrough |
| default: |
| whichImports = imports || (imports = []); |
| break; |
| } |
| token = readString(); |
| skip(";"); |
| whichImports.push(token); |
| } |
| |
| function parseSyntax() { |
| skip("="); |
| syntax = readString(); |
| isProto3 = syntax === "proto3"; |
| |
| /* istanbul ignore if */ |
| if (!isProto3 && syntax !== "proto2") |
| throw illegal(syntax, "syntax"); |
| |
| skip(";"); |
| } |
| |
| function parseCommon(parent, token) { |
| switch (token) { |
| |
| case "option": |
| parseOption(parent, token); |
| skip(";"); |
| return true; |
| |
| case "message": |
| parseType(parent, token); |
| return true; |
| |
| case "enum": |
| parseEnum(parent, token); |
| return true; |
| |
| case "service": |
| parseService(parent, token); |
| return true; |
| |
| case "extend": |
| parseExtension(parent, token); |
| return true; |
| } |
| return false; |
| } |
| |
| function ifBlock(obj, fnIf, fnElse) { |
| var trailingLine = tn.line; |
| if (obj) { |
| if(typeof obj.comment !== "string") { |
| obj.comment = cmnt(); // try block-type comment |
| } |
| obj.filename = parse.filename; |
| } |
| if (skip("{", true)) { |
| var token; |
| while ((token = next()) !== "}") |
| fnIf(token); |
| skip(";", true); |
| } else { |
| if (fnElse) |
| fnElse(); |
| skip(";"); |
| if (obj && (typeof obj.comment !== "string" || preferTrailingComment)) |
| obj.comment = cmnt(trailingLine) || obj.comment; // try line-type comment |
| } |
| } |
| |
| function parseType(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "type name"); |
| |
| var type = new Type(token); |
| ifBlock(type, function parseType_block(token) { |
| if (parseCommon(type, token)) |
| return; |
| |
| switch (token) { |
| |
| case "map": |
| parseMapField(type, token); |
| break; |
| |
| case "required": |
| case "repeated": |
| parseField(type, token); |
| break; |
| |
| case "optional": |
| /* istanbul ignore if */ |
| if (isProto3) { |
| parseField(type, "proto3_optional"); |
| } else { |
| parseField(type, "optional"); |
| } |
| break; |
| |
| case "oneof": |
| parseOneOf(type, token); |
| break; |
| |
| case "extensions": |
| readRanges(type.extensions || (type.extensions = [])); |
| break; |
| |
| case "reserved": |
| readRanges(type.reserved || (type.reserved = []), true); |
| break; |
| |
| default: |
| /* istanbul ignore if */ |
| if (!isProto3 || !typeRefRe.test(token)) |
| throw illegal(token); |
| |
| push(token); |
| parseField(type, "optional"); |
| break; |
| } |
| }); |
| parent.add(type); |
| } |
| |
| function parseField(parent, rule, extend) { |
| var type = next(); |
| if (type === "group") { |
| parseGroup(parent, rule); |
| return; |
| } |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(type)) |
| throw illegal(type, "type"); |
| |
| var name = next(); |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(name)) |
| throw illegal(name, "name"); |
| |
| name = applyCase(name); |
| skip("="); |
| |
| var field = new Field(name, parseId(next()), type, rule, extend); |
| ifBlock(field, function parseField_block(token) { |
| |
| /* istanbul ignore else */ |
| if (token === "option") { |
| parseOption(field, token); |
| skip(";"); |
| } else |
| throw illegal(token); |
| |
| }, function parseField_line() { |
| parseInlineOptions(field); |
| }); |
| |
| if (rule === "proto3_optional") { |
| // for proto3 optional fields, we create a single-member Oneof to mimic "optional" behavior |
| var oneof = new OneOf("_" + name); |
| field.setOption("proto3_optional", true); |
| oneof.add(field); |
| parent.add(oneof); |
| } else { |
| parent.add(field); |
| } |
| |
| // JSON defaults to packed=true if not set so we have to set packed=false explicity when |
| // parsing proto2 descriptors without the option, where applicable. This must be done for |
| // all known packable types and anything that could be an enum (= is not a basic type). |
| if (!isProto3 && field.repeated && (types.packed[type] !== undefined || types.basic[type] === undefined)) |
| field.setOption("packed", false, /* ifNotSet */ true); |
| } |
| |
| function parseGroup(parent, rule) { |
| var name = next(); |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(name)) |
| throw illegal(name, "name"); |
| |
| var fieldName = util.lcFirst(name); |
| if (name === fieldName) |
| name = util.ucFirst(name); |
| skip("="); |
| var id = parseId(next()); |
| var type = new Type(name); |
| type.group = true; |
| var field = new Field(fieldName, id, name, rule); |
| field.filename = parse.filename; |
| ifBlock(type, function parseGroup_block(token) { |
| switch (token) { |
| |
| case "option": |
| parseOption(type, token); |
| skip(";"); |
| break; |
| |
| case "required": |
| case "repeated": |
| parseField(type, token); |
| break; |
| |
| case "optional": |
| /* istanbul ignore if */ |
| if (isProto3) { |
| parseField(type, "proto3_optional"); |
| } else { |
| parseField(type, "optional"); |
| } |
| break; |
| |
| /* istanbul ignore next */ |
| default: |
| throw illegal(token); // there are no groups with proto3 semantics |
| } |
| }); |
| parent.add(type) |
| .add(field); |
| } |
| |
| function parseMapField(parent) { |
| skip("<"); |
| var keyType = next(); |
| |
| /* istanbul ignore if */ |
| if (types.mapKey[keyType] === undefined) |
| throw illegal(keyType, "type"); |
| |
| skip(","); |
| var valueType = next(); |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(valueType)) |
| throw illegal(valueType, "type"); |
| |
| skip(">"); |
| var name = next(); |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(name)) |
| throw illegal(name, "name"); |
| |
| skip("="); |
| var field = new MapField(applyCase(name), parseId(next()), keyType, valueType); |
| ifBlock(field, function parseMapField_block(token) { |
| |
| /* istanbul ignore else */ |
| if (token === "option") { |
| parseOption(field, token); |
| skip(";"); |
| } else |
| throw illegal(token); |
| |
| }, function parseMapField_line() { |
| parseInlineOptions(field); |
| }); |
| parent.add(field); |
| } |
| |
| function parseOneOf(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "name"); |
| |
| var oneof = new OneOf(applyCase(token)); |
| ifBlock(oneof, function parseOneOf_block(token) { |
| if (token === "option") { |
| parseOption(oneof, token); |
| skip(";"); |
| } else { |
| push(token); |
| parseField(oneof, "optional"); |
| } |
| }); |
| parent.add(oneof); |
| } |
| |
| function parseEnum(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "name"); |
| |
| var enm = new Enum(token); |
| ifBlock(enm, function parseEnum_block(token) { |
| switch(token) { |
| case "option": |
| parseOption(enm, token); |
| skip(";"); |
| break; |
| |
| case "reserved": |
| readRanges(enm.reserved || (enm.reserved = []), true); |
| break; |
| |
| default: |
| parseEnumValue(enm, token); |
| } |
| }); |
| parent.add(enm); |
| } |
| |
| function parseEnumValue(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token)) |
| throw illegal(token, "name"); |
| |
| skip("="); |
| var value = parseId(next(), true), |
| dummy = {}; |
| ifBlock(dummy, function parseEnumValue_block(token) { |
| |
| /* istanbul ignore else */ |
| if (token === "option") { |
| parseOption(dummy, token); // skip |
| skip(";"); |
| } else |
| throw illegal(token); |
| |
| }, function parseEnumValue_line() { |
| parseInlineOptions(dummy); // skip |
| }); |
| parent.add(token, value, dummy.comment); |
| } |
| |
| function parseOption(parent, token) { |
| var isCustom = skip("(", true); |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(token = next())) |
| throw illegal(token, "name"); |
| |
| var name = token; |
| var option = name; |
| var propName; |
| |
| if (isCustom) { |
| skip(")"); |
| name = "(" + name + ")"; |
| option = name; |
| token = peek(); |
| if (fqTypeRefRe.test(token)) { |
| propName = token.substr(1); //remove '.' before property name |
| name += token; |
| next(); |
| } |
| } |
| skip("="); |
| var optionValue = parseOptionValue(parent, name); |
| setParsedOption(parent, option, optionValue, propName); |
| } |
| |
| function parseOptionValue(parent, name) { |
| if (skip("{", true)) { // { a: "foo" b { c: "bar" } } |
| var result = {}; |
| while (!skip("}", true)) { |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "name"); |
| |
| var value; |
| var propName = token; |
| if (peek() === "{") |
| value = parseOptionValue(parent, name + "." + token); |
| else { |
| skip(":"); |
| if (peek() === "{") |
| value = parseOptionValue(parent, name + "." + token); |
| else { |
| value = readValue(true); |
| setOption(parent, name + "." + token, value); |
| } |
| } |
| var prevValue = result[propName]; |
| if (prevValue) |
| value = [].concat(prevValue).concat(value); |
| result[propName] = value; |
| skip(",", true); |
| } |
| return result; |
| } |
| |
| var simpleValue = readValue(true); |
| setOption(parent, name, simpleValue); |
| return simpleValue; |
| // Does not enforce a delimiter to be universal |
| } |
| |
| function setOption(parent, name, value) { |
| if (parent.setOption) |
| parent.setOption(name, value); |
| } |
| |
| function setParsedOption(parent, name, value, propName) { |
| if (parent.setParsedOption) |
| parent.setParsedOption(name, value, propName); |
| } |
| |
| function parseInlineOptions(parent) { |
| if (skip("[", true)) { |
| do { |
| parseOption(parent, "option"); |
| } while (skip(",", true)); |
| skip("]"); |
| } |
| return parent; |
| } |
| |
| function parseService(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "service name"); |
| |
| var service = new Service(token); |
| ifBlock(service, function parseService_block(token) { |
| if (parseCommon(service, token)) |
| return; |
| |
| /* istanbul ignore else */ |
| if (token === "rpc") |
| parseMethod(service, token); |
| else |
| throw illegal(token); |
| }); |
| parent.add(service); |
| } |
| |
| function parseMethod(parent, token) { |
| // Get the comment of the preceding line now (if one exists) in case the |
| // method is defined across multiple lines. |
| var commentText = cmnt(); |
| |
| var type = token; |
| |
| /* istanbul ignore if */ |
| if (!nameRe.test(token = next())) |
| throw illegal(token, "name"); |
| |
| var name = token, |
| requestType, requestStream, |
| responseType, responseStream; |
| |
| skip("("); |
| if (skip("stream", true)) |
| requestStream = true; |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(token = next())) |
| throw illegal(token); |
| |
| requestType = token; |
| skip(")"); skip("returns"); skip("("); |
| if (skip("stream", true)) |
| responseStream = true; |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(token = next())) |
| throw illegal(token); |
| |
| responseType = token; |
| skip(")"); |
| |
| var method = new Method(name, type, requestType, responseType, requestStream, responseStream); |
| method.comment = commentText; |
| ifBlock(method, function parseMethod_block(token) { |
| |
| /* istanbul ignore else */ |
| if (token === "option") { |
| parseOption(method, token); |
| skip(";"); |
| } else |
| throw illegal(token); |
| |
| }); |
| parent.add(method); |
| } |
| |
| function parseExtension(parent, token) { |
| |
| /* istanbul ignore if */ |
| if (!typeRefRe.test(token = next())) |
| throw illegal(token, "reference"); |
| |
| var reference = token; |
| ifBlock(null, function parseExtension_block(token) { |
| switch (token) { |
| |
| case "required": |
| case "repeated": |
| parseField(parent, token, reference); |
| break; |
| |
| case "optional": |
| /* istanbul ignore if */ |
| if (isProto3) { |
| parseField(parent, "proto3_optional", reference); |
| } else { |
| parseField(parent, "optional", reference); |
| } |
| break; |
| |
| default: |
| /* istanbul ignore if */ |
| if (!isProto3 || !typeRefRe.test(token)) |
| throw illegal(token); |
| push(token); |
| parseField(parent, "optional", reference); |
| break; |
| } |
| }); |
| } |
| |
| var token; |
| while ((token = next()) !== null) { |
| switch (token) { |
| |
| case "package": |
| |
| /* istanbul ignore if */ |
| if (!head) |
| throw illegal(token); |
| |
| parsePackage(); |
| break; |
| |
| case "import": |
| |
| /* istanbul ignore if */ |
| if (!head) |
| throw illegal(token); |
| |
| parseImport(); |
| break; |
| |
| case "syntax": |
| |
| /* istanbul ignore if */ |
| if (!head) |
| throw illegal(token); |
| |
| parseSyntax(); |
| break; |
| |
| case "option": |
| |
| parseOption(ptr, token); |
| skip(";"); |
| break; |
| |
| default: |
| |
| /* istanbul ignore else */ |
| if (parseCommon(ptr, token)) { |
| head = false; |
| continue; |
| } |
| |
| /* istanbul ignore next */ |
| throw illegal(token); |
| } |
| } |
| |
| parse.filename = null; |
| return { |
| "package" : pkg, |
| "imports" : imports, |
| weakImports : weakImports, |
| syntax : syntax, |
| root : root |
| }; |
| } |
| |
| /** |
| * Parses the given .proto source and returns an object with the parsed contents. |
| * @name parse |
| * @function |
| * @param {string} source Source contents |
| * @param {IParseOptions} [options] Parse options. Defaults to {@link parse.defaults} when omitted. |
| * @returns {IParserResult} Parser result |
| * @property {string} filename=null Currently processing file name for error reporting, if known |
| * @property {IParseOptions} defaults Default {@link IParseOptions} |
| * @variation 2 |
| */ |