diff --git a/src/enum.js b/src/enum.js index 1c016209..fcc56190 100644 --- a/src/enum.js +++ b/src/enum.js @@ -20,7 +20,7 @@ var Namespace = require("./namespace"), * @param {Object.} [comments] The value comments for this enum * @param {Object.>|undefined} [valuesOptions] The value options for this enum */ -function Enum(name, values, options, comment, comments, valuesOptions) { +function Enum(name, values, options, comment, comments, valuesOptions, valuesFeatures) { ReflectionObject.call(this, name, options); if (values && typeof values !== "object") @@ -56,6 +56,12 @@ function Enum(name, values, options, comment, comments, valuesOptions) { */ this.valuesOptions = valuesOptions; + /** + * Values features, if any + * @type {Object>|undefined} + */ + this.valuesFeatures = valuesFeatures; + /** * Reserved ranges, if any. * @type {Array.} @@ -119,7 +125,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) { +Enum.prototype.add = function add(name, id, comment, options, features) { // utilized by the parser but not by .fromJSON if (!util.isString(name)) @@ -150,6 +156,12 @@ Enum.prototype.add = function add(name, id, comment, options) { this.valuesOptions[name] = options || null; } + if (features) { + if (this.valuesFeatures === undefined) + this.valuesFeatures = {}; + this.valuesFeatures[name] = features || null; + } + this.comments[name] = comment || null; return this; }; @@ -176,6 +188,8 @@ Enum.prototype.remove = function remove(name) { if (this.valuesOptions) delete this.valuesOptions[name]; + if (this.valuesFeatures) + delete this.valuesFeatures[name]; return this; }; diff --git a/src/object.js b/src/object.js index bd04ceca..f86462f3 100644 --- a/src/object.js +++ b/src/object.js @@ -41,6 +41,11 @@ function ReflectionObject(name, options) { */ this.name = name; + /** + * Resolved Features. + */ + this.features = null; + /** * Parent namespace. * @type {Namespace|null} @@ -175,6 +180,17 @@ ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet) return this; }; +/** + * Sets a feature. + * @param {string} name Feature name + * @param {*} value Feature value + * @returns {ReflectionObject} `this` + */ +ReflectionObject.prototype.setFeature = function setFeature(name, value) { + (this.features || (this.features = {}))[name] = value; + return this; +}; + /** * Sets a parsed option. * @param {string} name parsed Option name diff --git a/src/parse.js b/src/parse.js index da325b8e..9852d901 100644 --- a/src/parse.js +++ b/src/parse.js @@ -4,6 +4,7 @@ module.exports = parse; parse.filename = null; parse.defaults = { keepCase: false }; +const { hasOwnProperty } = require("tslint/lib/utils"); var tokenize = require("./tokenize"), Root = require("./root"), Type = require("./type"), @@ -25,7 +26,8 @@ var base10Re = /^[1-9][0-9]*$/, numberRe = /^(?![eE])[0-9]*(?:\.[0-9]*)?(?:[eE][+-]?[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]*)+$/; + fqTypeRefRe = /^(?:\.[a-zA-Z_][a-zA-Z_0-9]*)+$/, + featuresRefRe = /features\.([a-zA-Z_]*)/; /** * Result object returned from {@link parse}. @@ -312,6 +314,7 @@ function parse(source, root, options) { case "extend": parseExtension(parent, token); return true; + } return false; } @@ -480,7 +483,7 @@ function parse(source, root, options) { parseOption(type, token); skip(";"); break; - + case "required": case "repeated": parseField(type, token); @@ -611,6 +614,11 @@ function parse(source, root, options) { this.options = {}; this.options[name] = value; }; + dummy.setFeature = function(name, value) { + if (this.features === undefined) + this.features = {}; + this.features[name] = value; + }; ifBlock(dummy, function parseEnumValue_block(token) { /* istanbul ignore else */ @@ -623,33 +631,40 @@ function parse(source, root, options) { }, function parseEnumValue_line() { parseInlineOptions(dummy); // skip }); - parent.add(token, value, dummy.comment, dummy.options); + parent.add(token, value, dummy.comment, dummy.options, dummy.features); } function parseOption(parent, token) { + if (featuresRefRe.test(token = next())) { + var name = token.match(featuresRefRe)[1] + skip("="); + setFeature(parent, name, token = next()) + } else { var isCustom = skip("(", true); if (!typeRefRe.test(token = next())) throw illegal(token, "name"); + - var name = token; - var option = name; - var propName; + var name = token; + var option = name; + var propName; - if (isCustom) { - skip(")"); - name = "(" + name + ")"; - option = name; - token = peek(); - if (fqTypeRefRe.test(token)) { - propName = token.slice(1); //remove '.' before property name - name += token; - next(); + if (isCustom) { + skip(")"); + name = "(" + name + ")"; + option = name; + token = peek(); + if (fqTypeRefRe.test(token)) { + propName = token.slice(1); //remove '.' before property name + name += token; + next(); + } } - } - skip("="); - var optionValue = parseOptionValue(parent, name); - setParsedOption(parent, option, optionValue, propName); + skip("="); + var optionValue = parseOptionValue(parent, name); + setParsedOption(parent, option, optionValue, propName); + } } function parseOptionValue(parent, name) { @@ -720,6 +735,12 @@ 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); diff --git a/tests/data/feature-resolution.proto b/tests/data/feature-resolution.proto new file mode 100644 index 00000000..98233e92 --- /dev/null +++ b/tests/data/feature-resolution.proto @@ -0,0 +1,60 @@ +edition = "2023"; + +option features.amazing_feature = A; + +service MyService { + option features.amazing_feature = E; + rpc MyMethod (MyRequest) returns (MyResponse) { + option features.amazing_feature = L; + }; +} + +message Message { + option features.amazing_feature = B; + + string string_val = 1; + repeated string string_repeated = 2 [features.amazing_feature = F]; + + uint64 uint64_val = 3; + repeated uint64 uint64_repeated = 4; + + bytes bytes_val = 5; + repeated bytes bytes_repeated = 6; + + SomeEnum enum_val = 7; + repeated SomeEnum enum_repeated = 8; + + extensions 10 to 100; + extend Message { + required int32 bar = 10 [features.amazing_feature = I]; + } + + message Nested { + option features.amazing_feature = H; + optional int64 count = 9; + } + + enum SomeEnumInMessage { + option features.amazing_feature = G; + ONE = 11; + TWO = 12; + } + + oneof SomeOneOf { + option features.amazing_feature = J; + int32 a = 13; + string b = 14; + } + + map int64_map = 15; +} + +extend Message { + required int32 bar = 16 [features.amazing_feature = D]; +} + +enum SomeEnum { + option features.amazing_feature = C; + ONE = 1 [features.amazing_feature = K]; + TWO = 2; +} \ No newline at end of file diff --git a/tests/feature_resolution_editions.js b/tests/feature_resolution_editions.js new file mode 100644 index 00000000..88128a0d --- /dev/null +++ b/tests/feature_resolution_editions.js @@ -0,0 +1,115 @@ +/* +1. Defaults +2. File - A +3. Message - B +4. Enum - C +5. File extension - D +6. File service - E +7. Message Field - F +8. Message Enum - G +9. Message Message - H +10. Message Extension - I +11. "one of" Field - J +12. Enum value - K +13. Service method - L + + + +edition = "2023"; + +option features.amazing_feature = A; + +service MyService { + option features.amazing_feature = E; + rpc MyMethod (MyRequest) returns (MyResponse) { + option features.amazing_feature = L; + }; +} + +message Message { + option features.amazing_feature = B; + + string string_val = 1; + repeated string string_repeated = 2 [features.amazing_feature = F]; + + uint64 uint64_val = 3; + repeated uint64 uint64_repeated = 4; + + bytes bytes_val = 5; + repeated bytes bytes_repeated = 6; + + SomeEnum enum_val = 7; + repeated SomeEnum enum_repeated = 8; + + extensions 10 to 100; + extend Message { + int32 bar = 10 [features.amazing_feature = I]; + } + + message Nested { + option features.amazing_feature = H; + optional int32 count = 1; + } + + enum SomeEnum { + option features.amazing_feature = G; + ONE = 1; + TWO = 2; + } + + oneof bar { + option features.amazing_feature = J; + int32 a = 1; + string b = 2; + } + + map int64_map = 9; +} + +extend Message { + int32 bar = 11 [features.amazing_feature = D]; +} + +enum SomeEnum { + option features.amazing_feature = C; + ONE = 1 [features.amazing_feature = K]; + TWO = 2; +} + +*/ + +var tape = require("tape"); + +var protobuf = require(".."); + + +tape.test.only("feature resolution editions", function(test) { + + protobuf.load("tests/data/feature-resolution.proto", function(err, root) { + if (err) + return test.fail(err.message); + + // test.same(root.fea, { + // 1: "a", + // 2: "b" + // }, "should also expose their values by id"); + + // console.log(root.features.amazing_feature) + + test.same(root.features.amazing_feature, 'A'); + test.same(root.lookup("Message").features.amazing_feature, 'B') + test.same(root.lookupService("MyService").features.amazing_feature, 'E'); + test.same(root.lookupEnum("SomeEnum").features.amazing_feature, 'C') + test.same(root.lookup("Message").lookupEnum("SomeEnumInMessage").features.amazing_feature, 'G') + test.same(root.lookup("Message").lookup("Nested").features.amazing_feature, 'H') + test.same(root.lookupService("MyService").lookup("MyMethod").features.amazing_feature, 'L') + test.same(root.lookup("Message").fields.stringRepeated.features.amazing_feature, 'F') + test.same(root.lookup("Message").lookup(".Message.bar").features.amazing_feature, 'I') + test.same(root.lookupEnum("SomeEnum").valuesFeatures.ONE.amazing_feature, 'K') + + test.end(); +}) + + + +}) \ No newline at end of file diff --git a/tests/field_presence_editions.js b/tests/field_presence_editions.js new file mode 100644 index 00000000..bb884ce4 --- /dev/null +++ b/tests/field_presence_editions.js @@ -0,0 +1,51 @@ +// TODO: write a protobuf editions test +// var tape = require("tape"); + +// var protobuf = require(".."); + +// var protoEditionsLegacyRequired = 'edition = "2023";'+ +// "message Test {"+ +// "uint32 a = 1 [features.field_presence = LEGACY_REQUIRED];"+ +// "}"; + +// var proto2Required = 'syntax = "proto2";'+ +// "message Test {"+ +// "required uint32 a = 1;"+ +// "}"; + +// var protoEditionsImplicit = 'edition = "2023";'+ +// "message Test {"+ +// "uint32 a = 1 [features.field_presence = IMPLICIT];"+ +// "}"; + +// var msg = { +// // a: [1,2,3] +// }; + +// tape.test.only("packed repeated values", function(test) { +// var rootEditionsLegacyRequired = protobuf.parse(protoEditionsLegacyRequired).root, +// rootProto2Required = protobuf.parse(proto2Required).root; +// rootProtoEditionsImplicit = protobuf.parse(protoEditionsImplicit).root; +// // console.log(rootEditionsLegacyRequired) +// // console.log(rootProto2Required) +// var Test1 = rootEditionsLegacyRequired.lookup("Test"), +// Test2 = rootProto2Required.lookup("Test"); +// Test3 = rootProtoEditionsImplicit.lookup("Test"); +// console.log(Test2) + +// // This should throw, because we are passing a msg without a required field, and +// // LEGACY_REQUIRED = required +// var dec1 = Test1.decode(Test1.encode(msg).finish()); +// console.log(dec1) +// // This should also throw, same reason above, but are actually using proto2 +// var dec2 = Test2.decode(Test2.encode(msg).finish()); +// console.log(dec2) +// // This test shou +// var dec2 = Test2.decode(Test2.encode(msg).finish()); +// console.log(dec2) +// // test.same(dec1, msg, "should decode packed even if defined non-packed"); +// // var dec2 = Test1.decode(Test2.encode(msg).finish()); +// // test.same(dec2, msg, "should decode non-packed even if defined packed"); + +// test.end(); +// });