mirror of
https://github.com/protobufjs/protobuf.js.git
synced 2025-12-08 20:58:55 +00:00
feat: add feature resolution for protobuf editions
This commit is contained in:
parent
d8eb1b4e80
commit
65d3ed15e3
18
src/enum.js
18
src/enum.js
@ -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) {
|
||||
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<string, Object<string, *>>|undefined}
|
||||
*/
|
||||
this.valuesFeatures = valuesFeatures;
|
||||
|
||||
/**
|
||||
* Reserved ranges, if any.
|
||||
* @type {Array.<number[]|string>}
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
59
src/parse.js
59
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);
|
||||
|
||||
60
tests/data/feature-resolution.proto
Normal file
60
tests/data/feature-resolution.proto
Normal file
@ -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<string,int64> 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;
|
||||
}
|
||||
115
tests/feature_resolution_editions.js
Normal file
115
tests/feature_resolution_editions.js
Normal file
@ -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<string,int64> 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();
|
||||
})
|
||||
|
||||
|
||||
|
||||
})
|
||||
51
tests/field_presence_editions.js
Normal file
51
tests/field_presence_editions.js
Normal file
@ -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();
|
||||
// });
|
||||
Loading…
x
Reference in New Issue
Block a user