Skip to content

Commit

Permalink
feat: proto3 optional support (#1584)
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin E. Coe <bencoe@google.com>
  • Loading branch information
alexander-fenster and bcoe authored Apr 15, 2021
1 parent 94e9ef0 commit 6c4d307
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 5 deletions.
6 changes: 5 additions & 1 deletion cli/targets/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ".",
Expand All @@ -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) + ","
Expand Down
3 changes: 3 additions & 0 deletions src/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down
41 changes: 37 additions & 4 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
26 changes: 26 additions & 0 deletions tests/comp_optional.js
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit 6c4d307

Please sign in to comment.