blob: a1161f344d89f9cf54f69c5e4c26fb974d8a9bc5 [file] [log] [blame]
// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Protocol Buffer 2 Serializer which serializes messages
* into a user-friendly text format. Note that this code can run a bit
* slowly (especially for parsing) and should therefore not be used for
* time or space-critical applications.
*
* @see http://goo.gl/QDmDr
*/
goog.provide('goog.proto2.TextFormatSerializer');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.json');
goog.require('goog.math');
goog.require('goog.object');
goog.require('goog.proto2.FieldDescriptor');
goog.require('goog.proto2.Message');
goog.require('goog.proto2.Serializer');
goog.require('goog.string');
/**
* TextFormatSerializer, a serializer which turns Messages into the human
* readable text format.
* @param {boolean=} opt_ignoreMissingFields If true, then fields that cannot be
* found on the proto when parsing the text format will be ignored.
* @param {boolean=} opt_useEnumValues If true, serialization code for enums
* will use enum integer values instead of human-readable symbolic names.
* @constructor
* @extends {goog.proto2.Serializer}
* @final
*/
goog.proto2.TextFormatSerializer = function(
opt_ignoreMissingFields, opt_useEnumValues) {
/**
* Whether to ignore fields not defined on the proto when parsing the text
* format.
* @type {boolean}
* @private
*/
this.ignoreMissingFields_ = !!opt_ignoreMissingFields;
/**
* Whether to use integer enum values during enum serialization.
* If false, symbolic names will be used.
* @type {boolean}
* @private
*/
this.useEnumValues_ = !!opt_useEnumValues;
};
goog.inherits(goog.proto2.TextFormatSerializer, goog.proto2.Serializer);
/**
* Deserializes a message from text format and places the data in the message.
* @param {goog.proto2.Message} message The message in which to
* place the information.
* @param {*} data The text format data.
* @return {?string} The parse error or null on success.
* @override
*/
goog.proto2.TextFormatSerializer.prototype.deserializeTo =
function(message, data) {
var descriptor = message.getDescriptor();
var textData = data.toString();
var parser = new goog.proto2.TextFormatSerializer.Parser();
if (!parser.parse(message, textData, this.ignoreMissingFields_)) {
return parser.getError();
}
return null;
};
/**
* Serializes a message to a string.
* @param {goog.proto2.Message} message The message to be serialized.
* @return {string} The serialized form of the message.
* @override
*/
goog.proto2.TextFormatSerializer.prototype.serialize = function(message) {
var printer = new goog.proto2.TextFormatSerializer.Printer_();
this.serializeMessage_(message, printer);
return printer.toString();
};
/**
* Serializes the message and prints the text form into the given printer.
* @param {goog.proto2.Message} message The message to serialize.
* @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
* which the text format will be printed.
* @private
*/
goog.proto2.TextFormatSerializer.prototype.serializeMessage_ =
function(message, printer) {
var descriptor = message.getDescriptor();
var fields = descriptor.getFields();
// Add the defined fields, recursively.
goog.array.forEach(fields, function(field) {
this.printField_(message, field, printer);
}, this);
// Add the unknown fields, if any.
message.forEachUnknown(function(tag, value) {
this.serializeUnknown_(tag, value, printer);
}, this);
};
/**
* Serializes an unknown field. When parsed from the JsPb object format, this
* manifests as either a primitive type, an array, or a raw object with integer
* keys. There is no descriptor available to interpret the types of nested
* messages.
* @param {number} tag The tag for the field. Since it's unknown, this is a
* number rather than a string.
* @param {*} value The value of the field.
* @param {!goog.proto2.TextFormatSerializer.Printer_} printer The printer to
* which the text format will be serialized.
* @private
*/
goog.proto2.TextFormatSerializer.prototype.serializeUnknown_ =
function(tag, value, printer) {
if (!goog.isDefAndNotNull(value)) {
return;
}
if (goog.isArray(value)) {
goog.array.forEach(value, function(val) {
this.serializeUnknown_(tag, val, printer);
}, this);
return;
}
if (goog.isObject(value)) {
printer.append(tag);
printer.append(' {');
printer.appendLine();
printer.indent();
if (value instanceof goog.proto2.Message) {
// Note(user): This conditional is here to make the
// testSerializationOfUnknown unit test pass, but in practice we should
// never have a Message for an "unknown" field.
this.serializeMessage_(value, printer);
} else {
// For an unknown message, fields are keyed by positive integers. We
// don't have a 'length' property to use for enumeration, so go through
// all properties and ignore the ones that aren't legal keys.
for (var key in value) {
var keyAsNumber = goog.string.parseInt(key);
goog.asserts.assert(goog.math.isInt(keyAsNumber));
this.serializeUnknown_(keyAsNumber, value[key], printer);
}
}
printer.dedent();
printer.append('}');
printer.appendLine();
return;
}
if (goog.isString(value)) {
value = goog.string.quote(value);
}
printer.append(tag);
printer.append(': ');
printer.append(value.toString());
printer.appendLine();
};
/**
* Prints the serialized value for the given field to the printer.
* @param {*} value The field's value.
* @param {goog.proto2.FieldDescriptor} field The field whose value is being
* printed.
* @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
* which the value will be printed.
* @private
*/
goog.proto2.TextFormatSerializer.prototype.printFieldValue_ =
function(value, field, printer) {
switch (field.getFieldType()) {
case goog.proto2.FieldDescriptor.FieldType.DOUBLE:
case goog.proto2.FieldDescriptor.FieldType.FLOAT:
case goog.proto2.FieldDescriptor.FieldType.INT64:
case goog.proto2.FieldDescriptor.FieldType.UINT64:
case goog.proto2.FieldDescriptor.FieldType.INT32:
case goog.proto2.FieldDescriptor.FieldType.UINT32:
case goog.proto2.FieldDescriptor.FieldType.FIXED64:
case goog.proto2.FieldDescriptor.FieldType.FIXED32:
case goog.proto2.FieldDescriptor.FieldType.BOOL:
case goog.proto2.FieldDescriptor.FieldType.SFIXED32:
case goog.proto2.FieldDescriptor.FieldType.SFIXED64:
case goog.proto2.FieldDescriptor.FieldType.SINT32:
case goog.proto2.FieldDescriptor.FieldType.SINT64:
printer.append(value);
break;
case goog.proto2.FieldDescriptor.FieldType.BYTES:
case goog.proto2.FieldDescriptor.FieldType.STRING:
value = goog.string.quote(value.toString());
printer.append(value);
break;
case goog.proto2.FieldDescriptor.FieldType.ENUM:
if (!this.useEnumValues_) {
// Search the enum type for a matching key.
var found = false;
goog.object.forEach(field.getNativeType(), function(eValue, key) {
if (eValue == value) {
printer.append(key);
found = true;
}
});
}
if (!found || this.useEnumValues_) {
// Otherwise, just print the numeric value.
printer.append(value.toString());
}
break;
case goog.proto2.FieldDescriptor.FieldType.GROUP:
case goog.proto2.FieldDescriptor.FieldType.MESSAGE:
this.serializeMessage_(
/** @type {goog.proto2.Message} */ (value), printer);
break;
}
};
/**
* Prints the serialized field to the printer.
* @param {goog.proto2.Message} message The parent message.
* @param {goog.proto2.FieldDescriptor} field The field to print.
* @param {goog.proto2.TextFormatSerializer.Printer_} printer The printer to
* which the field will be printed.
* @private
*/
goog.proto2.TextFormatSerializer.prototype.printField_ =
function(message, field, printer) {
// Skip fields not present.
if (!message.has(field)) {
return;
}
var count = message.countOf(field);
for (var i = 0; i < count; ++i) {
// Field name.
printer.append(field.getName());
// Field delimiter.
if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
printer.append(' {');
printer.appendLine();
printer.indent();
} else {
printer.append(': ');
}
// Write the field value.
this.printFieldValue_(message.get(field, i), field, printer);
// Close the field.
if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
printer.dedent();
printer.append('}');
printer.appendLine();
} else {
printer.appendLine();
}
}
};
////////////////////////////////////////////////////////////////////////////////
/**
* Helper class used by the text format serializer for pretty-printing text.
* @constructor
* @private
*/
goog.proto2.TextFormatSerializer.Printer_ = function() {
/**
* The current indentation count.
* @type {number}
* @private
*/
this.indentation_ = 0;
/**
* The buffer of string pieces.
* @type {Array<string>}
* @private
*/
this.buffer_ = [];
/**
* Whether indentation is required before the next append of characters.
* @type {boolean}
* @private
*/
this.requiresIndentation_ = true;
};
/**
* @return {string} The contents of the printer.
* @override
*/
goog.proto2.TextFormatSerializer.Printer_.prototype.toString = function() {
return this.buffer_.join('');
};
/**
* Increases the indentation in the printer.
*/
goog.proto2.TextFormatSerializer.Printer_.prototype.indent = function() {
this.indentation_ += 2;
};
/**
* Decreases the indentation in the printer.
*/
goog.proto2.TextFormatSerializer.Printer_.prototype.dedent = function() {
this.indentation_ -= 2;
goog.asserts.assert(this.indentation_ >= 0);
};
/**
* Appends the given value to the printer.
* @param {*} value The value to append.
*/
goog.proto2.TextFormatSerializer.Printer_.prototype.append = function(value) {
if (this.requiresIndentation_) {
for (var i = 0; i < this.indentation_; ++i) {
this.buffer_.push(' ');
}
this.requiresIndentation_ = false;
}
this.buffer_.push(value.toString());
};
/**
* Appends a newline to the printer.
*/
goog.proto2.TextFormatSerializer.Printer_.prototype.appendLine = function() {
this.buffer_.push('\n');
this.requiresIndentation_ = true;
};
////////////////////////////////////////////////////////////////////////////////
/**
* Helper class for tokenizing the text format.
* @param {string} data The string data to tokenize.
* @param {boolean=} opt_ignoreWhitespace If true, whitespace tokens will not
* be reported by the tokenizer.
* @param {boolean=} opt_ignoreComments If true, comment tokens will not be
* reported by the tokenizer.
* @constructor
* @private
*/
goog.proto2.TextFormatSerializer.Tokenizer_ =
function(data, opt_ignoreWhitespace, opt_ignoreComments) {
/**
* Whether to skip whitespace tokens on output.
* @private {boolean}
*/
this.ignoreWhitespace_ = !!opt_ignoreWhitespace;
/**
* Whether to skip comment tokens on output.
* @private {boolean}
*/
this.ignoreComments_ = !!opt_ignoreComments;
/**
* The data being tokenized.
* @private {string}
*/
this.data_ = data;
/**
* The current index in the data.
* @private {number}
*/
this.index_ = 0;
/**
* The data string starting at the current index.
* @private {string}
*/
this.currentData_ = data;
/**
* The current token type.
* @private {goog.proto2.TextFormatSerializer.Tokenizer_.Token}
*/
this.current_ = {
type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes.END,
value: null
};
};
/**
* @typedef {{type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes,
* value: ?string}}
*/
goog.proto2.TextFormatSerializer.Tokenizer_.Token;
/**
* @return {goog.proto2.TextFormatSerializer.Tokenizer_.Token} The current
* token.
*/
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.getCurrent = function() {
return this.current_;
};
/**
* An enumeration of all the token types.
* @enum {!RegExp}
*/
goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes = {
END: /---end---/,
// Leading "-" to identify "-infinity"."
IDENTIFIER: /^-?[a-zA-Z][a-zA-Z0-9_]*/,
NUMBER: /^(0x[0-9a-f]+)|(([-])?[0-9][0-9]*(\.?[0-9]+)?(e[+-]?[0-9]+|[f])?)/,
COMMENT: /^#.*/,
OPEN_BRACE: /^{/,
CLOSE_BRACE: /^}/,
OPEN_TAG: /^</,
CLOSE_TAG: /^>/,
OPEN_LIST: /^\[/,
CLOSE_LIST: /^\]/,
STRING: new RegExp('^"([^"\\\\]|\\\\.)*"'),
COLON: /^:/,
COMMA: /^,/,
SEMI: /^;/,
WHITESPACE: /^\s/
};
/**
* Advances to the next token.
* @return {boolean} True if a valid token was found, false if the end was
* reached or no valid token was found.
*/
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.next = function() {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
// Skip any whitespace if requested.
while (this.nextInternal_()) {
var type = this.getCurrent().type;
if ((type != types.WHITESPACE && type != types.COMMENT) ||
(type == types.WHITESPACE && !this.ignoreWhitespace_) ||
(type == types.COMMENT && !this.ignoreComments_)) {
return true;
}
}
// If we reach this point, set the current token to END.
this.current_ = {
type: goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes.END,
value: null
};
return false;
};
/**
* Internal method for determining the next token.
* @return {boolean} True if a next token was found, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Tokenizer_.prototype.nextInternal_ =
function() {
if (this.index_ >= this.data_.length) {
return false;
}
var data = this.currentData_;
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
var next = null;
// Loop through each token type and try to match the beginning of the string
// with the token's regular expression.
goog.object.some(types, function(type, id) {
if (next || type == types.END) {
return false;
}
// Note: This regular expression check is at, minimum, O(n).
var info = type.exec(data);
if (info && info.index == 0) {
next = {
type: type,
value: info[0]
};
}
return !!next;
});
// Advance the index by the length of the token.
if (next) {
this.current_ =
/** @type {goog.proto2.TextFormatSerializer.Tokenizer_.Token} */ (next);
this.index_ += next.value.length;
this.currentData_ = this.currentData_.substring(next.value.length);
}
return !!next;
};
////////////////////////////////////////////////////////////////////////////////
/**
* Helper class for parsing the text format.
* @constructor
* @final
*/
goog.proto2.TextFormatSerializer.Parser = function() {
/**
* The error during parsing, if any.
* @type {?string}
* @private
*/
this.error_ = null;
/**
* The current tokenizer.
* @type {goog.proto2.TextFormatSerializer.Tokenizer_}
* @private
*/
this.tokenizer_ = null;
/**
* Whether to ignore missing fields in the proto when parsing.
* @type {boolean}
* @private
*/
this.ignoreMissingFields_ = false;
};
/**
* Parses the given data, filling the message as it goes.
* @param {goog.proto2.Message} message The message to fill.
* @param {string} data The text format data.
* @param {boolean=} opt_ignoreMissingFields If true, fields missing in the
* proto will be ignored.
* @return {boolean} True on success, false on failure. On failure, the
* getError method can be called to get the reason for failure.
*/
goog.proto2.TextFormatSerializer.Parser.prototype.parse =
function(message, data, opt_ignoreMissingFields) {
this.error_ = null;
this.ignoreMissingFields_ = !!opt_ignoreMissingFields;
this.tokenizer_ =
new goog.proto2.TextFormatSerializer.Tokenizer_(data, true, true);
this.tokenizer_.next();
return this.consumeMessage_(message, '');
};
/**
* @return {?string} The parse error, if any.
*/
goog.proto2.TextFormatSerializer.Parser.prototype.getError = function() {
return this.error_;
};
/**
* Reports a parse error.
* @param {string} msg The error message.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.reportError_ =
function(msg) {
this.error_ = msg;
};
/**
* Attempts to consume the given message.
* @param {goog.proto2.Message} message The message to consume and fill. If
* null, then the message contents will be consumed without ever being set
* to anything.
* @param {string} delimiter The delimiter expected at the end of the message.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeMessage_ =
function(message, delimiter) {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
while (!this.lookingAt_('>') && !this.lookingAt_('}') &&
!this.lookingAtType_(types.END)) {
if (!this.consumeField_(message)) { return false; }
}
if (delimiter) {
if (!this.consume_(delimiter)) { return false; }
} else {
if (!this.lookingAtType_(types.END)) {
this.reportError_('Expected END token');
}
}
return true;
};
/**
* Attempts to consume the value of the given field.
* @param {goog.proto2.Message} message The parent message.
* @param {goog.proto2.FieldDescriptor} field The field.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeFieldValue_ =
function(message, field) {
var value = this.getFieldValue_(field);
if (goog.isNull(value)) { return false; }
if (field.isRepeated()) {
message.add(field, value);
} else {
message.set(field, value);
}
return true;
};
/**
* Attempts to convert a string to a number.
* @param {string} num in hexadecimal or float format.
* @return {!number} The converted number or null on error.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.getNumberFromString_ =
function(num) {
var returnValue = goog.string.contains(num, '.') ?
parseFloat(num) : // num is a float.
goog.string.parseInt(num); // num is an int.
goog.asserts.assert(!isNaN(returnValue));
goog.asserts.assert(isFinite(returnValue));
return returnValue;
};
/**
* Parse NaN, positive infinity, or negative infinity from a string.
* @param {string} identifier An identifier string to check.
* @return {?number} Infinity, negative infinity, NaN, or null if none
* of the constants could be parsed.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.parseNumericalConstant_ =
function(identifier) {
if (/^-?inf(?:inity)?f?$/i.test(identifier)) {
return Infinity * (goog.string.startsWith(identifier, '-') ? -1 : 1);
}
if (/^nanf?$/i.test(identifier)) {
return NaN;
}
return null;
};
/**
* Attempts to parse the given field's value from the stream.
* @param {goog.proto2.FieldDescriptor} field The field.
* @return {*} The field's value or null if none.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.getFieldValue_ =
function(field) {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
switch (field.getFieldType()) {
case goog.proto2.FieldDescriptor.FieldType.DOUBLE:
case goog.proto2.FieldDescriptor.FieldType.FLOAT:
var identifier = this.consumeIdentifier_();
if (identifier) {
var numericalIdentifier =
goog.proto2.TextFormatSerializer.Parser.parseNumericalConstant_(
identifier);
// Use isDefAndNotNull since !!NaN is false.
if (goog.isDefAndNotNull(numericalIdentifier)) {
return numericalIdentifier;
}
}
case goog.proto2.FieldDescriptor.FieldType.INT32:
case goog.proto2.FieldDescriptor.FieldType.UINT32:
case goog.proto2.FieldDescriptor.FieldType.FIXED32:
case goog.proto2.FieldDescriptor.FieldType.SFIXED32:
case goog.proto2.FieldDescriptor.FieldType.SINT32:
var num = this.consumeNumber_();
if (!num) {
return null;
}
return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(num);
case goog.proto2.FieldDescriptor.FieldType.INT64:
case goog.proto2.FieldDescriptor.FieldType.UINT64:
case goog.proto2.FieldDescriptor.FieldType.FIXED64:
case goog.proto2.FieldDescriptor.FieldType.SFIXED64:
case goog.proto2.FieldDescriptor.FieldType.SINT64:
var num = this.consumeNumber_();
if (!num) {
return null;
}
if (field.getNativeType() == Number) {
// 64-bit number stored as a number.
return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(
num);
}
return num; // 64-bit numbers are by default stored as strings.
case goog.proto2.FieldDescriptor.FieldType.BOOL:
var ident = this.consumeIdentifier_();
if (!ident) {
return null;
}
switch (ident) {
case 'true': return true;
case 'false': return false;
default:
this.reportError_('Unknown type for bool: ' + ident);
return null;
}
case goog.proto2.FieldDescriptor.FieldType.ENUM:
if (this.lookingAtType_(types.NUMBER)) {
var num = this.consumeNumber_();
if (!num) {
return null;
}
return goog.proto2.TextFormatSerializer.Parser.getNumberFromString_(
num);
} else {
// Search the enum type for a matching key.
var name = this.consumeIdentifier_();
if (!name) {
return null;
}
var enumValue = field.getNativeType()[name];
if (enumValue == null) {
this.reportError_('Unknown enum value: ' + name);
return null;
}
return enumValue;
}
case goog.proto2.FieldDescriptor.FieldType.BYTES:
case goog.proto2.FieldDescriptor.FieldType.STRING:
return this.consumeString_();
}
};
/**
* Attempts to consume a nested message.
* @param {goog.proto2.Message} message The parent message.
* @param {goog.proto2.FieldDescriptor} field The field.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeNestedMessage_ =
function(message, field) {
var delimiter = '';
// Messages support both < > and { } as delimiters for legacy reasons.
if (this.tryConsume_('<')) {
delimiter = '>';
} else {
if (!this.consume_('{')) { return false; }
delimiter = '}';
}
var msg = field.getFieldMessageType().createMessageInstance();
var result = this.consumeMessage_(msg, delimiter);
if (!result) { return false; }
// Add the message to the parent message.
if (field.isRepeated()) {
message.add(field, msg);
} else {
message.set(field, msg);
}
return true;
};
/**
* Attempts to consume the value of an unknown field. This method uses
* heuristics to try to consume just the right tokens.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeUnknownFieldValue_ =
function() {
// : is optional.
this.tryConsume_(':');
// Handle form: [.. , ... , ..]
if (this.tryConsume_('[')) {
while (true) {
this.tokenizer_.next();
if (this.tryConsume_(']')) {
break;
}
if (!this.consume_(',')) { return false; }
}
return true;
}
// Handle nested messages/groups.
if (this.tryConsume_('<')) {
return this.consumeMessage_(null /* unknown */, '>');
} else if (this.tryConsume_('{')) {
return this.consumeMessage_(null /* unknown */, '}');
} else {
// Otherwise, consume a single token for the field value.
this.tokenizer_.next();
}
return true;
};
/**
* Attempts to consume a field under a message.
* @param {goog.proto2.Message} message The parent message. If null, then the
* field value will be consumed without being assigned to anything.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeField_ =
function(message) {
var fieldName = this.consumeIdentifier_();
if (!fieldName) {
this.reportError_('Missing field name');
return false;
}
var field = null;
if (message) {
field = message.getDescriptor().findFieldByName(fieldName.toString());
}
if (field == null) {
if (this.ignoreMissingFields_) {
return this.consumeUnknownFieldValue_();
} else {
this.reportError_('Unknown field: ' + fieldName);
return false;
}
}
if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.MESSAGE ||
field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.GROUP) {
// : is optional here.
this.tryConsume_(':');
if (!this.consumeNestedMessage_(message, field)) { return false; }
} else {
// Long Format: "someField: 123"
// Short Format: "someField: [123, 456, 789]"
if (!this.consume_(':')) { return false; }
if (field.isRepeated() && this.tryConsume_('[')) {
// Short repeated format, e.g. "foo: [1, 2, 3]"
while (true) {
if (!this.consumeFieldValue_(message, field)) { return false; }
if (this.tryConsume_(']')) {
break;
}
if (!this.consume_(',')) { return false; }
}
} else {
// Normal field format.
if (!this.consumeFieldValue_(message, field)) { return false; }
}
}
// For historical reasons, fields may optionally be separated by commas or
// semicolons.
this.tryConsume_(',') || this.tryConsume_(';');
return true;
};
/**
* Attempts to consume a token with the given string value.
* @param {string} value The string value for the token.
* @return {boolean} True if the token matches and was consumed, false
* otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.tryConsume_ =
function(value) {
if (this.lookingAt_(value)) {
this.tokenizer_.next();
return true;
}
return false;
};
/**
* Consumes a token of the given type.
* @param {goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes} type The type
* of the token to consume.
* @return {?string} The string value of the token or null on error.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeToken_ =
function(type) {
if (!this.lookingAtType_(type)) {
this.reportError_('Expected token type: ' + type);
return null;
}
var value = this.tokenizer_.getCurrent().value;
this.tokenizer_.next();
return value;
};
/**
* Consumes an IDENTIFIER token.
* @return {?string} The string value or null on error.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeIdentifier_ =
function() {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
return this.consumeToken_(types.IDENTIFIER);
};
/**
* Consumes a NUMBER token.
* @return {?string} The string value or null on error.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeNumber_ =
function() {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
return this.consumeToken_(types.NUMBER);
};
/**
* Consumes a STRING token. Strings may come in multiple adjacent tokens which
* are automatically concatenated, like in C or Python.
* @return {?string} The *deescaped* string value or null on error.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consumeString_ =
function() {
var types = goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes;
var value = this.consumeToken_(types.STRING);
if (!value) {
return null;
}
var stringValue = goog.json.parse(value).toString();
while (this.lookingAtType_(types.STRING)) {
value = this.consumeToken_(types.STRING);
stringValue += goog.json.parse(value).toString();
}
return stringValue;
};
/**
* Consumes a token with the given value. If not found, reports an error.
* @param {string} value The string value expected for the token.
* @return {boolean} True on success, false otherwise.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.consume_ = function(value) {
if (!this.tryConsume_(value)) {
this.reportError_('Expected token "' + value + '"');
return false;
}
return true;
};
/**
* @param {string} value The value to check against.
* @return {boolean} True if the current token has the given string value.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.lookingAt_ =
function(value) {
return this.tokenizer_.getCurrent().value == value;
};
/**
* @param {goog.proto2.TextFormatSerializer.Tokenizer_.TokenTypes} type The
* token type.
* @return {boolean} True if the current token has the given type.
* @private
*/
goog.proto2.TextFormatSerializer.Parser.prototype.lookingAtType_ =
function(type) {
return this.tokenizer_.getCurrent().type == type;
};