Skip to content

Commit 7a25398

Browse files
feat: parsed options (#1256)
* Add option parsing tests to ensure no regressions are caused * Properly parse option values into objects in addition to the regular option parsing. Repeated options are preserved as well as repeated fields within nested options. Parsed options are kept in a parsedOptions field on every level (message, field etc.) * fix: bad merge * fix: lint * fix: lint * fix: lint * fix: lint * fix: lint * fix: build types Co-authored-by: Alexander Fenster <[email protected]> Co-authored-by: Alexander Fenster <[email protected]>
1 parent 7fd2e18 commit 7a25398

8 files changed

+396
-7
lines changed

index.d.ts

+21
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,9 @@ export abstract class ReflectionObject {
859859
/** Options. */
860860
public options?: { [k: string]: any };
861861

862+
/** Options. */
863+
public parsedOptions?: { [k: string]: any }[];
864+
862865
/** Unique name within its namespace. */
863866
public name: string;
864867

@@ -920,6 +923,15 @@ export abstract class ReflectionObject {
920923
*/
921924
public setOption(name: string, value: any, ifNotSet?: boolean): ReflectionObject;
922925

926+
/**
927+
* Sets a parsed option.
928+
* @param name parsed Option name
929+
* @param value Option value
930+
* @param [propName] dot '.' delimited full path of property within the option to set. if undefined\empty, will add a new option with that value
931+
* @returns `this`
932+
*/
933+
public setParsedOption(name: string, value: any, propName?: string): ReflectionObject;
934+
923935
/**
924936
* Sets multiple options.
925937
* @param options Options to set
@@ -2162,6 +2174,15 @@ export namespace util {
21622174
*/
21632175
function decorateEnum(object: object): Enum;
21642176

2177+
/**
2178+
* Sets the value of a property by property path. If a value already exists, it is turned to an array
2179+
* @param dst Destination object
2180+
* @param path dot '.' delimited path of the property to set
2181+
* @param value the value to set
2182+
* @returns Destination object
2183+
*/
2184+
function setProperty(dst: { [k: string]: any }, path: string, value: object);
2185+
21652186
/** Decorator root (TypeScript). */
21662187
let decorateRoot: Root;
21672188

src/object.js

+43
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ function ReflectionObject(name, options) {
2929
*/
3030
this.options = options; // toJSON
3131

32+
/**
33+
* Parsed Options.
34+
* @type {Array.<Object.<string,*>>|undefined}
35+
*/
36+
this.parsedOptions = null;
37+
3238
/**
3339
* Unique name within its namespace.
3440
* @type {string}
@@ -169,6 +175,43 @@ ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet)
169175
return this;
170176
};
171177

178+
/**
179+
* Sets a parsed option.
180+
* @param {string} name parsed Option name
181+
* @param {*} value Option value
182+
* @param {string} propName dot '.' delimited full path of property within the option to set. if undefined\empty, will add a new option with that value
183+
* @returns {ReflectionObject} `this`
184+
*/
185+
ReflectionObject.prototype.setParsedOption = function setParsedOption(name, value, propName) {
186+
if (!this.parsedOptions) {
187+
this.parsedOptions = [];
188+
}
189+
var parsedOptions = this.parsedOptions;
190+
if (propName) {
191+
// If setting a sub property of an option then try to merge it
192+
// with an existing option
193+
var opt = parsedOptions.find(function (opt) {
194+
return Object.prototype.hasOwnProperty.call(opt, name);
195+
});
196+
if (opt) {
197+
// If we found an existing option - just merge the property value
198+
var newValue = opt[name];
199+
util.setProperty(newValue, propName, value);
200+
} else {
201+
// otherwise, create a new option, set it's property and add it to the list
202+
opt = {};
203+
opt[name] = util.setProperty({}, propName, value);
204+
parsedOptions.push(opt);
205+
}
206+
} else {
207+
// Always create a new option when setting the value of the option itself
208+
var newOpt = {};
209+
newOpt[name] = value;
210+
parsedOptions.push(newOpt);
211+
}
212+
return this;
213+
};
214+
172215
/**
173216
* Sets multiple options.
174217
* @param {Object.<string,*>} options Options to set

src/parse.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -542,39 +542,58 @@ function parse(source, root, options) {
542542
throw illegal(token, "name");
543543

544544
var name = token;
545+
var option = name;
546+
var propName;
547+
545548
if (isCustom) {
546549
skip(")");
547550
name = "(" + name + ")";
551+
option = name;
548552
token = peek();
549553
if (fqTypeRefRe.test(token)) {
554+
propName = token.substr(1); //remove '.' before property name
550555
name += token;
551556
next();
552557
}
553558
}
554559
skip("=");
555-
parseOptionValue(parent, name);
560+
var optionValue = parseOptionValue(parent, name);
561+
setParsedOption(parent, option, optionValue, propName);
556562
}
557563

558564
function parseOptionValue(parent, name) {
559565
if (skip("{", true)) { // { a: "foo" b { c: "bar" } }
566+
var result = {};
560567
while (!skip("}", true)) {
561568
/* istanbul ignore if */
562569
if (!nameRe.test(token = next()))
563570
throw illegal(token, "name");
564571

572+
var value;
573+
var propName = token;
565574
if (peek() === "{")
566-
parseOptionValue(parent, name + "." + token);
575+
value = parseOptionValue(parent, name + "." + token);
567576
else {
568577
skip(":");
569578
if (peek() === "{")
570-
parseOptionValue(parent, name + "." + token);
571-
else
572-
setOption(parent, name + "." + token, readValue(true));
579+
value = parseOptionValue(parent, name + "." + token);
580+
else {
581+
value = readValue(true);
582+
setOption(parent, name + "." + token, value);
583+
}
573584
}
585+
var prevValue = result[propName];
586+
if (prevValue)
587+
value = [].concat(prevValue).concat(value);
588+
result[propName] = value;
574589
skip(",", true);
575590
}
576-
} else
577-
setOption(parent, name, readValue(true));
591+
return result;
592+
}
593+
594+
var simpleValue = readValue(true);
595+
setOption(parent, name, simpleValue);
596+
return simpleValue;
578597
// Does not enforce a delimiter to be universal
579598
}
580599

