diff --git a/src/enum.js b/src/enum.js index d6f4f175b..6c65dc527 100644 --- a/src/enum.js +++ b/src/enum.js @@ -57,11 +57,17 @@ function Enum(name, values, options, comment, comments, valuesOptions) { this.valuesOptions = valuesOptions; /** - * Values features, if any + * Resolved values features, if any * @type {Object>|undefined} */ this._valuesFeatures = {}; + /** + * Unresolved values features, if any + * @type {Object>|undefined} + */ + this._proto_valuesFeatures = {}; + /** * Reserved ranges, if any. * @type {Array.} @@ -78,6 +84,28 @@ function Enum(name, values, options, comment, comments, valuesOptions) { this.valuesById[ this.values[keys[i]] = values[keys[i]] ] = keys[i]; } +/** + * Resolves value features + * @returns {Enum} `this` + */ +Enum.prototype.resolve = function resolve() { + + if (this.resolved) + return this; + + for (var key of Object.keys(this._proto_valuesFeatures)) { + + if (this.parent) { + var parentFeaturesCopy = Object.assign({}, this.parent._features); + this._valuesFeatures[key] = Object.assign(parentFeaturesCopy, this._proto_valuesFeatures[key] || {}); + } else { + this._valuesFeatures[key] = Object.assign({}, this._proto_valuesFeatures[key]); + } + } + return ReflectionObject.prototype.resolve.call(this); +}; + + /** * Enum descriptor. * @interface IEnum @@ -158,14 +186,19 @@ Enum.prototype.add = function add(name, id, comment, options) { for (var key of Object.keys(this.valuesOptions)) { var features = Array.isArray(this.valuesOptions[key]) ? this.valuesOptions[key].find(x => {return Object.prototype.hasOwnProperty.call(x, "features");}) : this.valuesOptions[key] === "features"; if (features) { - if (!this._valuesFeatures) { - this._valuesFeatures = {}; - } - this._valuesFeatures[key] = features.features || {}; + this._proto_valuesFeatures[key] = features.features; + } else { + this._proto_valuesFeatures[key] = {}; } } } + for (var key of Object.keys(this.values)) { + if (!this._proto_valuesFeatures[key]) { + this._proto_valuesFeatures[key] = {} + } + } + this.comments[name] = comment || null; return this; }; diff --git a/src/namespace.js b/src/namespace.js index 731afc75f..b7788b892 100644 --- a/src/namespace.js +++ b/src/namespace.js @@ -302,12 +302,13 @@ Namespace.prototype.define = function define(path, json) { */ Namespace.prototype.resolveAll = function resolveAll() { var nested = this.nestedArray, i = 0; + this.resolve(); while (i < nested.length) if (nested[i] instanceof Namespace) nested[i++].resolveAll(); else nested[i++].resolve(); - return this.resolve(); + return this; }; /** diff --git a/src/object.js b/src/object.js index 6cafcaab4..3655f6958 100644 --- a/src/object.js +++ b/src/object.js @@ -7,6 +7,7 @@ var util = require("./util"); var Root; // cyclic +// TODO: Replace with embedded proto. var editions2023Defaults = {enum_type: "OPEN", field_presence: "EXPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"}; var proto2Defaults = {enum_type: "CLOSED", field_presence: "EXPLICIT", json_format: "LEGACY_BEST_EFFORT", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "EXPANDED", utf8_validation: "NONE"}; var proto3Defaults = {enum_type: "OPEN", field_presence: "IMPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"}; @@ -51,7 +52,7 @@ function ReflectionObject(name, options) { this._features = {}; /** - * Resolved Features. + * Unresolved Features. */ this._proto_features = null; @@ -160,10 +161,10 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) { ReflectionObject.prototype.resolve = function resolve() { if (this.resolved) return this; - if (this.root instanceof Root || this.parent) { + if (this instanceof Root || this.parent && this.parent.resolved) this._resolveFeatures(); + if (this.root instanceof Root) this.resolved = true; - } return this; }; @@ -174,28 +175,24 @@ ReflectionObject.prototype.resolve = function resolve() { ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() { var defaults = {}; - if (this.root.getOption("syntax") === "proto2") { - defaults = Object.assign({}, proto2Defaults); - } else if (this.root.getOption("syntax") === "proto3") { - defaults = Object.assign({}, proto3Defaults); - } else if (this.root.getOption("edition") === "2023") { - defaults = Object.assign({}, editions2023Defaults); + if (this instanceof Root) { + if (this.root.getOption("syntax") === "proto2") { + defaults = Object.assign({}, proto2Defaults); + } else if (this.root.getOption("syntax") === "proto3") { + defaults = Object.assign({}, proto3Defaults); + } else if (this.root.getOption("edition") === "2023") { + defaults = Object.assign({}, editions2023Defaults); + } } - if (this.parent) { - // This is an annoying workaround since we can't use the spread operator - // (Breaks the bundler and eslint) - // If we don't create a shallow copy, we end up also altering the parent's - // features - var parentFeaturesMerged = Object.assign(defaults, this.parent._proto_features); - this._features = Object.assign(parentFeaturesMerged, this._proto_features || {}); - this._proto_features = this._features; - this.parent._resolveFeatures(); - } else { + if (this instanceof Root) { this._features = Object.assign(defaults, this._proto_features || {}); + } else if (this.parent) { + var parentFeaturesCopy = Object.assign({}, this.parent._features); + this._features = Object.assign(parentFeaturesCopy, this._proto_features || {}); + } else { + this._features = Object.assign({}, this._proto_features); } - this._proto_features = this._features; - }; /** diff --git a/src/parse.js b/src/parse.js index 8135aa2f0..3ac4c9cb9 100644 --- a/src/parse.js +++ b/src/parse.js @@ -147,6 +147,8 @@ function parse(source, root, options) { } catch (err) { if (typeRefRe.test(token) && edition) { target.push(token); + } else { + throw err; } } } diff --git a/src/service.js b/src/service.js index bc2c3080c..80b648001 100644 --- a/src/service.js +++ b/src/service.js @@ -106,10 +106,11 @@ Service.prototype.get = function get(name) { * @override */ Service.prototype.resolveAll = function resolveAll() { + Namespace.prototype.resolve.call(this); var methods = this.methodsArray; for (var i = 0; i < methods.length; ++i) methods[i].resolve(); - return Namespace.prototype.resolve.call(this); + return this; }; /** diff --git a/src/type.js b/src/type.js index b477a4ee2..30f2b31a2 100644 --- a/src/type.js +++ b/src/type.js @@ -299,13 +299,14 @@ Type.prototype.toJSON = function toJSON(toJSONOptions) { * @override */ Type.prototype.resolveAll = function resolveAll() { + Namespace.prototype.resolveAll.call(this); var fields = this.fieldsArray, i = 0; while (i < fields.length) fields[i++].resolve(); var oneofs = this.oneofsArray; i = 0; while (i < oneofs.length) oneofs[i++].resolve(); - return Namespace.prototype.resolveAll.call(this); + return this; }; /** diff --git a/tests/feature_resolution_editions.js b/tests/feature_resolution_editions.js index cf123e5bc..cf5e4fc1c 100644 --- a/tests/feature_resolution_editions.js +++ b/tests/feature_resolution_editions.js @@ -77,122 +77,15 @@ var proto2Defaults = {enum_type: 'CLOSED', field_presence: 'EXPLICIT', json_form var proto3Defaults = {enum_type: 'OPEN', field_presence: 'IMPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'} -var protoEditions2023Overridden = `edition = "2023"; -option features.json_format = LEGACY_BEST_EFFORT; -option features.(abc).d_e = deeply_nested_false; +// var test1 = +// var test3 = -message Message { - string string_val = 1; - string string_repeated = 2 [features.enum_type = CLOSED]; - - message Nested { - option features.(abc).d_e = deeply_nested_true; - option features.field_presence = IMPLICIT; - int64 count = 9; - } -} -` - -var test1 =`edition = "2023"; - -option features.amazing_feature = A;` -var test2 = `edition = "2023"; -option features.amazing_feature = A; - -message Message { - option features.amazing_feature = B; -}` -var test3 = `edition = "2023"; -option features.amazing_feature = A; -enum SomeEnum { - option features.amazing_feature = C; - ONE = 1; - TWO = 2; -}` - -var test4 = `edition = "2023"; -option features.amazing_feature = A; - -message Message { - option features.amazing_feature = B; -} - -extend Message { - int32 bar = 16 [features.amazing_feature = D]; -} -` -var test5 = `edition = "2023"; -option features.amazing_feature = A; -service MyService { - option features.amazing_feature = E; - message MyRequest {}; - message MyResponse {}; -} -` -var test6 = `edition = "2023"; -option features.amazing_feature = A; -message Message { - string string_val = 1; - string string_repeated = 2 [features.amazing_feature = F]; -}` - -var test7 = `edition = "2023"; -option features.amazing_feature = A; -message Message { - enum SomeEnumInMessage { - option features.amazing_feature = G; - ONE = 11; - TWO = 12; - } -}` -var test8 = `edition = "2023"; -option features.amazing_feature = A; -message Message { - message Nested { - option features.amazing_feature = H; - int64 count = 9; - } -}` -var test9 = `edition = "2023"; -option features.amazing_feature = A; -message Message { - extend Message { - int32 bar = 10 [features.amazing_feature = I]; - } -}` -var test10 = `edition = "2023"; -option features.amazing_feature = A; -message Message { - oneof SomeOneOf { - option features.amazing_feature = J; - int32 a = 13; - string b = 14; - } -}` -var test11 = `edition = "2023"; -option features.amazing_feature = A; -enum SomeEnum { - option features.amazing_feature = C; - ONE = 1 [features.amazing_feature = K]; - TWO = 2; -}` -var test12 = `edition = "2023"; -option features.amazing_feature = A; -service MyService { - option features.amazing_feature = E; - message MyRequest {}; - message MyResponse {}; - rpc MyMethod (MyRequest) returns (MyResponse) { - option features.amazing_feature = L; - }; -} -` tape.test("feature resolution defaults", function(test) { var rootEditions = protobuf.parse(protoEditions2023).root; rootEditions.resolveAll(); @@ -210,8 +103,26 @@ tape.test("feature resolution defaults", function(test) { }) tape.test("feature resolution inheritance", function(test) { - var rootEditionsOverriden = protobuf.parse(protoEditions2023Overridden).root - + var rootEditionsOverriden = protobuf.parse(`edition = "2023"; + option features.json_format = LEGACY_BEST_EFFORT; + + option features.(abc).d_e = deeply_nested_false; + + message Message { + string string_val = 1; + string string_repeated = 2 [features.enum_type = CLOSED]; + + message Nested { + option features.(abc).d_e = deeply_nested_true; + option features.field_presence = IMPLICIT; + int64 count = 9; + } + + enum SomeEnum { + ONE = 1 [features.repeated_field_encoding = EXPANDED]; + TWO = 2; + } + }`).root rootEditionsOverriden.resolveAll(); // Should flip enum_type from default setting, inherit from Message, @@ -227,36 +138,172 @@ tape.test("feature resolution inheritance", function(test) { }) // Should inherit from default, and Message, only change field_presence and the custom extension - test.same(rootEditionsOverriden.lookup("Message").lookup("Nested")._features, - { enum_type: 'OPEN', field_presence: 'IMPLICIT', json_format: 'LEGACY_BEST_EFFORT', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', '(abc)': { d_e: 'deeply_nested_true' } }) + test.same(rootEditionsOverriden.lookup("Message").lookup("Nested")._features, { + enum_type: 'OPEN', + field_presence: 'IMPLICIT', + json_format: 'LEGACY_BEST_EFFORT', + message_encoding: 'LENGTH_PREFIXED', + repeated_field_encoding: 'PACKED', + utf8_validation: 'VERIFY', + '(abc)': { d_e: 'deeply_nested_true' } + }) + + test.same(rootEditionsOverriden.lookup("Message").lookup("Nested")._features, { + enum_type: 'OPEN', + field_presence: 'IMPLICIT', + json_format: 'LEGACY_BEST_EFFORT', + message_encoding: 'LENGTH_PREFIXED', + repeated_field_encoding: 'PACKED', + utf8_validation: 'VERIFY', + '(abc)': { d_e: 'deeply_nested_true' } + }) + + test.same(rootEditionsOverriden.lookupEnum("SomeEnum")._valuesFeatures["ONE"], { + enum_type: 'OPEN', + field_presence: 'EXPLICIT', + json_format: 'LEGACY_BEST_EFFORT', + message_encoding: 'LENGTH_PREFIXED', + repeated_field_encoding: 'EXPANDED', + utf8_validation: 'VERIFY', + '(abc)': { d_e: 'deeply_nested_false' } + }) + + test.same(rootEditionsOverriden.lookupEnum("SomeEnum")._valuesFeatures["TWO"], { + enum_type: 'OPEN', + field_presence: 'EXPLICIT', + json_format: 'LEGACY_BEST_EFFORT', + message_encoding: 'LENGTH_PREFIXED', + repeated_field_encoding: 'PACKED', + utf8_validation: 'VERIFY', + '(abc)': { d_e: 'deeply_nested_false' } + }) test.end(); }) // Tests precedence for different levels of feature resolution tape.test("feature resolution editions precedence", function(test) { - var root1 = protobuf.parse(test1).root.resolveAll() - var root2 = protobuf.parse(test2).root.resolveAll(); - var root3 = protobuf.parse(test3).root.resolveAll(); - var root4 = protobuf.parse(test4).root.resolveAll(); - var root5 = protobuf.parse(test5).root.resolveAll(); - var root6 = protobuf.parse(test6).root.resolveAll(); - var root7 = protobuf.parse(test7).root.resolveAll(); - var root8 = protobuf.parse(test8).root.resolveAll(); - var root9 = protobuf.parse(test9).root.resolveAll(); - var root10 = protobuf.parse(test10).root.resolveAll(); - var root11 = protobuf.parse(test11).root.resolveAll(); - var root12 = protobuf.parse(test12).root.resolveAll(); + var root1 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A;`).root.resolveAll() + test.same(root1._features.amazing_feature, 'A'); + + var root2 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + + message Message { + option features.amazing_feature = B; + }`).root.resolveAll(); + test.same(root2.lookup("Message")._features.amazing_feature, 'B') + + var root3 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + enum SomeEnum { + option features.amazing_feature = C; + ONE = 1; + TWO = 2; + }`).root.resolveAll(); + test.same(root3.lookupEnum("SomeEnum")._features.amazing_feature, 'C') + + var root4 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + + message Message { + option features.amazing_feature = B; + } + + extend Message { + int32 bar = 16 [features.amazing_feature = D]; + } + `).root.resolveAll(); + test.same(root4.lookup("Message").fields[".bar"].declaringField._features.amazing_feature, 'D') + + var root5 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + service MyService { + option features.amazing_feature = E; + message MyRequest {}; + message MyResponse {}; + } + `).root.resolveAll(); + test.same(root5.lookupService("MyService")._features.amazing_feature, 'E'); + + var root6 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + message Message { + string string_val = 1; + string string_repeated = 2 [features.amazing_feature = F]; + }`).root.resolveAll(); + test.same(root6.lookup("Message").fields.stringRepeated._features.amazing_feature, 'F') + + var root7 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + message Message { + enum SomeEnumInMessage { + option features.amazing_feature = G; + ONE = 11; + TWO = 12; + } + }`).root.resolveAll(); + test.same(root7.lookup("Message").lookupEnum("SomeEnumInMessage")._features.amazing_feature, 'G') + + var root8 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + message Message { + message Nested { + option features.amazing_feature = H; + int64 count = 9; + } + }`).root.resolveAll(); + test.same(root8.lookup("Message").lookup("Nested")._features.amazing_feature, 'H') + + var root9 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + message Message { + extend Message { + int32 bar = 10 [features.amazing_feature = I]; + } + }`).root.resolveAll(); + test.same(root9.lookup("Message").lookup(".Message.bar")._features.amazing_feature, 'I') + + var root10 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + message Message { + oneof SomeOneOf { + option features.amazing_feature = J; + int32 a = 13; + string b = 14; + } + }`).root.resolveAll(); test.same(root10.lookup("Message").lookup("SomeOneOf")._features.amazing_feature, 'J') + + var root11 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + enum SomeEnum { + option features.amazing_feature = C; + ONE = 1 [features.amazing_feature = K]; + TWO = 2; + }`).root.resolveAll(); test.same(root11.lookupEnum("SomeEnum")._valuesFeatures["ONE"].amazing_feature, 'K') + + var root12 = protobuf.parse(`edition = "2023"; + option features.amazing_feature = A; + service MyService { + option features.amazing_feature = E; + message MyRequest {}; + message MyResponse {}; + rpc MyMethod (MyRequest) returns (MyResponse) { + option features.amazing_feature = L; + }; + }`).root.resolveAll(); + test.same(root12.lookupService("MyService").lookup("MyMethod")._features.amazing_feature, 'L') test.end();