Skip to content

Commit 65d3ed1

Browse files
committed
feat: add feature resolution for protobuf editions
1 parent d8eb1b4 commit 65d3ed1

File tree

6 files changed

+300
-23
lines changed

6 files changed

+300
-23
lines changed

src/enum.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var Namespace = require("./namespace"),
2020
* @param {Object.<string,string>} [comments] The value comments for this enum
2121
* @param {Object.<string,Object<string,*>>|undefined} [valuesOptions] The value options for this enum
2222
*/
23-
function Enum(name, values, options, comment, comments, valuesOptions) {
23+
function Enum(name, values, options, comment, comments, valuesOptions, valuesFeatures) {
2424
ReflectionObject.call(this, name, options);
2525

2626
if (values && typeof values !== "object")
@@ -56,6 +56,12 @@ function Enum(name, values, options, comment, comments, valuesOptions) {
5656
*/
5757
this.valuesOptions = valuesOptions;
5858

59+
/**
60+
* Values features, if any
61+
* @type {Object<string, Object<string, *>>|undefined}
62+
*/
63+
this.valuesFeatures = valuesFeatures;
64+
5965
/**
6066
* Reserved ranges, if any.
6167
* @type {Array.<number[]|string>}
@@ -119,7 +125,7 @@ Enum.prototype.toJSON = function toJSON(toJSONOptions) {
119125
* @throws {TypeError} If arguments are invalid
120126
* @throws {Error} If there is already a value with this name or id
121127
*/
122-
Enum.prototype.add = function add(name, id, comment, options) {
128+
Enum.prototype.add = function add(name, id, comment, options, features) {
123129
// utilized by the parser but not by .fromJSON
124130

125131
if (!util.isString(name))
@@ -150,6 +156,12 @@ Enum.prototype.add = function add(name, id, comment, options) {
150156
this.valuesOptions[name] = options || null;
151157
}
152158

159+
if (features) {
160+
if (this.valuesFeatures === undefined)
161+
this.valuesFeatures = {};
162+
this.valuesFeatures[name] = features || null;
163+
}
164+
153165
this.comments[name] = comment || null;
154166
return this;
155167
};
@@ -176,6 +188,8 @@ Enum.prototype.remove = function remove(name) {
176188
if (this.valuesOptions)
177189
delete this.valuesOptions[name];
178190

191+
if (this.valuesFeatures)
192+
delete this.valuesFeatures[name];
179193
return this;
180194
};
181195

src/object.js

+16
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ function ReflectionObject(name, options) {
4141
*/
4242
this.name = name;
4343

44+
/**
45+
* Resolved Features.
46+
*/
47+
this.features = null;
48+
4449
/**
4550
* Parent namespace.
4651
* @type {Namespace|null}
@@ -175,6 +180,17 @@ ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet)
175180
return this;
176181
};
177182

183+
/**
184+
* Sets a feature.
185+
* @param {string} name Feature name
186+
* @param {*} value Feature value
187+
* @returns {ReflectionObject} `this`
188+
*/
189+
ReflectionObject.prototype.setFeature = function setFeature(name, value) {
190+
(this.features || (this.features = {}))[name] = value;
191+
return this;
192+
};
193+
178194
/**
179195
* Sets a parsed option.
180196
* @param {string} name parsed Option name

src/parse.js

+42-21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = parse;
44
parse.filename = null;
55
parse.defaults = { keepCase: false };
66

7+
const { hasOwnProperty } = require("tslint/lib/utils");
78
var tokenize = require("./tokenize"),
89
Root = require("./root"),
910
Type = require("./type"),
@@ -25,7 +26,8 @@ var base10Re = /^[1-9][0-9]*$/,
2526
numberRe = /^(?![eE])[0-9]*(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?$/,
2627
nameRe = /^[a-zA-Z_][a-zA-Z_0-9]*$/,
2728
typeRefRe = /^(?:\.?[a-zA-Z_][a-zA-Z_0-9]*)(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*$/,
28-
fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/;
29+
fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/,
30+
featuresRefRe = /features\.([a-zA-Z_]*)/;
2931

3032
/**
3133
* Result object returned from {@link parse}.
@@ -312,6 +314,7 @@ function parse(source, root, options) {
312314
case "extend":
313315
parseExtension(parent, token);
314316
return true;
317+
315318
}
316319
return false;
317320
}
@@ -480,7 +483,7 @@ function parse(source, root, options) {
480483
parseOption(type, token);
481484
skip(";");
482485
break;
483-
486+
484487
case "required":
485488
case "repeated":
486489
parseField(type, token);
@@ -611,6 +614,11 @@ function parse(source, root, options) {
611614
this.options = {};
612615
this.options[name] = value;
613616
};
617+
dummy.setFeature = function(name, value) {
618+
if (this.features === undefined)
619+
this.features = {};
620+
this.features[name] = value;
621+
};
614622
ifBlock(dummy, function parseEnumValue_block(token) {
615623

616624
/* istanbul ignore else */
@@ -623,33 +631,40 @@ function parse(source, root, options) {
623631
}, function parseEnumValue_line() {
624632
parseInlineOptions(dummy); // skip
625633
});
626-
parent.add(token, value, dummy.comment, dummy.options);
634+
parent.add(token, value, dummy.comment, dummy.options, dummy.features);
627635
}
628636

629637
function parseOption(parent, token) {
638+
if (featuresRefRe.test(token = next())) {
639+
var name = token.match(featuresRefRe)[1]
640+
skip("=");
641+
setFeature(parent, name, token = next())
642+
} else {
630643
var isCustom = skip("(", true);
631644
if (!typeRefRe.test(token = next()))
632645
throw illegal(token, "name");
633-
634-
var name = token;
635-
var option = name;
636-
var propName;
637-
638-
if (isCustom) {
639-
skip(")");
640-
name = "(" + name + ")";
641-
option = name;
642-
token = peek();
643-
if (fqTypeRefRe.test(token)) {
644-
propName = token.slice(1); //remove '.' before property name
645-
name += token;
646-
next();
646+
647+
648+
var name = token;
649+
var option = name;
650+
var propName;
651+
652+
if (isCustom) {
653+
skip(")");
654+
name = "(" + name + ")";
655+
option = name;
656+
token = peek();
657+
if (fqTypeRefRe.test(token)) {
658+
propName = token.slice(1); //remove '.' before property name
659+
name += token;
660+
next();
661+
}
647662
}
648-
}
649663

650-
skip("=");
651-
var optionValue = parseOptionValue(parent, name);
652-
setParsedOption(parent, option, optionValue, propName);
664+
skip("=");
665+
var optionValue = parseOptionValue(parent, name);
666+
setParsedOption(parent, option, optionValue, propName);
667+
}
653668
}
654669

655670
function parseOptionValue(parent, name) {
@@ -720,6 +735,12 @@ function parse(source, root, options) {
720735
parent.setOption(name, value);
721736
}
722737

738+
function setFeature(parent, name, value) {
739+
if (parent.setFeature) {
740+
parent.setFeature(name, value);
741+
}
742+
}
743+
723744
function setParsedOption(parent, name, value, propName) {
724745
if (parent.setParsedOption)
725746
parent.setParsedOption(name, value, propName);

tests/data/feature-resolution.proto

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
edition = "2023";
2+
3+
option features.amazing_feature = A;
4+
5+
service MyService {
6+
option features.amazing_feature = E;
7+
rpc MyMethod (MyRequest) returns (MyResponse) {
8+
option features.amazing_feature = L;
9+
};
10+
}
11+
12+
message Message {
13+
option features.amazing_feature = B;
14+
15+
string string_val = 1;
16+
repeated string string_repeated = 2 [features.amazing_feature = F];
17+
18+
uint64 uint64_val = 3;
19+
repeated uint64 uint64_repeated = 4;
20+
21+
bytes bytes_val = 5;
22+
repeated bytes bytes_repeated = 6;
23+
24+
SomeEnum enum_val = 7;
25+
repeated SomeEnum enum_repeated = 8;
26+
27+
extensions 10 to 100;
28+
extend Message {
29+
required int32 bar = 10 [features.amazing_feature = I];
30+
}
31+
32+
message Nested {
33+
option features.amazing_feature = H;
34+
optional int64 count = 9;
35+
}
36+
37+
enum SomeEnumInMessage {
38+
option features.amazing_feature = G;
39+
ONE = 11;
40+
TWO = 12;
41+
}
42+
43+
oneof SomeOneOf {
44+
option features.amazing_feature = J;
45+
int32 a = 13;
46+
string b = 14;
47+
}
48+
49+
map<string,int64> int64_map = 15;
50+
}
51+
52+
extend Message {
53+
required int32 bar = 16 [features.amazing_feature = D];
54+
}
55+
56+
enum SomeEnum {
57+
option features.amazing_feature = C;
58+
ONE = 1 [features.amazing_feature = K];
59+
TWO = 2;
60+
}

tests/feature_resolution_editions.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
1. Defaults
3+
2. File - A
4+
3. Message - B
5+
4. Enum - C
6+
5. File extension - D
7+
6. File service - E
8+
7. Message Field - F
9+
8. Message Enum - G
10+
9. Message Message - H
11+
10. Message Extension - I
12+
11. "one of" Field - J
13+
12. Enum value - K
14+
13. Service method - L
15+
16+
17+
18+
edition = "2023";
19+
20+
option features.amazing_feature = A;
21+
22+
service MyService {
23+
option features.amazing_feature = E;
24+
rpc MyMethod (MyRequest) returns (MyResponse) {
25+
option features.amazing_feature = L;
26+
};
27+
}
28+
29+
message Message {
30+
option features.amazing_feature = B;
31+
32+
string string_val = 1;
33+
repeated string string_repeated = 2 [features.amazing_feature = F];
34+
35+
uint64 uint64_val = 3;
36+
repeated uint64 uint64_repeated = 4;
37+
38+
bytes bytes_val = 5;
39+
repeated bytes bytes_repeated = 6;
40+
41+
SomeEnum enum_val = 7;
42+
repeated SomeEnum enum_repeated = 8;
43+
44+
extensions 10 to 100;
45+
extend Message {
46+
int32 bar = 10 [features.amazing_feature = I];
47+
}
48+
49+
message Nested {
50+
option features.amazing_feature = H;
51+
optional int32 count = 1;
52+
}
53+
54+
enum SomeEnum {
55+
option features.amazing_feature = G;
56+
ONE = 1;
57+
TWO = 2;
58+
}
59+
60+
oneof bar {
61+
option features.amazing_feature = J;
62+
int32 a = 1;
63+
string b = 2;
64+
}
65+
66+
map<string,int64> int64_map = 9;
67+
}
68+
69+
extend Message {
70+
int32 bar = 11 [features.amazing_feature = D];
71+
}
72+
73+
enum SomeEnum {
74+
option features.amazing_feature = C;
75+
ONE = 1 [features.amazing_feature = K];
76+
TWO = 2;
77+
}
78+
79+
*/
80+
81+
var tape = require("tape");
82+
83+
var protobuf = require("..");
84+
85+
86+
tape.test.only("feature resolution editions", function(test) {
87+
88+
protobuf.load("tests/data/feature-resolution.proto", function(err, root) {
89+
if (err)
90+
return test.fail(err.message);
91+
92+
// test.same(root.fea, {
93+
// 1: "a",
94+
// 2: "b"
95+
// }, "should also expose their values by id");
96+
97+
// console.log(root.features.amazing_feature)
98+
99+
test.same(root.features.amazing_feature, 'A');
100+
test.same(root.lookup("Message").features.amazing_feature, 'B')
101+
test.same(root.lookupService("MyService").features.amazing_feature, 'E');
102+
test.same(root.lookupEnum("SomeEnum").features.amazing_feature, 'C')
103+
test.same(root.lookup("Message").lookupEnum("SomeEnumInMessage").features.amazing_feature, 'G')
104+
test.same(root.lookup("Message").lookup("Nested").features.amazing_feature, 'H')
105+
test.same(root.lookupService("MyService").lookup("MyMethod").features.amazing_feature, 'L')
106+
test.same(root.lookup("Message").fields.stringRepeated.features.amazing_feature, 'F')
107+
test.same(root.lookup("Message").lookup(".Message.bar").features.amazing_feature, 'I')
108+
test.same(root.lookupEnum("SomeEnum").valuesFeatures.ONE.amazing_feature, 'K')
109+
110+
test.end();
111+
})
112+
113+
114+
115+
})

0 commit comments

Comments
 (0)