Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: proto3 optional support #1584

Merged
merged 2 commits into from Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion cli/targets/static.js
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
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
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
@@ -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();
});