Skip to content

Commit

Permalink
feat: add feature resolution and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sofisl committed Sep 14, 2024
1 parent 8dabd5e commit 68b5339
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 79 deletions.
38 changes: 24 additions & 14 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, valuesFeatures) {
function Enum(name, values, options, comment, comments, valuesOptions) {
ReflectionObject.call(this, name, options);

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

/**
* Values features, if any
* @type {Object<string, Object<string, *>>|undefined}
*/
this.valuesFeatures = valuesFeatures;
this._valuesFeatures = {};

/**
* Reserved ranges, if any.
Expand Down Expand Up @@ -125,7 +121,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, features) {
Enum.prototype.add = function add(name, id, comment, options) {
// utilized by the parser but not by .fromJSON

if (!util.isString(name))
Expand Down Expand Up @@ -154,14 +150,30 @@ Enum.prototype.add = function add(name, id, comment, options, features) {
if (this.valuesOptions === undefined)
this.valuesOptions = {};
this.valuesOptions[name] = options || null;
}

if (features) {
if (this.valuesFeatures === undefined)
this.valuesFeatures = {};
this.valuesFeatures[name] = features || null;
// console.log(options)
// if (/features/.test(options)) {
// var features = Object.keys(this.valuesOptions).find(x => {return x.hasOwnProperty("features")});
// this._valuesFeatures[name] = features ?? {};
// console.log(this._valuesFeatures[name])
// }

console.log(this.valuesOptions)
for (var key in this.valuesOptions) {
console.log(this.valuesOptions[key])
var features = Array.isArray(this.valuesOptions) ? this.valuesOptions[key].find(x => {return x.hasOwnProperty("features")}) : this.valuesOptions[key] === "features";
if (features) {
if (!this._valuesFeatures) {
this._valuesFeatures = {};
}
this._valuesFeatures[key] = features.features || {};
}
}
}


// console.log(this.valuesOptions)

this.comments[name] = comment || null;
return this;
};
Expand All @@ -188,8 +200,6 @@ Enum.prototype.remove = function remove(name) {
if (this.valuesOptions)
delete this.valuesOptions[name];

if (this.valuesFeatures)
delete this.valuesFeatures[name];
return this;
};

Expand Down
27 changes: 21 additions & 6 deletions src/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function ReflectionObject(name, options) {
/**
* Resolved Features.
*/
this.features = null;
this._features = null;

/**
* Parent namespace.
Expand Down Expand Up @@ -151,11 +151,19 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
ReflectionObject.prototype.resolve = function resolve() {
if (this.resolved)
return this;
if (this.root instanceof Root)
this.resolved = true; // only if part of a root
this._resolveFeatures();
// if (this.root instanceof Root)
this.resolved = true;
return this;
};

ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
this._features = {...this.parent?._features ?? {}, ...this._features};
if (this.parent) {
this.parent._resolveFeatures();
}
}

/**
* Gets an option value.
* @param {string} name Option name
Expand Down Expand Up @@ -202,6 +210,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
if (!this.parsedOptions) {
this.parsedOptions = [];
}
var isFeature = /features/.test(name);
var parsedOptions = this.parsedOptions;
if (propName) {
// If setting a sub property of an option then try to merge it
Expand All @@ -211,12 +220,13 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
});
if (opt) {
// If we found an existing option - just merge the property value
// (If it's a feature, will just write over)
var newValue = opt[name];
util.setProperty(newValue, propName, value);
util.setProperty(newValue, propName, value, isFeature);
} else {
// otherwise, create a new option, set it's property and add it to the list
// otherwise, create a new option, set its property and add it to the list
opt = {};
opt[name] = util.setProperty({}, propName, value);
opt[name] = util.setProperty({}, propName, value, isFeature);
parsedOptions.push(opt);
}
} else {
Expand All @@ -225,6 +235,11 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
newOpt[name] = value;
parsedOptions.push(newOpt);
}

if (isFeature) {
var features = parsedOptions.find(x => {return x.hasOwnProperty("features")});
this._features = features.features ?? {};
}
return this;
};

Expand Down
115 changes: 82 additions & 33 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var tokenize = require("./tokenize"),
Enum = require("./enum"),
Service = require("./service"),
Method = require("./method"),
ReflectionObject = require("./object"),
types = require("./types"),
util = require("./util");

Expand All @@ -26,7 +27,11 @@ var base10Re = /^[1-9][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]*)+$/,
featuresRefRe = /features\.([a-zA-Z_]*)/;
featuresTypeRefRe = /^(features)(.*)/;

var editions2023Defaults = {features: {enum_type: 'OPEN', field_presence: 'EXPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'}}
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'}}
var proto3Defaults = {features: {enum_type: 'OPEN', field_presence: 'IMPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'}}

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

if (isProto3) {
for (var key of Object.keys(proto3Defaults)) {
setParsedOption(root, key, proto3Defaults[key])
}
} else {
for (var key of Object.keys(proto2Defaults)) {
setParsedOption(root, key, proto2Defaults[key])
}
}

skip(";");
}

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

root.setOption("edition", edition);

for (var key of Object.keys(editions2023Defaults)) {
setParsedOption(root, key, editions2023Defaults[key])
}
skip(";");
}

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

case "required":
case "repeated":
if (edition)
throw illegal(token)
parseField(type, token);
break;

case "optional":
/* istanbul ignore if */
if (isProto3) {
parseField(type, "proto3_optional");
} else if (edition) {
throw illegal(token);
} else {
parseField(type, "optional");
}
Expand Down Expand Up @@ -416,6 +438,7 @@ function parse(source, root, options) {
var name = next();

/* istanbul ignore if */

if (!nameRe.test(name))
throw illegal(name, "name");

Expand Down Expand Up @@ -605,13 +628,43 @@ function parse(source, root, options) {
dummy.setOption = function(name, value) {
if (this.options === undefined)
this.options = {};

this.options[name] = value;
};
dummy.setFeature = function(name, value) {
if (this.features === undefined)
this.features = {};
this.features[name] = value;
};
dummy.setParsedOption = function(name, value, propName) {
if (!this.parsedOptions) {
this.parsedOptions = [];
}
var parsedOptions = this.parsedOptions;
if (propName) {
// If setting a sub property of an option then try to merge it
// with an existing option
var opt = parsedOptions.find(function (opt) {
return Object.prototype.hasOwnProperty.call(opt, name);
});
if (opt) {
// If we found an existing option - just merge the property value
// (If it's a feature, will just write over)
var newValue = opt[name];
util.setProperty(newValue, propName, value);
} else {
// otherwise, create a new option, set its property and add it to the list
opt = {};
opt[name] = util.setProperty({}, propName, value);
parsedOptions.push(opt);
}
} else {
// Always create a new option when setting the value of the option itself
var newOpt = {};
newOpt[name] = value;
parsedOptions.push(newOpt);
}

if (/features/.test(name)) {
var features = parsedOptions.find(x => {return x.hasOwnProperty("features")});
this._features = features.features || {};
}
}
ifBlock(dummy, function parseEnumValue_block(token) {

/* istanbul ignore else */
Expand All @@ -624,48 +677,52 @@ function parse(source, root, options) {
}, function parseEnumValue_line() {
parseInlineOptions(dummy); // skip
});
parent.add(token, value, dummy.comment, dummy.options, dummy.features);
parent.add(token, value, dummy.comment, dummy.parsedOptions);
}

function parseOption(parent, token) {
// console.log(featuresRefRe.test(token = next()))
if (featuresRefRe.test(peek())) {
var name;
var option;
var optionValue;
var propName;
// The two logic branches below are parallel tracks, but with different regexes for the following use cases:
// features expects: option features.abc.amazing_feature = A;
// custom options expects: option (mo_single_msg).nested.value = "x";
if (featuresTypeRefRe.test(peek())) {
var token = next();
var name = token.match(featuresRefRe)[1]
skip("=");
setFeature(parent, name, token = next())
name = token;
option = token.match(featuresTypeRefRe)[1];
var propNameWithPeriod = token.match(featuresTypeRefRe)[2];
if (fqTypeRefRe.test(propNameWithPeriod)) {
propName = propNameWithPeriod.slice(1); //remove '.' before property name
}
} else {
var isCustom = skip("(", true);
if (!typeRefRe.test(token = next()))
throw illegal(token, "name");


var name = token;
var option = name;
var propName;
name = token;
option = name;

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

console.log(token)
}
skip("=");
var optionValue = parseOptionValue(parent, name);
setParsedOption(parent, option, optionValue, propName);
}
}

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

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

skip(":", true);

if (peek() === "{")
if (peek() === "{") {
value = parseOptionValue(parent, name + "." + token);
else if (peek() === "[") {
// option (my_option) = {
// repeated_value: [ "foo", "bar" ]
// };
} else if (peek() === "[") {
value = [];
var lastValue;
if (skip("[", true)) {
Expand Down Expand Up @@ -732,12 +786,6 @@ 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 All @@ -761,8 +809,9 @@ function parse(source, root, options) {

var service = new Service(token);
ifBlock(service, function parseService_block(token) {
if (parseCommon(service, token))
if (parseCommon(service, token)) {
return;
}

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

default:
/* istanbul ignore if */
if (!isProto3 || !typeRefRe.test(token))
if ((!isProto3 && !edition) || !typeRefRe.test(token))
throw illegal(token);
push(token);
parseField(parent, "optional", reference);
Expand Down
Loading

0 comments on commit 68b5339

Please sign in to comment.