@@ -583,6 +602,11 @@ function parse(source, root, options) {
583602
parent.setOption(name, value);
584603
}
585604

605+
function setParsedOption(parent, name, value, propName) {
606+
if (parent.setParsedOption)
607+
parent.setParsedOption(name, value, propName);
608+
}
609+
586610
function parseInlineOptions(parent) {
587611
if (skip("[", true)) {
588612
do {

src/util.js

+31
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,37 @@ util.decorateEnum = function decorateEnum(object) {
165165
return enm;
166166
};
167167

168+
169+
/**
170+
* Sets the value of a property by property path. If a value already exists, it is turned to an array
171+
* @param {Object.<string,*>} dst Destination object
172+
* @param {string} path dot '.' delimited path of the property to set
173+
* @param {Object} value the value to set
174+
* @returns {Object.<string,*>} Destination object
175+
*/
176+
util.setProperty = function setProperty(dst, path, value) {
177+
function setProp(dst, path, value) {
178+
var part = path.shift();
179+
if (path.length > 0) {
180+
dst[part] = setProp(dst[part] || {}, path, value);
181+
} else {
182+
var prevValue = dst[part];
183+
if (prevValue)
184+
value = [].concat(prevValue).concat(value);
185+
dst[part] = value;
186+
}
187+
return dst;
188+
}
189+
190+
if (typeof dst !== "object")
191+
throw TypeError("dst must be an object");
192+
if (!path)
193+
throw TypeError("path must be specified");
194+
195+
path = path.split(".");
196+
return setProp(dst, path, value);
197+
};
198+
168199
/**
169200
* Decorator root (TypeScript).
170201
* @name util.decorateRoot

tests/api_object.js

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ tape.test("reflection objects", function(test) {
2424
obj.setOption("c", 3);
2525
test.same(obj.options, { a: 1, b: 2, c: 3 }, "should set single options");
2626

27+
obj.setParsedOption("opt1", {a: 1, b: 2});
28+
test.same(obj.parsedOptions, [{"opt1": {a: 1, b: 2}}], "should set single parsed option");
29+
obj.setParsedOption("opt1", {a: 3, b: 4});
30+
test.same(obj.parsedOptions, [{"opt1": {a: 1, b: 2}}, {"opt1": {a: 3, b: 4}}], "should allow same option twice");
31+
obj.setParsedOption("opt2", 1, "x");
32+
test.same(obj.parsedOptions, [{"opt1": {a: 1, b: 2}}, {"opt1": {a: 3, b: 4}}, {"opt2": {x: 1}}], "should create new option using property path");
33+
obj.setParsedOption("opt2", 5, "a.b");
34+
test.same(obj.parsedOptions, [{"opt1": {a: 1, b: 2}}, {"opt1": {a: 3, b: 4}}, {"opt2": {x: 1, a: {b :5}}}], "should merge new property path in existing option");
35+
obj.setParsedOption("opt2", 6, "x");
36+
test.same(obj.parsedOptions, [{"opt1": {a: 1, b: 2}}, {"opt1": {a: 3, b: 4}}, {"opt2": {x: [1,6], a: {b :5}}}], "should convert property to array when set more than once");
37+
38+
2739
test.equal(obj.toString(), "ReflectionObject Test", "should convert to a string (even if not part of a root)");
2840
obj.name = "";
2941
test.equal(obj.toString(), "ReflectionObject", "should convert to a string even with no full name");

tests/api_util.js

+29
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,34 @@ tape.test("util", function(test) {
7070
test.end();
7171
});
7272

73+
test.test(test.name + " - setProperty", function(test) {
74+
var o = {};
75+
76+
test.throws(function() {
77+
util.setProperty(5, 'prop1', 5);
78+
}, TypeError, "dst must be an object");
79+
80+
test.throws(function () {
81+
util.setProperty(o, '', 5);
82+
}, TypeError, "path must be specified");
83+
84+
util.setProperty(o, 'prop1', 5);
85+
test.same(o, {prop1: 5}, "should set single property value");
86+
87+
util.setProperty(o, 'prop1', 6);
88+
test.same(o, {prop1: [5, 6]}, "should convert to array if same property is set");
89+
90+
util.setProperty(o, 'prop.subprop', { subsub: 5});
91+
test.same(o, {prop1: [5, 6], prop: {subprop: {subsub: 5}}}, "should handle nested properties properly");
92+
93+
util.setProperty(o, 'prop.subprop.subsub', 6);
94+
test.same(o, {prop1: [5, 6], prop: {subprop: {subsub: [5, 6]}}}, "should convert to array nested property");
95+
96+
util.setProperty(o, 'prop.subprop', { subsub2: 7});
97+
test.same(o, {prop1: [5, 6], prop: {subprop: [{subsub: [5,6]}, {subsub2: 7}]}}, "should convert nested properties to array");
98+
99+
test.end();
100+
});
101+
73102
test.end();
74103
});

tests/comp_options-parse.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
var tape = require("tape");
2+
var protobuf = require("..");
3+
4+
tape.test("Options", function (test) {
5+
var root = protobuf.loadSync("tests/data/options_test.proto");
6+
7+
test.test(test.name + " - field options (Int)", function (test) {
8+
var TestFieldOptionsInt = root.lookup("TestFieldOptionsInt");
9+
test.equal(TestFieldOptionsInt.fields.field1.options["(fo_rep_int)"], 2, "should take second repeated int option");
10+
test.same(TestFieldOptionsInt.fields.field1.parsedOptions, [{"(fo_rep_int)": 1}, {"(fo_rep_int)": 2}], "should take all repeated int option");
11+
12+
test.equal(TestFieldOptionsInt.fields.field2.options["(fo_single_int)"], 3, "should correctly parse single int option");
13+
test.same(TestFieldOptionsInt.fields.field2.parsedOptions, [{"(fo_single_int)": 3}], "should correctly parse single int option");
14+
test.end();
15+
});
16+
17+
test.test(test.name + " - message options (Int)", function (test) {
18+
var TestMessageOptionsInt = root.lookup("TestMessageOptionsInt");
19+
test.equal(TestMessageOptionsInt.options["(mo_rep_int)"], 2, "should take second repeated int message option");
20+
test.equal(TestMessageOptionsInt.options["(mo_single_int)"], 3, "should correctly parse single int message option");
21+
test.same(TestMessageOptionsInt.parsedOptions, [{"(mo_rep_int)": 1}, {"(mo_rep_int)": 2}, {"(mo_single_int)": 3}], "should take all int message option");
22+
test.end();
23+
});
24+
25+
test.test(test.name + " - field options (Message)", function (test) {
26+
var TestFieldOptionsMsg = root.lookup("TestFieldOptionsMsg");
27+
test.equal(TestFieldOptionsMsg.fields.field1.options["(fo_rep_msg).value"], 4, "should take second repeated message option");
28+
test.equal(TestFieldOptionsMsg.fields.field1.options["(fo_rep_msg).rep_value"], 6, "should take second repeated int in second repeated option");
29+
test.same(TestFieldOptionsMsg.fields.field1.parsedOptions, [
30+
{"(fo_rep_msg)": {value: 1, rep_value: [2, 3]}},
31+
{"(fo_rep_msg)": {value: 4, rep_value: [5, 6]}}], "should take all repeated message option");
32+
test.equal(TestFieldOptionsMsg.fields.field2.options["(fo_single_msg).value"], 7, "should correctly parse single msg option");
33+
test.equal(TestFieldOptionsMsg.fields.field2.options["(fo_single_msg).rep_value"], 9, "should take second repeated int in single msg option");
34+
test.same(TestFieldOptionsMsg.fields.field2.parsedOptions, [{"(fo_single_msg)": {value: 7, rep_value: [8,9]}}], "should take all repeated message option");
35+
test.end();
36+
});
37+
38+
test.test(test.name + " - message options (Message)", function (test) {
39+
var TestMessageOptionsMsg = root.lookup("TestMessageOptionsMsg");
40+
test.equal(TestMessageOptionsMsg.options["(mo_rep_msg).value"], 4, "should take second repeated message option");
41+
test.equal(TestMessageOptionsMsg.options["(mo_rep_msg).rep_value"], 6, "should take second repeated int in second repeated option");
42+
test.equal(TestMessageOptionsMsg.options["(mo_single_msg).value"], 7, "should correctly parse single msg option");
43+
test.equal(TestMessageOptionsMsg.options["(mo_single_msg).rep_value"], 9, "should take second repeated int in single msg option");
44+
test.same(TestMessageOptionsMsg.parsedOptions, [
45+
{"(mo_rep_msg)": {value: 1, rep_value: [2, 3]}},
46+
{"(mo_rep_msg)": {value: 4, rep_value: [5, 6]}},
47+
{"(mo_single_msg)": {value: 7, rep_value: [8, 9]}},
48+
], "should take all message options");
49+
test.end();
50+
});
51+
52+
test.test(test.name + " - field options (Nested)", function (test) {
53+
var TestFieldOptionsNested = root.lookup("TestFieldOptionsNested");
54+
test.equal(TestFieldOptionsNested.fields.field1.options["(fo_rep_msg).value"], 1, "should merge repeated options messages");
55+
test.equal(TestFieldOptionsNested.fields.field1.options["(fo_rep_msg).rep_value"], 3, "should parse in any order");
56+
test.equal(TestFieldOptionsNested.fields.field1.options["(fo_rep_msg).nested.nested.value"], "x", "should correctly parse nested field options");
57+
test.equal(TestFieldOptionsNested.fields.field1.options["(fo_rep_msg).rep_nested.value"], "z", "should take second repeated nested options");
58+
test.equal(TestFieldOptionsNested.fields.field1.options["(fo_rep_msg).nested.value"], "w", "should merge nested options");
59+
test.same(TestFieldOptionsNested.fields.field1.parsedOptions,[
60+
{"(fo_rep_msg)": {value: 1, nested: { nested: { value: "x"}}, rep_nested: [{value: "y"},{value: "z"}], rep_value: 3}},
61+
{"(fo_rep_msg)": { nested: { value: "w"}}},
62+
],"should parse all options including nested");
63+
64+
test.equal(TestFieldOptionsNested.fields.field2.options["(fo_single_msg).nested.value"], "x", "should correctly parse nested property name");
65+
test.equal(TestFieldOptionsNested.fields.field2.options["(fo_single_msg).rep_nested.value"], "y", "should take second repeated nested options");
66+
test.same(TestFieldOptionsNested.fields.field2.parsedOptions, [{
67+
"(fo_single_msg)": {
68+
nested: {value: "x"},
69+
rep_nested: [{value: "x"}, {value: "y"}]
70+
}
71+
}
72+
], "should parse single nested option correctly");
73+
74+
test.equal(TestFieldOptionsNested.fields.field3.options["(fo_single_msg).nested.value"], "x", "should correctly parse nested field options");
75+
test.equal(TestFieldOptionsNested.fields.field3.options["(fo_single_msg).nested.nested.nested.value"], "y", "should correctly parse several nesting levels");
76+
test.same(TestFieldOptionsNested.fields.field3.parsedOptions, [{
77+
"(fo_single_msg)": {
78+
nested: {
79+
value: "x",
80+
nested: {nested: {value: "y"}}
81+
}
82+
}
83+
}], "should correctly parse several nesting levels");
84+
85+
test.end();
86+
});
87+
88+
test.test(test.name + " - message options (Nested)", function (test) {
89+
var TestMessageOptionsNested = root.lookup("TestMessageOptionsNested");
90+
test.equal(TestMessageOptionsNested.options["(mo_rep_msg).value"], 1, "should merge repeated options messages");
91+
test.equal(TestMessageOptionsNested.options["(mo_rep_msg).rep_value"], 3, "should parse in any order");
92+
test.equal(TestMessageOptionsNested.options["(mo_rep_msg).nested.nested.value"], "x", "should correctly parse nested field options");
93+
test.equal(TestMessageOptionsNested.options["(mo_rep_msg).rep_nested.value"], "z", "should take second repeated nested options");
94+
test.equal(TestMessageOptionsNested.options["(mo_rep_msg).nested.value"], "w", "should merge nested options");
95+
96+
test.equal(TestMessageOptionsNested.options["(mo_single_msg).nested.value"], "x", "should correctly parse nested property name");
97+
test.equal(TestMessageOptionsNested.options["(mo_single_msg).rep_nested.value"], "y", "should take second repeated nested options");
98+
test.equal(TestMessageOptionsNested.options["(mo_single_msg).rep_nested.nested.nested.value"], "y", "should correctly parse several nesting levels");
99+
100+
test.same(TestMessageOptionsNested.parsedOptions, [
101+
{
102+
"(mo_rep_msg)": {
103+
value: 1,
104+
nested: {nested: {value: "x"}},
105+
rep_nested: [{value: "y"}, {value: "z"}],
106+
rep_value: 3
107+
}
108+
},
109+
{"(mo_rep_msg)": {nested: {value: "w"}}},
110+
{
111+
"(mo_single_msg)": {
112+
nested: {value: "x"},
113+
rep_nested: [{value: "x", nested: {nested: {value: "y"}}}, {value: "y"}]
114+
}
115+
}
116+
], "should correctly parse all nested message options");
117+
test.end();
118+
});
119+
120+
test.end();
121+
});

0 commit comments

Comments
 (0)