feat: add feature resolution for protobuf editions

This commit is contained in:
Sofia Leon 2024-09-10 14:38:31 -07:00
parent d8eb1b4e80
commit 65d3ed15e3
6 changed files with 298 additions and 21 deletions

View File

@ -20,7 +20,7 @@ var Namespace = require("./namespace"),
* @param {Object.<string,string>} [comments] The value comments for this enum
* @param {Object.<string,Object<string,*>>|undefined} [valuesOptions] The value options for this enum
*/
function Enum(name, values, options, comment, comments, valuesOptions) {
function Enum(name, values, options, comment, comments, valuesOptions, valuesFeatures) {
ReflectionObject.call(this, name, options);
if (values && typeof values !== "object")
@ -56,6 +56,12 @@ function Enum(name, values, options, comment, comments, valuesOptions) {
*/
this.valuesOptions = valuesOptions;
/**
* Values features, if any
* @type {Object<string, Object<string, *>>|undefined}
*/
this.valuesFeatures = valuesFeatures;
/**
* Reserved ranges, if any.
* @type {Array.<number[]|string>}
@ -119,7 +125,7 @@ Enum.prototype.toJSON = function toJSON(toJSONOptions) {
* @throws {TypeError} If arguments are invalid
* @throws {Error} If there is already a value with this name or id
*/
Enum.prototype.add = function add(name, id, comment, options) {
Enum.prototype.add = function add(name, id, comment, options, features) {
// utilized by the parser but not by .fromJSON
if (!util.isString(name))
@ -150,6 +156,12 @@ Enum.prototype.add = function add(name, id, comment, options) {
this.valuesOptions[name] = options || null;
}
if (features) {
if (this.valuesFeatures === undefined)
this.valuesFeatures = {};
this.valuesFeatures[name] = features || null;
}
this.comments[name] = comment || null;
return this;
};
@ -176,6 +188,8 @@ Enum.prototype.remove = function remove(name) {
if (this.valuesOptions)
delete this.valuesOptions[name];
if (this.valuesFeatures)
delete this.valuesFeatures[name];
return this;
};

View File

@ -41,6 +41,11 @@ function ReflectionObject(name, options) {
*/
this.name = name;
/**
* Resolved Features.
*/
this.features = null;
/**
* Parent namespace.
* @type {Namespace|null}
@ -175,6 +180,17 @@ ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet)
return this;
};
/**
* Sets a feature.
* @param {string} name Feature name
* @param {*} value Feature value
* @returns {ReflectionObject} `this`
*/
ReflectionObject.prototype.setFeature = function setFeature(name, value) {
(this.features || (this.features = {}))[name] = value;
return this;
};
/**
* Sets a parsed option.
* @param {string} name parsed Option name

View File

@ -4,6 +4,7 @@ module.exports = parse;
parse.filename = null;
parse.defaults = { keepCase: false };
const { hasOwnProperty } = require("tslint/lib/utils");
var tokenize = require("./tokenize"),
Root = require("./root"),
Type = require("./type"),
@ -25,7 +26,8 @@ var base10Re = /^[1-9][0-9]*$/,
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]*)+$/;
fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/,
featuresRefRe = /features\.([a-zA-Z_]*)/;
/**
* Result object returned from {@link parse}.
@ -312,6 +314,7 @@ function parse(source, root, options) {
case "extend":
parseExtension(parent, token);
return true;
}
return false;
}
@ -480,7 +483,7 @@ function parse(source, root, options) {
parseOption(type, token);
skip(";");
break;
case "required":
case "repeated":
parseField(type, token);
@ -611,6 +614,11 @@ function parse(source, root, options) {
this.options = {};
this.options[name] = value;
};
dummy.setFeature = function(name, value) {
if (this.features === undefined)
this.features = {};
this.features[name] = value;
};
ifBlock(dummy, function parseEnumValue_block(token) {
/* istanbul ignore else */
@ -623,33 +631,40 @@ function parse(source, root, options) {
}, function parseEnumValue_line() {
parseInlineOptions(dummy); // skip
});
parent.add(token, value, dummy.comment, dummy.options);
parent.add(token, value, dummy.comment, dummy.options, dummy.features);
}
function parseOption(parent, token) {
if (featuresRefRe.test(token = next())) {
var name = token.match(featuresRefRe)[1]
skip("=");
setFeature(parent, name, token = next())
} else {
var isCustom = skip("(", true);
if (!typeRefRe.test(token = next()))
throw illegal(token, "name");
var name = token;
var option = name;
var propName;
var name = token;
var option = name;
var propName;
if (isCustom) {
skip(")");
name = "(" + name + ")";
option = name;
token = peek();
if (fqTypeRefRe.test(token)) {
propName = token.slice(1); //remove '.' before property name
name += token;
next();
if (isCustom) {
skip(")");
name = "(" + name + ")";
option = name;
token = peek();
if (fqTypeRefRe.test(token)) {
propName = token.slice(1); //remove '.' before property name
name += token;
next();
}
}
}
skip("=");
var optionValue = parseOptionValue(parent, name);
setParsedOption(parent, option, optionValue, propName);
skip("=");
var optionValue = parseOptionValue(parent, name);
setParsedOption(parent, option, optionValue, propName);
}
}
function parseOptionValue(parent, name) {
@ -720,6 +735,12 @@ function parse(source, root, options) {
parent.setOption(name, value);
}
function setFeature(parent, name, value) {
if (parent.setFeature) {
parent.setFeature(name, value);
}
}
function setParsedOption(parent, name, value, propName) {
if (parent.setParsedOption)
parent.setParsedOption(name, value, propName);

View File

@ -0,0 +1,60 @@
edition = "2023";
option features.amazing_feature = A;
service MyService {
option features.amazing_feature = E;
rpc MyMethod (MyRequest) returns (MyResponse) {
option features.amazing_feature = L;
};
}
message Message {
option features.amazing_feature = B;
string string_val = 1;
repeated string string_repeated = 2 [features.amazing_feature = F];
uint64 uint64_val = 3;
repeated uint64 uint64_repeated = 4;
bytes bytes_val = 5;
repeated bytes bytes_repeated = 6;
SomeEnum enum_val = 7;
repeated SomeEnum enum_repeated = 8;
extensions 10 to 100;
extend Message {
required int32 bar = 10 [features.amazing_feature = I];
}
message Nested {
option features.amazing_feature = H;
optional int64 count = 9;
}
enum SomeEnumInMessage {
option features.amazing_feature = G;
ONE = 11;
TWO = 12;
}
oneof SomeOneOf {
option features.amazing_feature = J;
int32 a = 13;
string b = 14;
}
map<string,int64> int64_map = 15;
}
extend Message {
required int32 bar = 16 [features.amazing_feature = D];
}
enum SomeEnum {
option features.amazing_feature = C;
ONE = 1 [features.amazing_feature = K];
TWO = 2;
}

View File

@ -0,0 +1,115 @@
/*
1. Defaults
2. File - A
3. Message - B
4. Enum - C
5. File extension - D
6. File service - E
7. Message Field - F
8. Message Enum - G
9. Message Message - H
10. Message Extension - I
11. "one of" Field - J
12. Enum value - K
13. Service method - L
edition = "2023";
option features.amazing_feature = A;
service MyService {
option features.amazing_feature = E;
rpc MyMethod (MyRequest) returns (MyResponse) {
option features.amazing_feature = L;
};
}
message Message {
option features.amazing_feature = B;
string string_val = 1;
repeated string string_repeated = 2 [features.amazing_feature = F];
uint64 uint64_val = 3;
repeated uint64 uint64_repeated = 4;
bytes bytes_val = 5;
repeated bytes bytes_repeated = 6;
SomeEnum enum_val = 7;
repeated SomeEnum enum_repeated = 8;
extensions 10 to 100;
extend Message {
int32 bar = 10 [features.amazing_feature = I];
}
message Nested {
option features.amazing_feature = H;
optional int32 count = 1;
}
enum SomeEnum {
option features.amazing_feature = G;
ONE = 1;
TWO = 2;
}
oneof bar {
option features.amazing_feature = J;
int32 a = 1;
string b = 2;
}
map<string,int64> int64_map = 9;
}
extend Message {
int32 bar = 11 [features.amazing_feature = D];
}
enum SomeEnum {
option features.amazing_feature = C;
ONE = 1 [features.amazing_feature = K];
TWO = 2;
}
*/
var tape = require("tape");
var protobuf = require("..");
tape.test.only("feature resolution editions", function(test) {
protobuf.load("tests/data/feature-resolution.proto", function(err, root) {
if (err)
return test.fail(err.message);
// test.same(root.fea, {
// 1: "a",
// 2: "b"
// }, "should also expose their values by id");
// console.log(root.features.amazing_feature)
test.same(root.features.amazing_feature, 'A');
test.same(root.lookup("Message").features.amazing_feature, 'B')
test.same(root.lookupService("MyService").features.amazing_feature, 'E');
test.same(root.lookupEnum("SomeEnum").features.amazing_feature, 'C')
test.same(root.lookup("Message").lookupEnum("SomeEnumInMessage").features.amazing_feature, 'G')
test.same(root.lookup("Message").lookup("Nested").features.amazing_feature, 'H')
test.same(root.lookupService("MyService").lookup("MyMethod").features.amazing_feature, 'L')
test.same(root.lookup("Message").fields.stringRepeated.features.amazing_feature, 'F')
test.same(root.lookup("Message").lookup(".Message.bar").features.amazing_feature, 'I')
test.same(root.lookupEnum("SomeEnum").valuesFeatures.ONE.amazing_feature, 'K')
test.end();
})
})

View File

@ -0,0 +1,51 @@
// TODO: write a protobuf editions test
// var tape = require("tape");
// var protobuf = require("..");
// var protoEditionsLegacyRequired = 'edition = "2023";'+
// "message Test {"+
// "uint32 a = 1 [features.field_presence = LEGACY_REQUIRED];"+
// "}";
// var proto2Required = 'syntax = "proto2";'+
// "message Test {"+
// "required uint32 a = 1;"+
// "}";
// var protoEditionsImplicit = 'edition = "2023";'+
// "message Test {"+
// "uint32 a = 1 [features.field_presence = IMPLICIT];"+
// "}";
// var msg = {
// // a: [1,2,3]
// };
// tape.test.only("packed repeated values", function(test) {
// var rootEditionsLegacyRequired = protobuf.parse(protoEditionsLegacyRequired).root,
// rootProto2Required = protobuf.parse(proto2Required).root;
// rootProtoEditionsImplicit = protobuf.parse(protoEditionsImplicit).root;
// // console.log(rootEditionsLegacyRequired)
// // console.log(rootProto2Required)
// var Test1 = rootEditionsLegacyRequired.lookup("Test"),
// Test2 = rootProto2Required.lookup("Test");
// Test3 = rootProtoEditionsImplicit.lookup("Test");
// console.log(Test2)
// // This should throw, because we are passing a msg without a required field, and
// // LEGACY_REQUIRED = required
// var dec1 = Test1.decode(Test1.encode(msg).finish());
// console.log(dec1)
// // This should also throw, same reason above, but are actually using proto2
// var dec2 = Test2.decode(Test2.encode(msg).finish());
// console.log(dec2)
// // This test shou
// var dec2 = Test2.decode(Test2.encode(msg).finish());
// console.log(dec2)
// // test.same(dec1, msg, "should decode packed even if defined non-packed");
// // var dec2 = Test1.decode(Test2.encode(msg).finish());
// // test.same(dec2, msg, "should decode non-packed even if defined packed");
// test.end();
// });