Skip to content
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: support proper JSON for Value and Duration #1495

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ function genValuePartial_fromObject(gen, field, fieldIndex, prop) {
("break");
} gen
("}");
} else gen
("if(typeof d%s!==\"object\")", prop)
("throw TypeError(%j)", field.fullName + ": object expected")
} else {
if (field.type !== "google.protobuf.Value" && field.type !== "google.protobuf.Duration")
// google.protobuf.Value and google.protobuf.Duration can accept non-objects
gen
("if(typeof d%s!==\"object\")", prop)
("throw TypeError(%j)", field.fullName + ": object expected");
gen
("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop);
}
} else {
var isUnsigned = false;
switch (field.type) {
Expand Down Expand Up @@ -132,10 +137,12 @@ converter.fromObject = function fromObject(mtype) {

// Non-repeated fields
} else {
if (!(field.resolvedType instanceof Enum)) gen // no need to test for null/undefined if an enum (uses switch)
if (!(field.resolvedType instanceof Enum) && field.type !== "google.protobuf.Value")
// no need to test for null/undefined if an enum (uses switch) and for google.protobuf.Value (can be null)
gen
("if(d%s!=null){", prop); // !== undefined && !== null
genValuePartial_fromObject(gen, field, /* not sorted */ i, prop);
if (!(field.resolvedType instanceof Enum)) gen
if (!(field.resolvedType instanceof Enum) && field.type !== "google.protobuf.Value") gen
("}");
}
} return gen
Expand Down
217 changes: 217 additions & 0 deletions src/wrappers.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,220 @@ wrappers[".google.protobuf.Any"] = {
return this.toObject(message, options);
}
};

// recursive .fromObject implementation for google.protobuf.Value
function googleProtobufValueFromObject(object, create) {
if (object === null) {
return create({
kind: "nullValue",
nullValue: 0
});
}
if (typeof object === "number") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is missing boolValue.

return create({
kind: "numberValue",
numberValue: object
});
}
if (typeof object === "string") {
return create({
kind: "stringValue",
stringValue: object
});
}
if (Array.isArray(object)) {
var array = object.map(function(element) { return googleProtobufValueFromObject(element, create); });
return create({
kind: "listValue",
listValue: {
values: array
}
});
}
if (typeof object === "object") {
var fields = {},
names = Object.keys(object),
i = 0;
for (; i < names.length; ++i) {
fields[names[i]] = googleProtobufValueFromObject(object[names[i]], create);
}
return create({
kind: "structValue",
structValue: {
fields: fields
}
});
}
return undefined;
}

// recursive .toObject implementation for google.protobuf.Value
function googleProtobufValueToObject(message) {
if (message.kind === "nullValue") {
return null;
}
if (message.kind === "numberValue") {
return message.numberValue;
}
if (message.kind === "stringValue") {
return message.stringValue;
}
if (message.kind === "listValue") {
return message.listValue.values.map(googleProtobufValueToObject);
}
if (message.kind === "structValue") {
if (!message.structValue.fields) {
return {};
}
var names = Object.keys(message.structValue.fields),
i = 0,
struct = {};
for (; i < names.length; ++i) {
struct[names[i]] = googleProtobufValueToObject(message.structValue["fields"][names[i]]);
}
return struct;
}
return undefined;
}

