Skip to content

Commit

Permalink
feat: add feature resolution for protobuf editions
Browse files Browse the repository at this point in the history
  • Loading branch information
sofisl committed Sep 10, 2024
1 parent d8eb1b4 commit 65d3ed1
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 23 deletions.
18 changes: 16 additions & 2 deletions src/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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>}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
};
Expand All @@ -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;
};

Expand Down
16 changes: 16 additions & 0 deletions src/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ function ReflectionObject(name, options) {
*/
this.name = name;

/**
* Resolved Features.
*/
this.features = null;

/**
* Parent namespace.
* @type {Namespace|null}
Expand Down Expand Up @@ -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
Expand Down
63 changes: 42 additions & 21 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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}.
Expand Down Expand Up @@ -312,6 +314,7 @@ function parse(source, root, options) {
case "extend":
parseExtension(parent, token);
return true;

}
return false;
}
Expand Down Expand Up @@ -480,7 +483,7 @@ function parse(source, root, options) {
parseOption(type, token);
skip(";");
break;

case "required":
case "repeated":
parseField(type, token);
Expand Down Expand Up @@ -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 */
Expand All @@ -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;

if (isCustom) {
skip(")");
name = "(" + name + ")";
option = name;
token = peek();
if (fqTypeRefRe.test(token)) {
propName = token.slice(1); //remove '.' before property name
name += token;
next();


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();
}
}
}

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) {
Expand Down Expand Up @@ -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);
Expand Down
60 changes: 60 additions & 0 deletions tests/data/feature-resolution.proto
Original file line number Diff line number Diff line change
@@ -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;
}
115 changes: 115 additions & 0 deletions tests/feature_resolution_editions.js
Original file line number Diff line number Diff line change
@@ -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();
})



})
Loading

0 comments on commit 65d3ed1

Please sign in to comment.