-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add feature resolution for protobuf editions #2029
base: master
Are you sure you want to change the base?
Conversation
tests/comp_options.js
Outdated
@@ -30,7 +30,7 @@ message Message { | |||
tape.test("complex options", function (test) { | |||
var root = protobuf.parse(proto).root; | |||
|
|||
test.deepEqual(root.parsedOptions[0], { | |||
test.deepEqual(root.parsedOptions[1], { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we now have defaults as the first parsed option, this test needs to look for the second parsed option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to put defaults into "parsedOptions"? That seems like the wrong place to store them, since they're global
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parsedOptions
are just options that have been properly parsed - in other words, a step after options. Since features are just options, and they need to be parsed just like options, they do need to go through this step. If you want I can delete them from options after they've been copied over into _features.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My confusion is why defaults end up here. Parsed features make sense to me, but you're explicitly adding edition defaults to parsedOptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha - yeah it seems it is related. I think the point is that we're taking advantage of the parsing setParsedOptions
offers, which ultimately is needed in feature resolution. If we move it outside of setting parsedOptions into some other feature resolution-specific method, we're essentially copying over reusable code. Same reason why we're caling setParsedOption here too, since it takes advantage of all the parsing already offered in that method.
src/object.js
Outdated
@@ -186,6 +207,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu | |||
if (!this.parsedOptions) { | |||
this.parsedOptions = []; | |||
} | |||
var isFeature = /features/.test(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it might have some false positive. Do we want to make sure that it starts with features.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're still missing an .
, this would pass for an option named featuresFoo
@@ -195,12 +217,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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to special-case features? They should all be scalar so we should never be trying to merge during parse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feature really only gets called when parsing Root, since root gets initialized with defaults, and then can be overwritten if there's options specified at the root level. For example, if we start off with editions syntax, and then immediately add the following at the root level:
option features.json_format = LEGACY_BEST_EFFORT;
Since the previous experience is to merge at each child, then the features-specific behavior gets lost, since we'd end up with an array of values (features.json_format = [LEGACY_BEST_EFFORT, ALLOW]
);
One option is to simply initialize defaults at the time that we set root-level options, so there's no overwriting, just 'filling in the blanks'. However this would require much of the same logic that setParsedOption
already does (i.e., searching for a property within the_proto_features
object), and it seems pretty un-DRY to rewrite it for features only because the setProperty value is different.
Instead, I'll change the isFeature
parameter to overwrite
in setProperty
to make it more general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds related to my confusion about calling setParsedOption
with defaults. Alternatively, you could just initialize _features
of the root to the relevant defaults during feature resolution right? i.e. if _parent
is null, initialize with the edition defaults
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but what if parent
isn't null? In that case we're rewriting a lot of the code that is in setParsedOption
to confirm whether or not a value exists and if to rewrite it, or to create a new property, etc.
src/parse.js
Outdated
@@ -338,13 +371,17 @@ function parse(source, root, options) { | |||
|
|||
case "required": | |||
case "repeated": | |||
if (edition) | |||
throw illegal(token); | |||
parseField(type, token); | |||
break; | |||
|
|||
case "optional": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to optional
and required
, we have the following grammar changes I don't see covered:
- "group" keyword is removed
- reserved field and enum value names are specified as identifiers, rather than string literals (i.e. no quotes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests for the grammar changes. As for the second point, I believe this is already covered by feature resolution tests since there are no quotes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that second case is covered, since you have no reserved fields. What I'm talking about is that for a proto2 message:
message Foo {
reserved "bar", "baz";
}
becomes this in edition 2023:
message Foo {
reserved bar, baz;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests.
src/parse.js
Outdated
dummy.setParsedOption = function(name, value, propName) { | ||
// In order to not change existing behavior, only calling | ||
// this for features | ||
if (/features/.test(name)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar issue with this regex I think
tests/comp_options.js
Outdated
@@ -30,7 +30,7 @@ message Message { | |||
tape.test("complex options", function (test) { | |||
var root = protobuf.parse(proto).root; | |||
|
|||
test.deepEqual(root.parsedOptions[0], { | |||
test.deepEqual(root.parsedOptions[1], { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to put defaults into "parsedOptions"? That seems like the wrong place to store them, since they're global
tests/feature_resolution_editions.js
Outdated
// Tests precedence for different levels of feature resolution | ||
tape.test("feature resolution editions precedence", function(test) { | ||
|
||
protobuf.load("tests/data/feature-resolution.proto", function(err, root) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be helpful to split up this proto into smaller ones, and define them inline per-test. I regret using monolithic protos in our tests, as it's much harder to understand what's being tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear you, but I think in this case it helps the test reader understand the overall precedence of options - at least it does for me. It's clear by the alphabetical order what has precedence in the grand scheme of things, rather than having to piece it together in each test. As long as it doesn't get used for other development, I think it's helpful to get the greater overall picture of precedence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that to understand this test you need to constantly be switching back and forth between two different files (as a reader, I find this test difficult to review for correctness). If you had much smaller protos defined inline in each test, it would be much clearer what the expectation is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, switched it.
src/object.js
Outdated
if (this.root instanceof Root) | ||
this.resolved = true; // only if part of a root | ||
this._resolveFeatures(); | ||
if (this.root instanceof Root || this.parent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this sufficient?
- We probably don't want to call
_resolveFeatures()
unless this condition is true - What if the parent is itself orphaned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Moved _resolveFeatures, I think you're right.
- If the parent itself is orphaned, then this wouldn't get called recursively since this usually only gets called under
resolveAll
.
src/object.js
Outdated
@@ -186,6 +207,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu | |||
if (!this.parsedOptions) { | |||
this.parsedOptions = []; | |||
} | |||
var isFeature = /features/.test(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're still missing an .
, this would pass for an option named featuresFoo
@@ -195,12 +217,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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds related to my confusion about calling setParsedOption
with defaults. Alternatively, you could just initialize _features
of the root to the relevant defaults during feature resolution right? i.e. if _parent
is null, initialize with the edition defaults
No description provided.