diff --git a/cli/targets/static.js b/cli/targets/static.js index f72734a82..ad4f7329b 100644 --- a/cli/targets/static.js +++ b/cli/targets/static.js @@ -394,7 +394,8 @@ function buildType(ref, type) { if (config.comments) { push(""); var jsType = toJsType(field); - if (field.optional && !field.map && !field.repeated && field.resolvedType instanceof Type) + if (field.optional && !field.map && !field.repeated && field.resolvedType instanceof Type || + field.options && field.options.proto3_optional) jsType = jsType + "|null|undefined"; pushComment([ field.comment || type.name + " " + field.name + ".", @@ -410,6 +411,9 @@ function buildType(ref, type) { push(escapeName(type.name) + ".prototype" + prop + " = $util.emptyArray;"); // overwritten in constructor else if (field.map) push(escapeName(type.name) + ".prototype" + prop + " = $util.emptyObject;"); // overwritten in constructor + else if (field.options && field.options.proto3_optional) { + push(escapeName(type.name) + ".prototype" + prop + " = null;"); // do not set default value for proto3 optional fields + } else if (field.long) push(escapeName(type.name) + ".prototype" + prop + " = $util.Long ? $util.Long.fromBits(" + JSON.stringify(field.typeDefault.low) + "," diff --git a/src/field.js b/src/field.js index 788c773b3..05d89cae8 100644 --- a/src/field.js +++ b/src/field.js @@ -82,6 +82,9 @@ function Field(name, id, type, rule, extend, options, comment) { * Field rule, if any. * @type {string|undefined} */ + if (rule === "proto3_optional") { + rule = "optional"; + } this.rule = rule && rule !== "optional" ? rule : undefined; // toJSON /** diff --git a/src/parse.js b/src/parse.js index 918d7c2eb..144feed29 100644 --- a/src/parse.js +++ b/src/parse.js @@ -316,11 +316,19 @@ function parse(source, root, options) { break; case "required": - case "optional": 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; @@ -379,7 +387,16 @@ function parse(source, root, options) { }, function parseField_line() { parseInlineOptions(field); }); - parent.add(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 @@ -413,11 +430,19 @@ function parse(source, root, options) { break; case "required": - case "optional": 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 @@ -699,10 +724,18 @@ function parse(source, root, options) { case "required": case "repeated": - case "optional": 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)) diff --git a/tests/comp_optional.js b/tests/comp_optional.js new file mode 100644 index 000000000..fd12fa9a5 --- /dev/null +++ b/tests/comp_optional.js @@ -0,0 +1,26 @@ +var tape = require("tape"); + +var protobuf = require(".."); + +var proto = "syntax = \"proto3\";\ +\ +message Message {\ + int32 regular_int32 = 1;\ + optional int32 optional_int32 = 2;\ + oneof _oneof_int32 {\ + int32 oneof_int32 = 3;\ + }\ +}\ +"; + +tape.test("proto3 optional", function(test) { + var root = protobuf.parse(proto).root; + + var Message = root.lookup("Message"); + test.equal(Message.fields.optionalInt32.optional, true); + test.equal(Message.fields.optionalInt32.options.proto3_optional, true); + test.equal(Message.oneofs._optionalInt32.name, '_optionalInt32'); + test.deepEqual(Message.oneofs._optionalInt32.oneof, ['optionalInt32']); + + test.end(); +});