Skip to content

Commit 68b5339

Browse files
committed
feat: add feature resolution and tests
1 parent 8dabd5e commit 68b5339

8 files changed

+237
-79
lines changed

src/enum.js

+24-14
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, valuesFeatures) {
23+
function Enum(name, values, options, comment, comments, valuesOptions) {
2424
ReflectionObject.call(this, name, options);
2525

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

59-
/**
60-
* Values features, if any
61-
* @type {Object<string, Object<string, *>>|undefined}
62-
*/
63-
this.valuesFeatures = valuesFeatures;
59+
this._valuesFeatures = {};
6460

6561
/**
6662
* Reserved ranges, if any.
@@ -125,7 +121,7 @@ Enum.prototype.toJSON = function toJSON(toJSONOptions) {
125121
* @throws {TypeError} If arguments are invalid
126122
* @throws {Error} If there is already a value with this name or id
127123
*/
128-
Enum.prototype.add = function add(name, id, comment, options, features) {
124+
Enum.prototype.add = function add(name, id, comment, options) {
129125
// utilized by the parser but not by .fromJSON
130126

131127
if (!util.isString(name))
@@ -154,14 +150,30 @@ Enum.prototype.add = function add(name, id, comment, options, features) {
154150
if (this.valuesOptions === undefined)
155151
this.valuesOptions = {};
156152
this.valuesOptions[name] = options || null;
157-
}
158153

159-
if (features) {
160-
if (this.valuesFeatures === undefined)
161-
this.valuesFeatures = {};
162-
this.valuesFeatures[name] = features || null;
154+
// console.log(options)
155+
// if (/features/.test(options)) {
156+
// var features = Object.keys(this.valuesOptions).find(x => {return x.hasOwnProperty("features")});
157+
// this._valuesFeatures[name] = features ?? {};
158+
// console.log(this._valuesFeatures[name])
159+
// }
160+
161+
console.log(this.valuesOptions)
162+
for (var key in this.valuesOptions) {
163+
console.log(this.valuesOptions[key])
164+
var features = Array.isArray(this.valuesOptions) ? this.valuesOptions[key].find(x => {return x.hasOwnProperty("features")}) : this.valuesOptions[key] === "features";
165+
if (features) {
166+
if (!this._valuesFeatures) {
167+
this._valuesFeatures = {};
168+
}
169+
this._valuesFeatures[key] = features.features || {};
170+
}
171+
}
163172
}
164173

174+
175+
// console.log(this.valuesOptions)
176+
165177
this.comments[name] = comment || null;
166178
return this;
167179
};
@@ -188,8 +200,6 @@ Enum.prototype.remove = function remove(name) {
188200
if (this.valuesOptions)
189201
delete this.valuesOptions[name];
190202

191-
if (this.valuesFeatures)
192-
delete this.valuesFeatures[name];
193203
return this;
194204
};
195205

src/object.js

+21-6
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function ReflectionObject(name, options) {
4444
/**
4545
* Resolved Features.
4646
*/
47-
this.features = null;
47+
this._features = null;
4848

4949
/**
5050
* Parent namespace.
@@ -151,11 +151,19 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
151151
ReflectionObject.prototype.resolve = function resolve() {
152152
if (this.resolved)
153153
return this;
154-
if (this.root instanceof Root)
155-
this.resolved = true; // only if part of a root
154+
this._resolveFeatures();
155+
// if (this.root instanceof Root)
156+
this.resolved = true;
156157
return this;
157158
};
158159

160+
ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
161+
this._features = {...this.parent?._features ?? {}, ...this._features};
162+
if (this.parent) {
163+
this.parent._resolveFeatures();
164+
}
165+
}
166+
159167
/**
160168
* Gets an option value.
161169
* @param {string} name Option name
@@ -202,6 +210,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
202210
if (!this.parsedOptions) {
203211
this.parsedOptions = [];
204212
}
213+
var isFeature = /features/.test(name);
205214
var parsedOptions = this.parsedOptions;
206215
if (propName) {
207216
// If setting a sub property of an option then try to merge it
@@ -211,12 +220,13 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
211220
});
212221
if (opt) {
213222
// If we found an existing option - just merge the property value
223+
// (If it's a feature, will just write over)
214224
var newValue = opt[name];
215-
util.setProperty(newValue, propName, value);
225+
util.setProperty(newValue, propName, value, isFeature);
216226
} else {
217-
// otherwise, create a new option, set it's property and add it to the list
227+
// otherwise, create a new option, set its property and add it to the list
218228
opt = {};
219-
opt[name] = util.setProperty({}, propName, value);
229+
opt[name] = util.setProperty({}, propName, value, isFeature);
220230
parsedOptions.push(opt);
221231
}
222232
} else {
@@ -225,6 +235,11 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
225235
newOpt[name] = value;
226236
parsedOptions.push(newOpt);
227237
}
238+
239+
if (isFeature) {
240+
var features = parsedOptions.find(x => {return x.hasOwnProperty("features")});
241+
this._features = features.features ?? {};
242+
}
228243
return this;
229244
};
230245

src/parse.js

+82-33
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var tokenize = require("./tokenize"),
1313
Enum = require("./enum"),
1414
Service = require("./service"),
1515
Method = require("./method"),
16+
ReflectionObject = require("./object"),
1617
types = require("./types"),
1718
util = require("./util");
1819

@@ -26,7 +27,11 @@ var base10Re = /^[1-9][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]*)*$/,
2829
fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/,
29-
featuresRefRe = /features\.([a-zA-Z_]*)/;
30+
featuresTypeRefRe = /^(features)(.*)/;
31+
32+
var editions2023Defaults = {features: {enum_type: 'OPEN', field_presence: 'EXPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'}}
33+
var proto2Defaults = {features: {enum_type: 'CLOSED', field_presence: 'EXPLICIT', json_format: 'LEGACY_BEST_EFFORT', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'EXPANDED', utf8_validation: 'NONE'}}
34+
var proto3Defaults = {features: {enum_type: 'OPEN', field_presence: 'IMPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'}}
3035

3136
/**
3237
* Result object returned from {@link parse}.
@@ -269,6 +274,16 @@ function parse(source, root, options) {
269274
// Otherwise the meaning is ambiguous between proto2 and proto3
270275
root.setOption("syntax", syntax);
271276

277+
if (isProto3) {
278+
for (var key of Object.keys(proto3Defaults)) {
279+
setParsedOption(root, key, proto3Defaults[key])
280+
}
281+
} else {
282+
for (var key of Object.keys(proto2Defaults)) {
283+
setParsedOption(root, key, proto2Defaults[key])
284+
}
285+
}
286+
272287
skip(";");
273288
}
274289

@@ -283,6 +298,9 @@ function parse(source, root, options) {
283298

284299
root.setOption("edition", edition);
285300

301+
for (var key of Object.keys(editions2023Defaults)) {
302+
setParsedOption(root, key, editions2023Defaults[key])
303+
}
286304
skip(";");
287305
}
288306

@@ -355,13 +373,17 @@ function parse(source, root, options) {
355373

356374
case "required":
357375
case "repeated":
376+
if (edition)
377+
throw illegal(token)
358378
parseField(type, token);
359379
break;
360380

361381
case "optional":
362382
/* istanbul ignore if */
363383
if (isProto3) {
364384
parseField(type, "proto3_optional");
385+
} else if (edition) {
386+
throw illegal(token);
365387
} else {
366388
parseField(type, "optional");
367389
}
@@ -416,6 +438,7 @@ function parse(source, root, options) {
416438
var name = next();
417439

418440
/* istanbul ignore if */
441+
419442
if (!nameRe.test(name))
420443
throw illegal(name, "name");
421444

@@ -605,13 +628,43 @@ function parse(source, root, options) {
605628
dummy.setOption = function(name, value) {
606629
if (this.options === undefined)
607630
this.options = {};
631+
608632
this.options[name] = value;
609633
};
610-
dummy.setFeature = function(name, value) {
611-
if (this.features === undefined)
612-
this.features = {};
613-
this.features[name] = value;
614-
};
634+
dummy.setParsedOption = function(name, value, propName) {
635+
if (!this.parsedOptions) {
636+
this.parsedOptions = [];
637+
}
638+
var parsedOptions = this.parsedOptions;
639+
if (propName) {
640+
// If setting a sub property of an option then try to merge it
641+
// with an existing option
642+
var opt = parsedOptions.find(function (opt) {
643+
return Object.prototype.hasOwnProperty.call(opt, name);
644+
});
645+
if (opt) {
646+
// If we found an existing option - just merge the property value
647+
// (If it's a feature, will just write over)
648+
var newValue = opt[name];
649+
util.setProperty(newValue, propName, value);
650+
} else {
651+
// otherwise, create a new option, set its property and add it to the list
652+
opt = {};
653+
opt[name] = util.setProperty({}, propName, value);
654+
parsedOptions.push(opt);
655+
}
656+
} else {
657+
// Always create a new option when setting the value of the option itself
658+
var newOpt = {};
659+
newOpt[name] = value;
660+
parsedOptions.push(newOpt);
661+
}
662+
663+
if (/features/.test(name)) {
664+
var features = parsedOptions.find(x => {return x.hasOwnProperty("features")});
665+
this._features = features.features || {};
666+
}
667+
}
615668
ifBlock(dummy, function parseEnumValue_block(token) {
616669

617670
/* istanbul ignore else */
@@ -624,48 +677,52 @@ function parse(source, root, options) {
624677
}, function parseEnumValue_line() {
625678
parseInlineOptions(dummy); // skip
626679
});
627-
parent.add(token, value, dummy.comment, dummy.options, dummy.features);
680+
parent.add(token, value, dummy.comment, dummy.parsedOptions);
628681
}
629682

630683
function parseOption(parent, token) {
631-
// console.log(featuresRefRe.test(token = next()))
632-
if (featuresRefRe.test(peek())) {
684+
var name;
685+
var option;
686+
var optionValue;
687+
var propName;
688+
// The two logic branches below are parallel tracks, but with different regexes for the following use cases:
689+
// features expects: option features.abc.amazing_feature = A;
690+
// custom options expects: option (mo_single_msg).nested.value = "x";
691+
if (featuresTypeRefRe.test(peek())) {
633692
var token = next();
634-
var name = token.match(featuresRefRe)[1]
635-
skip("=");
636-
setFeature(parent, name, token = next())
693+
name = token;
694+
option = token.match(featuresTypeRefRe)[1];
695+
var propNameWithPeriod = token.match(featuresTypeRefRe)[2];
696+
if (fqTypeRefRe.test(propNameWithPeriod)) {
697+
propName = propNameWithPeriod.slice(1); //remove '.' before property name
698+
}
637699
} else {
638700
var isCustom = skip("(", true);
639701
if (!typeRefRe.test(token = next()))
640702
throw illegal(token, "name");
641703

642704

643-
var name = token;
644-
var option = name;
645-
var propName;
705+
name = token;
706+
option = name;
646707

647708
if (isCustom) {
648709
skip(")");
649710
name = "(" + name + ")";
650711
option = name;
651712
token = peek();
652-
console.log('in custom?'+token)
653713
if (fqTypeRefRe.test(token)) {
654714
propName = token.slice(1); //remove '.' before property name
655715
name += token;
656716
next();
657717
}
658718
}
659-
660-
console.log(token)
719+
}
661720
skip("=");
662721
var optionValue = parseOptionValue(parent, name);
663722
setParsedOption(parent, option, optionValue, propName);
664-
}
665723
}
666724

667725
function parseOptionValue(parent, name) {
668-
// { a: "foo" b { c: "bar" } }
669726
if (skip("{", true)) {
670727
var objectResult = {};
671728

@@ -683,12 +740,9 @@ function parse(source, root, options) {
683740

684741
skip(":", true);
685742

686-
if (peek() === "{")
743+
if (peek() === "{") {
687744
value = parseOptionValue(parent, name + "." + token);
688-
else if (peek() === "[") {
689-
// option (my_option) = {
690-
// repeated_value: [ "foo", "bar" ]
691-
// };
745+
} else if (peek() === "[") {
692746
value = [];
693747
var lastValue;
694748
if (skip("[", true)) {
@@ -732,12 +786,6 @@ function parse(source, root, options) {
732786
parent.setOption(name, value);
733787
}
734788

735-
function setFeature(parent, name, value) {
736-
if (parent.setFeature) {
737-
parent.setFeature(name, value);
738-
}
739-
}
740-
741789
function setParsedOption(parent, name, value, propName) {
742790
if (parent.setParsedOption)
743791
parent.setParsedOption(name, value, propName);
@@ -761,8 +809,9 @@ function parse(source, root, options) {
761809

762810
var service = new Service(token);
763811
ifBlock(service, function parseService_block(token) {
764-
if (parseCommon(service, token))
812+
if (parseCommon(service, token)) {
765813
return;
814+
}
766815

767816
/* istanbul ignore else */
768817
if (token === "rpc")
@@ -849,7 +898,7 @@ function parse(source, root, options) {
849898

850899
default:
851900
/* istanbul ignore if */
852-
if (!isProto3 || !typeRefRe.test(token))
901+
if ((!isProto3 && !edition) || !typeRefRe.test(token))
853902
throw illegal(token);
854903
push(token);
855904
parseField(parent, "optional", reference);

0 commit comments

Comments
 (0)