diff --git a/cli/package-lock.json b/cli/package-lock.json index ab03aeb27..64b147c58 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -35,11 +35,11 @@ } }, "..": { - "name": "protobufjs", - "version": "7.1.2", + "name": "@postman/protobufjs", + "version": "7.3.2-postman.2", "dev": true, "hasInstallScript": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", diff --git a/ext/descriptor/index.js b/ext/descriptor/index.js index 6aafd2ab8..7525eb22f 100644 --- a/ext/descriptor/index.js +++ b/ext/descriptor/index.js @@ -70,7 +70,7 @@ var Namespace = $protobuf.Namespace, * @param {IFileDescriptorSet|Reader|Uint8Array} descriptor Descriptor * @returns {Root} Root instance */ -Root.fromDescriptor = function fromDescriptor(descriptor) { +Root.fromDescriptor = function fromDescriptor(descriptor, options) { // Decode the descriptor message if specified as a buffer: if (typeof descriptor.length === "number") @@ -89,7 +89,7 @@ Root.fromDescriptor = function fromDescriptor(descriptor) { root.files.push(filePackage.filename = fileDescriptor.name); if (fileDescriptor.messageType) for (i = 0; i < fileDescriptor.messageType.length; ++i) - filePackage.add(Type.fromDescriptor(fileDescriptor.messageType[i], fileDescriptor.syntax)); + filePackage.add(Type.fromDescriptor(fileDescriptor.messageType[i], fileDescriptor.syntax, options)); if (fileDescriptor.enumType) for (i = 0; i < fileDescriptor.enumType.length; ++i) filePackage.add(Enum.fromDescriptor(fileDescriptor.enumType[i])); @@ -198,7 +198,7 @@ var unnamedMessageIndex = 0; * @param {string} [syntax="proto2"] Syntax * @returns {Type} Type instance */ -Type.fromDescriptor = function fromDescriptor(descriptor, syntax) { +Type.fromDescriptor = function fromDescriptor(descriptor, syntax, options) { // Decode the descriptor message if specified as a buffer: if (typeof descriptor.length === "number") @@ -206,6 +206,7 @@ Type.fromDescriptor = function fromDescriptor(descriptor, syntax) { // Create the message type var type = new Type(descriptor.name.length ? descriptor.name : "Type" + unnamedMessageIndex++, fromDescriptorOptions(descriptor.options, exports.MessageOptions)), + useMapField = options && options.useMapField, i; /* Oneofs */ if (descriptor.oneofDecl) @@ -213,7 +214,17 @@ Type.fromDescriptor = function fromDescriptor(descriptor, syntax) { type.add(OneOf.fromDescriptor(descriptor.oneofDecl[i])); /* Fields */ if (descriptor.field) for (i = 0; i < descriptor.field.length; ++i) { - var field = Field.fromDescriptor(descriptor.field[i], syntax); + var mapType = useMapField && descriptor.nestedType + ? descriptor.nestedType.find(function (t) { + var currField = descriptor.field[i]; + var nestedTypeName = currField.typeName && currField.typeName.split('.').pop(); + return t.options && t.options.mapEntry && t.name === nestedTypeName + }) + : null; + var field = mapType + ? MapField.fromDescriptor(descriptor.field[i], mapType, syntax) + : Field.fromDescriptor(descriptor.field[i], syntax); + type.add(field); if (descriptor.field[i].hasOwnProperty("oneofIndex")) // eslint-disable-line no-prototype-builtins type.oneofsArray[descriptor.field[i].oneofIndex].add(field); @@ -223,9 +234,15 @@ Type.fromDescriptor = function fromDescriptor(descriptor, syntax) { type.add(Field.fromDescriptor(descriptor.extension[i], syntax)); /* Nested types */ if (descriptor.nestedType) for (i = 0; i < descriptor.nestedType.length; ++i) { - type.add(Type.fromDescriptor(descriptor.nestedType[i], syntax)); - if (descriptor.nestedType[i].options && descriptor.nestedType[i].options.mapEntry) - type.setOption("map_entry", true); + if (useMapField) { + // Nested types representing map entry are added as MapField and should not be added as Type + if (!descriptor.nestedType[i].options || !descriptor.nestedType[i].options.mapEntry) + type.add(Type.fromDescriptor(descriptor.nestedType[i], syntax, options)); + } else { + type.add(Type.fromDescriptor(descriptor.nestedType[i], syntax, options)); + if (descriptor.nestedType[i].options && descriptor.nestedType[i].options.mapEntry) + type.setOption("map_entry", true); + } } /* Nested enums */ if (descriptor.enumType) for (i = 0; i < descriptor.enumType.length; ++i) @@ -503,6 +520,65 @@ Field.prototype.toDescriptor = function toDescriptor(syntax) { return descriptor; }; +// --- MapField --- + +/** + * Creates a map field from a descriptor. + * @param {IFieldDescriptorProto|Reader|Uint8Array} descriptor Descriptor + * @param {IDescriptorProto} nestedType Nested type descriptor + * @returns {MapField} MapField instance + */ +MapField.fromDescriptor = function fromDescriptor(descriptor, nestedType) { + // Decode the descriptor message if specified as a buffer: + if (typeof descriptor.length === "number") + descriptor = exports.DescriptorProto.decode(descriptor); + + // Decode the nested type if specified as a buffer: + if (typeof nestedType.length === "number") + nestedType = exports.DescriptorProto.decode(nestedType); + + if (typeof descriptor.number !== "number") + throw Error("missing field id"); + + var typeName = nestedType.field[1].typeName; + var type = typeName && typeName.length + ? typeName + : fromDescriptorType(nestedType.field[1].type); + + var field = new MapField( + descriptor.name.length ? descriptor.name : "field" + descriptor.number, + descriptor.number, + fromDescriptorType(nestedType.field[0].type), + type, + fromDescriptorOptions(descriptor.options, exports.FieldOptions) + ); + + var extendee = descriptor.extendee; + if (extendee !== undefined) { + field.extend = extendee.length ? extendee : undefined; + } + + if (descriptor.defaultValue && descriptor.defaultValue.length) { + var defaultValue = descriptor.defaultValue; + switch (defaultValue) { + case "true": case "TRUE": + defaultValue = true; + break; + case "false": case "FALSE": + defaultValue = false; + break; + default: + var match = numberRe.exec(defaultValue); + if (match) + defaultValue = parseInt(defaultValue); // eslint-disable-line radix + break; + } + field.setOption("default", defaultValue); + } + + return field; +} + // --- Enum --- /** diff --git a/google/rpc/error_details.json b/google/rpc/error_details.json new file mode 100644 index 000000000..43aa1a5c5 --- /dev/null +++ b/google/rpc/error_details.json @@ -0,0 +1,204 @@ +{ + "nested": { + "google": { + "nested": { + "rpc": { + "nested": { + "RetryInfo": { + "fields": { + "retryDelay": { + "type": "Duration", + "id": 1 + } + }, + "nested": { + "Duration": { + "fields": { + "seconds": { + "type": "int64", + "id": 1 + }, + "nanos": { + "type": "int32", + "id": 2 + } + } + } + } + }, + "DebugInfo": { + "fields": { + "stackEntries": { + "rule": "repeated", + "type": "string", + "id": 1 + }, + "detail": { + "type": "string", + "id": 2 + } + } + }, + "QuotaFailure": { + "fields": { + "violations": { + "rule": "repeated", + "type": "Violation", + "id": 1 + } + }, + "nested": { + "Violation": { + "fields": { + "subject": { + "type": "string", + "id": 1 + }, + "description": { + "type": "string", + "id": 2 + } + } + } + } + }, + "ErrorInfo": { + "fields": { + "reason": { + "type": "string", + "id": 1 + }, + "domain": { + "type": "string", + "id": 2 + }, + "metadata": { + "keyType": "string", + "type": "string", + "id": 3 + } + } + }, + "PreconditionFailure": { + "fields": { + "violations": { + "rule": "repeated", + "type": "Violation", + "id": 1 + } + }, + "nested": { + "Violation": { + "fields": { + "type": { + "type": "string", + "id": 1 + }, + "subject": { + "type": "string", + "id": 2 + }, + "description": { + "type": "string", + "id": 3 + } + } + } + } + }, + "BadRequest": { + "fields": { + "fieldViolations": { + "rule": "repeated", + "type": "FieldViolation", + "id": 1 + } + }, + "nested": { + "FieldViolation": { + "fields": { + "field": { + "type": "string", + "id": 1 + }, + "description": { + "type": "string", + "id": 2 + } + } + } + } + }, + "RequestInfo": { + "fields": { + "requestId": { + "type": "string", + "id": 1 + }, + "servingData": { + "type": "string", + "id": 2 + } + } + }, + "ResourceInfo": { + "fields": { + "resourceType": { + "type": "string", + "id": 1 + }, + "resourceName": { + "type": "string", + "id": 2 + }, + "owner": { + "type": "string", + "id": 3 + }, + "description": { + "type": "string", + "id": 4 + } + } + }, + "Help": { + "fields": { + "links": { + "rule": "repeated", + "type": "Link", + "id": 1 + } + }, + "nested": { + "Link": { + "fields": { + "description": { + "type": "string", + "id": 1 + }, + "url": { + "type": "string", + "id": 2 + } + } + } + } + }, + "LocalizedMessage": { + "fields": { + "locale": { + "type": "string", + "id": 1 + }, + "message": { + "type": "string", + "id": 2 + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/google/rpc/error_details.proto b/google/rpc/error_details.proto new file mode 100644 index 000000000..40d90c372 --- /dev/null +++ b/google/rpc/error_details.proto @@ -0,0 +1,77 @@ +syntax = "proto3"; + +package google.rpc; + +message RetryInfo { + message Duration { + int64 seconds = 1; + int32 nanos = 2; + } + + Duration retry_delay = 1; +} + +message DebugInfo { + repeated string stack_entries = 1; + string detail = 2; +} + +message QuotaFailure { + message Violation { + string subject = 1; + string description = 2; + } + + repeated Violation violations = 1; +} + +message ErrorInfo { + string reason = 1; + string domain = 2; + map metadata = 3; +} + +message PreconditionFailure { + message Violation { + string type = 1; + string subject = 2; + string description = 3; + } + + repeated Violation violations = 1; +} + +message BadRequest { + message FieldViolation { + string field = 1; + string description = 2; + } + + repeated FieldViolation field_violations = 1; +} + +message RequestInfo { + string request_id = 1; + string serving_data = 2; +} + +message ResourceInfo { + string resource_type = 1; + string resource_name = 2; + string owner = 3; + string description = 4; +} + +message Help { + message Link { + string description = 1; + string url = 2; + } + + repeated Link links = 1; +} + +message LocalizedMessage { + string locale = 1; + string message = 2; +} diff --git a/google/rpc/status.json b/google/rpc/status.json new file mode 100644 index 000000000..18ba47f93 --- /dev/null +++ b/google/rpc/status.json @@ -0,0 +1,45 @@ +{ + "nested": { + "google": { + "nested": { + "protobuf": { + "nested": { + "Any": { + "fields": { + "type_url": { + "type": "string", + "id": 1 + }, + "value": { + "type": "bytes", + "id": 2 + } + } + } + } + }, + "rpc": { + "nested": { + "Status": { + "fields": { + "code": { + "type": "int32", + "id": 1 + }, + "message": { + "type": "string", + "id": 2 + }, + "details": { + "rule": "repeated", + "type": "google.protobuf.Any", + "id": 3 + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/google/rpc/status.proto b/google/rpc/status.proto new file mode 100644 index 000000000..50e324607 --- /dev/null +++ b/google/rpc/status.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +message Status { + int32 code = 1; + string message = 2; + repeated google.protobuf.Any details = 3; +} diff --git a/index.d.ts b/index.d.ts index 0744761c8..e8c95cbff 100644 --- a/index.d.ts +++ b/index.d.ts @@ -42,12 +42,12 @@ export namespace common { /** Properties of a google.protobuf.Value message. */ interface IValue { kind?: string; - nullValue?: 0; - numberValue?: number; - stringValue?: string; - boolValue?: boolean; - structValue?: IStruct; - listValue?: IListValue; + null_value?: 0; + number_value?: number; + string_value?: string; + bool_value?: boolean; + struct_value?: IStruct; + list_value?: IListValue; } /** Properties of a google.protobuf.ListValue message. */ @@ -2152,6 +2152,14 @@ export namespace util { */ function safeProp(prop: string): string; + /** + * Returns the value of a property found directly in a given object. + * @param object Source object + * @param prop Property name + * @returns Value or `undefined` if not set + */ + function getProp(object: object, prop: string): any; + /** * Converts the first character of a string to upper case. * @param str String to convert diff --git a/package-lock.json b/package-lock.json index f6d604a6d..ddf219ed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { - "name": "protobufjs", - "version": "7.3.2", + "name": "@postman/protobufjs", + "version": "7.3.2-postman.3", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "protobufjs", - "version": "7.3.2", + "name": "@postman/protobufjs", + "version": "7.3.2-postman.3", "hasInstallScript": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -5767,6 +5767,7 @@ "node_modules/jaguarjs-jsdoc": { "version": "0.0.1", "resolved": "git+ssh://git@github.com/dcodeIO/jaguarjs-jsdoc.git#ade85ac841f5ca8be40c60d506102039a036a8fa", + "integrity": "sha512-3FqwgUAqzFxO1FfRFvWUh3TtSrMSgtgeg5Twe64FbQTTXJ1I42K9hRnrwARv6NKUKi/74bPWaZHBlDj3b4Hx5A==", "dev": true, "license": "MIT" }, @@ -15474,6 +15475,7 @@ }, "jaguarjs-jsdoc": { "version": "git+ssh://git@github.com/dcodeIO/jaguarjs-jsdoc.git#ade85ac841f5ca8be40c60d506102039a036a8fa", + "integrity": "sha512-3FqwgUAqzFxO1FfRFvWUh3TtSrMSgtgeg5Twe64FbQTTXJ1I42K9hRnrwARv6NKUKi/74bPWaZHBlDj3b4Hx5A==", "dev": true, "from": "jaguarjs-jsdoc@github:dcodeIO/jaguarjs-jsdoc" }, diff --git a/package.json b/package.json index bd57b132e..b2e4cdaff 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { - "name": "protobufjs", - "version": "7.3.2", + "name": "@postman/protobufjs", + "version": "7.3.2-postman.3", "versionScheme": "~", "description": "Protocol Buffers for JavaScript (& TypeScript).", - "author": "Daniel Wirtz ", - "license": "BSD-3-Clause", - "repository": "protobufjs/protobuf.js", - "bugs": "https://github.com/protobufjs/protobuf.js/issues", - "homepage": "https://protobufjs.github.io/protobuf.js/", + "license": "Apache-2.0", + "repository": "https://github.com/postmanlabs/protobuf.js", + "bugs": "https://github.com/postmanlabs/protobuf.js/issues", + "homepage": "https://github.com/postmanlabs/protobuf.js", "engines": { "node": ">=12.0.0" }, diff --git a/src/common.js b/src/common.js index 489ee1c67..fce6ee831 100644 --- a/src/common.js +++ b/src/common.js @@ -139,49 +139,49 @@ common("struct", { * @interface IValue * @type {Object} * @property {string} [kind] - * @property {0} [nullValue] - * @property {number} [numberValue] - * @property {string} [stringValue] - * @property {boolean} [boolValue] - * @property {IStruct} [structValue] - * @property {IListValue} [listValue] + * @property {0} [null_value] + * @property {number} [number_value] + * @property {string} [string_value] + * @property {boolean} [bool_value] + * @property {IStruct} [struct_value] + * @property {IListValue} [list_value] * @memberof common */ Value: { oneofs: { kind: { oneof: [ - "nullValue", - "numberValue", - "stringValue", - "boolValue", - "structValue", - "listValue" + "null_value", + "number_value", + "string_value", + "bool_value", + "struct_value", + "list_value" ] } }, fields: { - nullValue: { + null_value: { type: "NullValue", id: 1 }, - numberValue: { + number_value: { type: "double", id: 2 }, - stringValue: { + string_value: { type: "string", id: 3 }, - boolValue: { + bool_value: { type: "bool", id: 4 }, - structValue: { + struct_value: { type: "Struct", id: 5 }, - listValue: { + list_value: { type: "ListValue", id: 6 } diff --git a/src/converter.js b/src/converter.js index 086e00307..390402bc8 100644 --- a/src/converter.js +++ b/src/converter.js @@ -40,10 +40,13 @@ function genValuePartial_fromObject(gen, field, fieldIndex, prop) { ("break"); } gen ("}"); - } else gen - ("if(typeof d%s!==\"object\")", prop) - ("throw TypeError(%j)", field.fullName + ": object expected") - ("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop); + } else + if (field.resolvedType.fullName === ".google.protobuf.Duration" || field.resolvedType.fullName === ".google.protobuf.Timestamp") { gen + ("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop); + } else gen + ("if(typeof d%s!==\"object\")", prop) + ("throw TypeError(%j)", field.fullName + ": object expected") + ("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop); } else { var isUnsigned = false; switch (field.type) { diff --git a/src/namespace.js b/src/namespace.js index 731afc75f..4d35de8da 100644 --- a/src/namespace.js +++ b/src/namespace.js @@ -191,7 +191,7 @@ Namespace.prototype.addJSON = function addJSON(nestedJson) { * @returns {ReflectionObject|null} The reflection object or `null` if it doesn't exist */ Namespace.prototype.get = function get(name) { - return this.nested && this.nested[name] + return util.getProp(this.nested, name) || null; }; diff --git a/src/service.js b/src/service.js index bc2c3080c..0d2b9428b 100644 --- a/src/service.js +++ b/src/service.js @@ -98,7 +98,7 @@ function clearCache(service) { * @override */ Service.prototype.get = function get(name) { - return this.methods[name] + return util.getProp(this.methods, name) || Namespace.prototype.get.call(this, name); }; diff --git a/src/type.js b/src/type.js index b477a4ee2..485d4145f 100644 --- a/src/type.js +++ b/src/type.js @@ -312,9 +312,9 @@ Type.prototype.resolveAll = function resolveAll() { * @override */ Type.prototype.get = function get(name) { - return this.fields[name] - || this.oneofs && this.oneofs[name] - || this.nested && this.nested[name] + return util.getProp(this.fields, name) + || util.getProp(this.oneofs, name) + || util.getProp(this.nested, name) || null; }; diff --git a/src/util.js b/src/util.js index 6c508990b..fb004116f 100644 --- a/src/util.js +++ b/src/util.js @@ -78,6 +78,16 @@ util.safeProp = function safeProp(prop) { return "." + prop; }; +/** + * Returns the value of a property found directly in a given object. + * @param {Object} object Source object + * @param {string} prop Property name + * @returns {*} Value or `undefined` if not set + */ +util.getProp = function get(object, prop) { + return object && Object.prototype.hasOwnProperty.call(object, prop) ? object[prop] : undefined; +}; + /** * Converts the first character of a string to upper case. * @param {string} str String to convert diff --git a/src/util/is-legacy-struct.js b/src/util/is-legacy-struct.js new file mode 100644 index 000000000..da5dfc7fb --- /dev/null +++ b/src/util/is-legacy-struct.js @@ -0,0 +1,56 @@ +"use strict"; +module.exports = isLegacyStruct; + +/** + * Identifies where the payload for a struct is in the form of a legacy struct. + * The legacy format is - + * + * { + * fields: { + * "key1": { + * "string_value": "test", + * }, + * "key2": { + * "number_value": 123, + * } + * } + * } + * + * @param {object} payload + * @returns {boolean} + */ +function isLegacyStruct(payload) { + // Value types in a struct + const valueKeysSet = new Set(['string_value', 'number_value', 'bool_value', 'struct_value', 'list_value', 'null_value']); + + // If object has only one key and that key is "fields" which is an object + if (payload && Object.keys(payload).length === 1 && payload.fields && typeof payload.fields === "object") { + // Case when the fields key is an array. This can be of the form - + // { + // fields: [ + // {key: "key1", value: {string_value: "test"}}, + // {key: "key2", value: {number_value: 123}} + // ] + // } + if (Array.isArray(payload.fields)) { + return payload.fields.every(field => Object.keys(field).length === 2 && + field.key && field.value && Object.keys(field.value).length === 1 + && valueKeysSet.has(Object.keys(field.value)[0])); + } + + // Get all the values of the fields object + // For the given example - + // fieldValues = [{string_value: "test"}, {number_value: 123}] + const fieldValues = Object.values(payload.fields); + + // Check if all the fieldValues have only one key and that key is a valid value type + if (fieldValues.every(fieldValue => { + const fieldValueKeys = fieldValue ? Object.keys(fieldValue) : []; + return fieldValueKeys.length === 1 && valueKeysSet.has(fieldValueKeys[0]); + })) { + return true; + } + } + + return false; +} diff --git a/src/verifier.js b/src/verifier.js index d58e27abe..df9479b95 100644 --- a/src/verifier.js +++ b/src/verifier.js @@ -30,12 +30,23 @@ function genVerifyValue(gen, field, fieldIndex, ref) { ("break") ("}"); } else { - gen - ("{") - ("var e=types[%i].verify(%s);", fieldIndex, ref) - ("if(e)") - ("return%j+e", field.name + ".") - ("}"); + // Special handling for Duration type to allow string input + if (field.resolvedType.fullName === ".google.protobuf.Duration" || field.resolvedType.fullName === ".google.protobuf.Timestamp") { gen + ("if(typeof %s===\"string\")", ref) + (";") // Allow string input for Duration + ("else {") + ("var e=types[%i].verify(%s);", fieldIndex, ref) + ("if(e)") + ("return%j+e", field.name + ".") + ("}") + } else { + gen + ("{") + ("var e=types[%i].verify(%s);", fieldIndex, ref) + ("if(e)") + ("return%j+e", field.name + ".") + ("}"); + } } } else { switch (field.type) { diff --git a/src/wrappers.js b/src/wrappers.js index 131e21c05..f409da4c9 100644 --- a/src/wrappers.js +++ b/src/wrappers.js @@ -8,6 +8,8 @@ var wrappers = exports; var Message = require("./message"); +var isLegacyStruct = require("./util/is-legacy-struct"); +var util = require("./util"); /** * From object converter part of an {@link IWrapper}. @@ -100,3 +102,384 @@ wrappers[".google.protobuf.Any"] = { return this.toObject(message, options); } }; + +// Custom wrapper for Struct +wrappers[".google.protobuf.Struct"] = { + fromObject: function(object) { + // If already a Struct instance, return as is + if (object instanceof this.ctor) return object; + // Convert plain JS object to Struct + var fields = {}; + if (object && typeof object === "object" && !Array.isArray(object)) { + + // If the struct is in the form of a legacy struct, we don't need to convert it. + // Return the fields object as is + if (isLegacyStruct(object)) { + var newFields = Array.isArray(object.fields) ? object.fields.reduce(function(acc, field) { + acc[field.key] = field.value; + return acc; + }, {}) : object.fields; + + return this.create({ fields: newFields }); + } + + for (var k in object) { + if (object[k] !== undefined) { + fields[k] = this.lookup("Value").fromObject(object[k]); + } + } + + return this.create({ fields }); + } + + return this.fromObject(object); + }, + toObject: function(message, options) { + // Convert Struct message to plain JS object + var obj = {}; + if (message && message.fields) { + var Value = this.lookup("Value"); + for (var k in message.fields) { + obj[k] = Value.toObject(message.fields[k], options); + } + return obj; + } + + return this.toObject(message, options); + } +}; + +// Custom wrapper for Value +wrappers[".google.protobuf.Value"] = { + fromObject: function(object) { + // If already a Value instance, return as is + if (object instanceof this.ctor) return object; + + // Handle different types and convert to appropriate Value field + if (object === null || object === undefined) { + return this.create({ null_value: 0 }); + } + + if (typeof object === "string") { + return this.create({ string_value: object }); + } + + if (typeof object === "number") { + return this.create({ number_value: object }); + } + + if (typeof object === "boolean") { + return this.create({ bool_value: object }); + } + + if (Array.isArray(object)) { + // Use the ListValue wrapper's fromObject to ensure correct construction + var ListValue = this.lookup("ListValue"); + return this.create({ list_value: ListValue.fromObject(object) }); + } + + if (typeof object === "object") { + // Convert object to Struct + var Struct = this.lookup("Struct"); + return this.create({ struct_value: Struct.fromObject(object) }); + } + + return this.fromObject(object); + }, + toObject: function(message, options) { + // Convert Value message to plain JS object + if (message.hasOwnProperty("null_value")) { + return null; + } + if (message.hasOwnProperty("string_value")) { + return message.string_value; + } + if (message.hasOwnProperty("number_value")) { + return message.number_value; + } + if (message.hasOwnProperty("bool_value")) { + return message.bool_value; + } + if (message.hasOwnProperty("list_value")) { + var values = []; + for (var i = 0; i < message.list_value.values.length; i++) { + values.push(wrappers[".google.protobuf.Value"].toObject.call(this, message.list_value.values[i], options)); + } + return values; + } + if (message.hasOwnProperty("struct_value")) { + var Struct = this.lookup("Struct"); + return Struct.toObject(message.struct_value, options); + } + + return this.toObject(message, options); + } +}; + +// Custom wrapper for ListValue +wrappers[".google.protobuf.ListValue"] = { + fromObject: function(object) { + // If already a ListValue instance, return as is + if (object instanceof this.ctor) return object; + + // Convert array to ListValue + if (Array.isArray(object)) { + var values = []; + var Value = this.lookup("Value"); + for (var i = 0; i < object.length; i++) { + values.push(Value.fromObject(object[i])); + } + var msg = this.create(); + msg.values = values; + return msg; + } + + return this.fromObject(object); + }, + toObject: function(message, options) { + // Convert ListValue message to plain JS array + if (message && message.values) { + var values = []; + var Value = this.lookup("Value"); + for (var i = 0; i < message.values.length; i++) { + values.push(Value.toObject(message.values[i], options)); + } + return values; + } + + return this.toObject(message, options); + } +}; + +// Custom wrapper for Duration +wrappers[".google.protobuf.Duration"] = { + fromObject: function(object) { + // If already a Duration instance, return as is + if (object instanceof this.ctor) return object; + + // Handle string input (e.g., "1.5s", "2m", "1h", "1h30m", "500ms", "250us", "100ns") + if (typeof object === "string") { + // Parse compound duration string like "1h30m15s" or "1.5s500ms250us100ns" + // Note: Multiple segments of the same unit are allowed and will be added together + // e.g., "2s32.232s" becomes "34.232s" + var totalSeconds = 0; + var totalNanos = 0; + var sign = 1; + + // Check for negative duration + if (object.startsWith('-')) { + sign = -1; + object = object.substring(1); + } + + // Match all duration parts (e.g., "1h", "30m", "15s", "500ms", "250us", "100ns") + var durationRegex = /(\d+(?:\.\d+)?)(ms|us|μs|ns|[smh])/g; + var matches = []; + var match; + while ((match = durationRegex.exec(object)) !== null) { + matches.push(match); + } + + if (matches.length === 0) { + throw new Error("Invalid duration format. Expected units: h, m, s, ms, us or ns (e.g. 1h30m, 2m, 5s, -1s)"); + } + + // Track units used for validation/warning + var unitsUsed = { s: 0, m: 0, h: 0, ms: 0, us: 0, ns: 0 }; + + // Process all matches + for (var i = 0; i < matches.length; i++) { + var value = parseFloat(matches[i][1]); + var unit = matches[i][2]; + + // Count usage of each unit + unitsUsed[unit]++; + + switch (unit) { + case 's': + totalSeconds += Math.floor(value); + totalNanos += Math.round((value - Math.floor(value)) * 1000000000); + break; + case 'm': + var minutesTotalSeconds = value * 60; + totalSeconds += Math.floor(minutesTotalSeconds); + totalNanos += Math.round((minutesTotalSeconds - Math.floor(minutesTotalSeconds)) * 1000000000); + break; + case 'h': + var hoursTotalSeconds = value * 3600; + totalSeconds += Math.floor(hoursTotalSeconds); + totalNanos += Math.round((hoursTotalSeconds - Math.floor(hoursTotalSeconds)) * 1000000000); + break; + case 'ms': + // Convert milliseconds to seconds and nanos + var msSeconds = Math.floor(value / 1000); + var msNanos = Math.round((value % 1000) * 1000000); + totalSeconds += msSeconds; + totalNanos += msNanos; + break; + case 'μs': + case 'us': + // Convert microseconds to seconds and nanos + var usSeconds = Math.floor(value / 1000000); + var usNanos = Math.round((value % 1000000) * 1000); + totalSeconds += usSeconds; + totalNanos += usNanos; + break; + case 'ns': + // Convert nanoseconds to seconds and nanos + var nsSeconds = Math.floor(value / 1000000000); + var nsNanos = Math.round(value % 1000000000); + totalSeconds += nsSeconds; + totalNanos += nsNanos; + break; + } + } + + // Warn about unusual formats (multiple segments of same unit) + var duplicateUnits = Object.keys(unitsUsed).filter(unit => unitsUsed[unit] > 1); + if (duplicateUnits.length > 0) { + console.warn('Warning: Duplicate units found in duration:', duplicateUnits.join(', '), + 'in input:', object, '- segments will be added together'); + } + + // Handle nanos overflow + if (totalNanos >= 1000000000) { + totalSeconds += Math.floor(totalNanos / 1000000000); + totalNanos = totalNanos % 1000000000; + } + + // Use util.Long.fromValue to properly create Long objects for int64 fields + return this.create({ + seconds: util.Long.fromValue(sign * totalSeconds), + nanos: sign * totalNanos + }); + } + + // Handle number input (seconds) + if (typeof object === "number") { + var seconds = Math.floor(object); + var nanos = Math.round((object - seconds) * 1000000000); + return this.create({ seconds: seconds, nanos: nanos }); + } + + // Handle object input + if (object && typeof object === "object") { + return this.create(object); + } + + return this.fromObject(object); + }, + toObject: function(message, options) { + // Convert Duration message to string representation + if (message && typeof message === "object" && !Array.isArray(message)) { + // Handle Long objects for seconds field + var seconds = message.seconds; + if (seconds && typeof seconds === 'object' && seconds.low !== undefined) { + // Convert Long to number using utility + seconds = seconds.toNumber(); + } + + // Handle durations correctly for all sign combinations + var totalSeconds; + var nanosSeconds = message.nanos / 1000000000; + + if (seconds < 0 && message.nanos < 0) { + // Both negative: add them together (both contribute to negative duration) + totalSeconds = seconds + nanosSeconds; + } else if (seconds < 0 && message.nanos >= 0) { + // Negative seconds, positive nanos: subtract nanos from negative seconds + totalSeconds = seconds - nanosSeconds; + } else if (seconds >= 0 && message.nanos < 0) { + // Positive seconds, negative nanos: subtract nanos from positive seconds + totalSeconds = seconds + nanosSeconds; // nanosSeconds is already negative + } else { + // Both positive: add them together + totalSeconds = seconds + nanosSeconds; + } + + if (totalSeconds === 0) return "0s"; + + var sign = totalSeconds < 0 ? "-" : ""; + totalSeconds = Math.abs(totalSeconds); + + // Always return duration in seconds format + if (totalSeconds === Math.floor(totalSeconds)) { + // Integer seconds + return sign + totalSeconds + "s"; + } else { + // Fractional seconds - use up to 9 decimal places, then clip trailing zeros + var formatted = totalSeconds.toFixed(9); + // Remove trailing zeros and decimal point if all zeros + formatted = formatted.replace(/\.?0+$/, ''); + return sign + formatted + "s"; + } + } + + return this.toObject(message, options); + } +}; + +// Custom wrapper for Timestamp +wrappers[".google.protobuf.Timestamp"] = { + fromObject: function(object) { + // If already a Timestamp instance, return as is + if (object instanceof this.ctor) { + return object; + } + + // Handle Date object + if (object instanceof Date) { + var seconds = Math.floor(object.getTime() / 1000); + var nanos = (object.getTime() % 1000) * 1000000; + // Use util.Long.fromValue to properly create Long objects for int64 fields + return this.create({ seconds: util.Long.fromValue(seconds), nanos: nanos }); + } + + // Handle number input (milliseconds since epoch) + if (typeof object === "number") { + var seconds = Math.floor(object / 1000); + var nanos = (object % 1000) * 1000000; + // Use util.Long.fromValue to properly create Long objects for int64 fields + return this.create({ seconds: util.Long.fromValue(seconds), nanos: nanos }); + } + + // Handle string input (ISO 8601 format) + if (typeof object === "string") { + var date = new Date(object); + if (isNaN(date.getTime())) { + throw new Error("Invalid timestamp format. Expected ISO 8601 format."); + } + var seconds = Math.floor(date.getTime() / 1000); + var nanos = (date.getTime() % 1000) * 1000000; + // Use util.Long.fromValue to properly create Long objects for int64 fields + return this.create({ seconds: util.Long.fromValue(seconds), nanos: nanos }); + } + + // Handle object input (but not Timestamp instances) + if (object && typeof object === "object" && !(object instanceof this.ctor)) { + return this.create(object); + } + + // Fallback to default behavior - call the original fromObject method + // Use the original fromObject method that was stored in originalThis.fromObject + return this.fromObject(object); + }, + toObject: function(message, options) { + + // Convert Timestamp message to Date object or ISO string + if (message && typeof message === "object" && !Array.isArray(message)) { + // Handle Long objects for seconds field + var seconds = message.seconds; + if (seconds && typeof seconds === 'object' && seconds.low !== undefined) { + // Convert Long to number using utility + seconds = seconds.toNumber(); + } + + var milliseconds = seconds * 1000 + Math.floor(message.nanos / 1000000); + var date = new Date(milliseconds); + return date.toISOString(); + } + + return this.toObject(message, options); + } +}; diff --git a/tests/data/common.json b/tests/data/common.json index f2875a788..d21c69d8e 100644 --- a/tests/data/common.json +++ b/tests/data/common.json @@ -47,37 +47,37 @@ "oneofs": { "kind": { "oneof": [ - "nullValue", - "numberValue", - "stringValue", - "boolValue", - "structValue", - "listValue" + "null_value", + "number_value", + "string_value", + "bool_value", + "struct_value", + "list_value" ] } }, "fields": { - "nullValue": { + "null_value": { "type": "NullValue", "id": 1 }, - "numberValue": { + "number_value": { "type": "double", "id": 2 }, - "stringValue": { + "string_value": { "type": "string", "id": 3 }, - "boolValue": { + "bool_value": { "type": "bool", "id": 4 }, - "structValue": { + "struct_value": { "type": "Struct", "id": 5 }, - "listValue": { + "list_value": { "type": "ListValue", "id": 6 } @@ -114,4 +114,4 @@ } } } -} \ No newline at end of file +}