// custom wrapper for google.protobuf.Value
wrappers[".google.protobuf.Value"] = {
fromObject: function(object) {
// heuristic: if an object looks like a regular representation of google.protobuf.Value,
// with all those stringValues, etc., just accept it as is for compatibility.
if (typeof object === "object" && object) {
// something that has just one property called stringValue, listValue, etc.,
// and possibly a property called kind, is likely an object we don't want to touch
var names = Object.keys(object);
if (names.length === 1 && names.match(/^(?:null|number|string|list|struct)Value$/) ||

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are cases where names.match() will be called on an Array object that doesn't have the match() method. For example, you might have an obj value that is { content: text } that results in a names array with a length of 1 (but no match() method).

Maybe add an undefined check here?

if (names.length === 1 && names.match && names.match(...) ...

names.length === 2 &&
names.every(function(name) {
return name.match(/^(?:kind|(?:null|number|string|list|struct)Value)$/); })
) {
return this.fromObject(object);
}
}

// otherwise, it's a JSON representation as described in google/protobuf/struct.proto
var self = this;
var message = googleProtobufValueFromObject(object, function(obj) { return self.create(obj); });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe create() is a static method--is that correct?

if (typeof message !== "undefined") {
return message;
}

// fallback to the normal .fromObject if decoding failed
return this.fromObject(object);
},

toObject: function(message, options) {
// decode value if requested
// In the next major version we will get rid of "options.values".
if (options && options.json && options.values) {
var object = googleProtobufValueToObject(message);
if (typeof object !== "undefined") {
return object;
}
}

return this.toObject(message, options);
}
};

// custom wrapper for google.protobuf.Struct
wrappers[".google.protobuf.Struct"] = {
fromObject: function(object) {
if (typeof object === "object" && object) {
var names = Object.keys(object),
i = 0,
fields = {},
Value = this.fields.fields.resolvedType;

// heuristic: if an object looks like a regular representation of google.protobuf.Struct,
// with just one field called `fields`, just accept it as is for compatibility.
if (names.length === 1 && names[0] === "fields") {
return this.fromObject(object);
}

for (; i < names.length; ++i) {
fields[names[i]] = Value.fromObject(object[names[i]]);
}
return this.create({
fields: fields
});
}

// fallback to the normal .fromObject if decoding failed
return this.fromObject(object);
},

toObject: function(message, options) {
// decode value if requested
// In the next major version we will get rid of "options.values".
if (options && options.json && options.values) {
if (!message.fields) {
return {};
}
var names = Object.keys(message.fields),
i = 0,
struct = {},
Value = this.fields.fields.resolvedType;
for (; i < names.length; ++i) {
struct[names[i]] = Value.toObject(message["fields"][names[i]], options);
}
return struct;
}

return this.toObject(message, options);
}
};

// custom wrapper for google.protobuf.Duration
wrappers[".google.protobuf.Duration"] = {
fromObject: function(object) {
var match;
if (typeof object === "string") {
// whole seconds
match = object.match(/^(\d+)s$/);
if (match) {
return this.create({
seconds: Number(match[1]),
nanos: 0
});
}
// fractional seconds
match = object.match(/^(\d*)\.(\d+)s$/);
if (match) {
var nanos = match[2];
// pad trailing zeros; cannot use .padEnd since it will break old versions
while (nanos.length < 9) {
nanos = nanos + "0";
}
return this.create({
seconds: match[1].length > 0 ? Number(match[1]) : 0,
nanos: Number(nanos)
});
}
}
return this.fromObject(object);
},

toObject: function(message, options) {
if (options && options.json && options.values) {
var durationSeconds = message.seconds;
if (message.nanos > 0) {
var nanosStr = String(message.nanos);
// add leading zeros; cannot use .padStart since it will break old versions
while (nanosStr.length < 9) {
nanosStr = "0" + nanosStr;
}
// nanosStr should contain 3, 6, or 9 fractional digits.
nanosStr = nanosStr.replace(/^((?:\d\d\d)+?)(?:0*)$/, "$1");
durationSeconds += "." + nanosStr;
}
durationSeconds += "s";
return durationSeconds;
}

return this.toObject(message, options);
}
};
85 changes: 85 additions & 0 deletions tests/comp_google_protobuf_duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
var tape = require("tape");
var long = require("long");

var protobuf = require("..");

var root = protobuf.Root.fromJSON({
nested: {
test: {
nested: {
Test: {
fields: {
value: {
type: "google.protobuf.Duration",
id: 1
}
}
}
}
}
}
}).addJSON(protobuf.common["google/protobuf/duration.proto"].nested).resolveAll();

var Test = root.lookupType("test.Test");

tape.test("google.protobuf.Duration", function(test) {
// examples from google/protobuf/duration.proto
var integerDuration = {value: "3s"};
var fractionalDuration1 = {value: "3.000000001s"};
var fractionalDuration2 = {value: "3.000001s"};
var regularDuration = {
value: {
seconds: long.fromNumber(4),
nanos: 2
}
};

var integerDurationMessage = Test.fromObject(integerDuration);
var fractionalDuration1Message = Test.fromObject(fractionalDuration1);
var fractionalDuration2Message = Test.fromObject(fractionalDuration2);
var regularDurationMessage = Test.fromObject(regularDuration);

test.same(integerDurationMessage, {
value: {
seconds: 3,
nanos: 0
}
}, "toObject should understand integer seconds as string");
test.same(fractionalDuration1Message, {
value: {
seconds: 3,
nanos: 1
}
}, "toObject should understand fractional seconds as string 1");
test.same(fractionalDuration2Message, {
value: {
seconds: 3,
nanos: 1000
}
}, "toObject should understand fractional seconds as string 2");
test.same(regularDurationMessage, {
value: {
seconds: {low: 4, high: 0, unsigned: false},
nanos: 2
}
}, "toObject should understand regular Duration message");

test.same(Test.toObject(integerDurationMessage, {json: true, values: true}), {value: "3s"}, "toObject should produce integer seconds");
test.same(Test.toObject(fractionalDuration1Message, {json: true, values: true}), {value: "3.000000001s"}, "toObject should produce fractional seconds 1");
test.same(Test.toObject(fractionalDuration2Message, {json: true, values: true}), {value: "3.000001s"}, "toObject should produce fractional seconds 2");
test.same(Test.toObject(regularDurationMessage, {json: true, values: true}), {value: "4.000000002s"}, "toObject should produce fractional seconds 3");
test.same(Test.toObject(regularDurationMessage), regularDurationMessage, "toObject should produce a regular Duration object by default");

test.same(Test.toObject({ value: { seconds: 0, nanos: 100000000 }}, {json: true, values: true}), { value: "0.100s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 10000000 }}, {json: true, values: true}), { value: "0.010s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 1000000 }}, {json: true, values: true}), { value: "0.001s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 100000 }}, {json: true, values: true}), { value: "0.000100s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 10000 }}, {json: true, values: true}), { value: "0.000010s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 1000 }}, {json: true, values: true}), { value: "0.000001s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 100 }}, {json: true, values: true}), { value: "0.000000100s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 10 }}, {json: true, values: true}), { value: "0.000000010s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 1 }}, {json: true, values: true}), { value: "0.000000001s"}, "toObject string should contain 3, 6, or 9 fractional digits 1");
test.same(Test.toObject({ value: { seconds: 0, nanos: 0 }}, {json: true, values: true}), { value: "0s"}, "toObject string is valid for zero duration");

test.end();
});
Loading