You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
385 lines
14 KiB
385 lines
14 KiB
// Copyright 2021-2024 Buf Technologies, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
import { ScalarType, } from "./descriptors.js";
|
|
import { protoCamelCase } from "./reflect/names.js";
|
|
import { reflect } from "./reflect/reflect.js";
|
|
import { anyUnpack } from "./wkt/index.js";
|
|
import { isWrapperDesc } from "./wkt/wrappers.js";
|
|
import { base64Encode } from "./wire/index.js";
|
|
import { createExtensionContainer, getExtension } from "./extensions.js";
|
|
import { checkField, formatVal } from "./reflect/reflect-check.js";
|
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
// bootstrap-inject google.protobuf.FeatureSet.FieldPresence.LEGACY_REQUIRED: const $name: FeatureSet_FieldPresence.$localName = $number;
|
|
const LEGACY_REQUIRED = 3;
|
|
// bootstrap-inject google.protobuf.FeatureSet.FieldPresence.IMPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number;
|
|
const IMPLICIT = 2;
|
|
// Default options for serializing to JSON.
|
|
const jsonWriteDefaults = {
|
|
alwaysEmitImplicit: false,
|
|
enumAsInteger: false,
|
|
useProtoFieldName: false,
|
|
};
|
|
function makeWriteOptions(options) {
|
|
return options ? Object.assign(Object.assign({}, jsonWriteDefaults), options) : jsonWriteDefaults;
|
|
}
|
|
/**
|
|
* Serialize the message to a JSON value, a JavaScript value that can be
|
|
* passed to JSON.stringify().
|
|
*/
|
|
export function toJson(schema, message, options) {
|
|
return reflectToJson(reflect(schema, message), makeWriteOptions(options));
|
|
}
|
|
/**
|
|
* Serialize the message to a JSON string.
|
|
*/
|
|
export function toJsonString(schema, message, options) {
|
|
var _a;
|
|
const jsonValue = toJson(schema, message, options);
|
|
return JSON.stringify(jsonValue, null, (_a = options === null || options === void 0 ? void 0 : options.prettySpaces) !== null && _a !== void 0 ? _a : 0);
|
|
}
|
|
/**
|
|
* Serialize a single enum value to JSON.
|
|
*/
|
|
export function enumToJson(descEnum, value) {
|
|
var _a;
|
|
if (descEnum.typeName == "google.protobuf.NullValue") {
|
|
return null;
|
|
}
|
|
const name = (_a = descEnum.value[value]) === null || _a === void 0 ? void 0 : _a.name;
|
|
if (name === undefined) {
|
|
throw new Error(`${String(value)} is not a value in ${descEnum.toString()}`);
|
|
}
|
|
return name;
|
|
}
|
|
function reflectToJson(msg, opts) {
|
|
var _a;
|
|
const wktJson = tryWktToJson(msg, opts);
|
|
if (wktJson !== undefined)
|
|
return wktJson;
|
|
const json = {};
|
|
for (const f of msg.sortedFields) {
|
|
if (!msg.isSet(f)) {
|
|
if (f.presence == LEGACY_REQUIRED) {
|
|
throw new Error(`cannot encode field ${msg.desc.typeName}.${f.name} to JSON: required field not set`);
|
|
}
|
|
if (!opts.alwaysEmitImplicit || f.presence !== IMPLICIT) {
|
|
// Fields with implicit presence omit zero values (e.g. empty string) by default
|
|
continue;
|
|
}
|
|
}
|
|
const jsonValue = fieldToJson(f, msg.get(f), opts);
|
|
if (jsonValue !== undefined) {
|
|
json[jsonName(f, opts)] = jsonValue;
|
|
}
|
|
}
|
|
if (opts.registry) {
|
|
const tagSeen = new Set();
|
|
for (const uf of (_a = msg.getUnknown()) !== null && _a !== void 0 ? _a : []) {
|
|
// Same tag can appear multiple times, so we
|
|
// keep track and skip identical ones.
|
|
if (tagSeen.has(uf.no)) {
|
|
continue;
|
|
}
|
|
const extension = opts.registry.getExtensionFor(msg.desc, uf.no);
|
|
if (!extension) {
|
|
continue;
|
|
}
|
|
const value = getExtension(msg.message, extension);
|
|
const [container, field] = createExtensionContainer(extension, value);
|
|
const jsonValue = fieldToJson(field, container.get(field), opts);
|
|
if (jsonValue !== undefined) {
|
|
json[extension.jsonName] = jsonValue;
|
|
}
|
|
}
|
|
}
|
|
return json;
|
|
}
|
|
function fieldToJson(f, val, opts) {
|
|
switch (f.fieldKind) {
|
|
case "scalar":
|
|
return scalarToJson(f, val);
|
|
case "message":
|
|
return reflectToJson(val, opts);
|
|
case "enum":
|
|
return enumToJsonInternal(f.enum, val, opts.enumAsInteger);
|
|
case "list":
|
|
return listToJson(val, opts);
|
|
case "map":
|
|
return mapToJson(val, opts);
|
|
}
|
|
}
|
|
function mapToJson(map, opts) {
|
|
const f = map.field();
|
|
const jsonObj = {};
|
|
switch (f.mapKind) {
|
|
case "scalar":
|
|
for (const [entryKey, entryValue] of map) {
|
|
jsonObj[entryKey] = scalarToJson(f, entryValue);
|
|
}
|
|
break;
|
|
case "message":
|
|
for (const [entryKey, entryValue] of map) {
|
|
jsonObj[entryKey] = reflectToJson(entryValue, opts);
|
|
}
|
|
break;
|
|
case "enum":
|
|
for (const [entryKey, entryValue] of map) {
|
|
jsonObj[entryKey] = enumToJsonInternal(f.enum, entryValue, opts.enumAsInteger);
|
|
}
|
|
break;
|
|
}
|
|
return opts.alwaysEmitImplicit || map.size > 0 ? jsonObj : undefined;
|
|
}
|
|
function listToJson(list, opts) {
|
|
const f = list.field();
|
|
const jsonArr = [];
|
|
switch (f.listKind) {
|
|
case "scalar":
|
|
for (const item of list) {
|
|
jsonArr.push(scalarToJson(f, item));
|
|
}
|
|
break;
|
|
case "enum":
|
|
for (const item of list) {
|
|
jsonArr.push(enumToJsonInternal(f.enum, item, opts.enumAsInteger));
|
|
}
|
|
break;
|
|
case "message":
|
|
for (const item of list) {
|
|
jsonArr.push(reflectToJson(item, opts));
|
|
}
|
|
break;
|
|
}
|
|
return opts.alwaysEmitImplicit || jsonArr.length > 0 ? jsonArr : undefined;
|
|
}
|
|
function enumToJsonInternal(desc, value, enumAsInteger) {
|
|
var _a;
|
|
if (typeof value != "number") {
|
|
throw new Error(`cannot encode ${desc} to JSON: expected number, got ${formatVal(value)}`);
|
|
}
|
|
if (desc.typeName == "google.protobuf.NullValue") {
|
|
return null;
|
|
}
|
|
if (enumAsInteger) {
|
|
return value;
|
|
}
|
|
const val = desc.value[value];
|
|
return (_a = val === null || val === void 0 ? void 0 : val.name) !== null && _a !== void 0 ? _a : value; // if we don't know the enum value, just return the number
|
|
}
|
|
function scalarToJson(field, value) {
|
|
var _a, _b, _c, _d, _e, _f;
|
|
switch (field.scalar) {
|
|
// int32, fixed32, uint32: JSON value will be a decimal number. Either numbers or strings are accepted.
|
|
case ScalarType.INT32:
|
|
case ScalarType.SFIXED32:
|
|
case ScalarType.SINT32:
|
|
case ScalarType.FIXED32:
|
|
case ScalarType.UINT32:
|
|
if (typeof value != "number") {
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_a = checkField(field, value)) === null || _a === void 0 ? void 0 : _a.message}`);
|
|
}
|
|
return value;
|
|
// float, double: JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity".
|
|
// Either numbers or strings are accepted. Exponent notation is also accepted.
|
|
case ScalarType.FLOAT:
|
|
case ScalarType.DOUBLE: // eslint-disable-line no-fallthrough
|
|
if (typeof value != "number") {
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_b = checkField(field, value)) === null || _b === void 0 ? void 0 : _b.message}`);
|
|
}
|
|
if (isNaN(value))
|
|
return "NaN";
|
|
if (value === Number.POSITIVE_INFINITY)
|
|
return "Infinity";
|
|
if (value === Number.NEGATIVE_INFINITY)
|
|
return "-Infinity";
|
|
return value;
|
|
// string:
|
|
case ScalarType.STRING:
|
|
if (typeof value != "string") {
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_c = checkField(field, value)) === null || _c === void 0 ? void 0 : _c.message}`);
|
|
}
|
|
return value;
|
|
// bool:
|
|
case ScalarType.BOOL:
|
|
if (typeof value != "boolean") {
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_d = checkField(field, value)) === null || _d === void 0 ? void 0 : _d.message}`);
|
|
}
|
|
return value;
|
|
// JSON value will be a decimal string. Either numbers or strings are accepted.
|
|
case ScalarType.UINT64:
|
|
case ScalarType.FIXED64:
|
|
case ScalarType.INT64:
|
|
case ScalarType.SFIXED64:
|
|
case ScalarType.SINT64:
|
|
if (typeof value != "bigint" && typeof value != "string") {
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_e = checkField(field, value)) === null || _e === void 0 ? void 0 : _e.message}`);
|
|
}
|
|
return value.toString();
|
|
// bytes: JSON value will be the data encoded as a string using standard base64 encoding with paddings.
|
|
// Either standard or URL-safe base64 encoding with/without paddings are accepted.
|
|
case ScalarType.BYTES:
|
|
if (value instanceof Uint8Array) {
|
|
return base64Encode(value);
|
|
}
|
|
throw new Error(`cannot encode ${field} to JSON: ${(_f = checkField(field, value)) === null || _f === void 0 ? void 0 : _f.message}`);
|
|
}
|
|
}
|
|
function jsonName(f, opts) {
|
|
return opts.useProtoFieldName ? f.name : f.jsonName;
|
|
}
|
|
// returns a json value if wkt, otherwise returns undefined.
|
|
function tryWktToJson(msg, opts) {
|
|
if (!msg.desc.typeName.startsWith("google.protobuf.")) {
|
|
return undefined;
|
|
}
|
|
switch (msg.desc.typeName) {
|
|
case "google.protobuf.Any":
|
|
return anyToJson(msg.message, opts);
|
|
case "google.protobuf.Timestamp":
|
|
return timestampToJson(msg.message);
|
|
case "google.protobuf.Duration":
|
|
return durationToJson(msg.message);
|
|
case "google.protobuf.FieldMask":
|
|
return fieldMaskToJson(msg.message);
|
|
case "google.protobuf.Struct":
|
|
return structToJson(msg.message);
|
|
case "google.protobuf.Value":
|
|
return valueToJson(msg.message);
|
|
case "google.protobuf.ListValue":
|
|
return listValueToJson(msg.message);
|
|
default:
|
|
if (isWrapperDesc(msg.desc)) {
|
|
const valueField = msg.desc.fields[0];
|
|
return scalarToJson(valueField, msg.get(valueField));
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
function anyToJson(val, opts) {
|
|
if (val.typeUrl === "") {
|
|
return {};
|
|
}
|
|
const { registry } = opts;
|
|
let message;
|
|
let desc;
|
|
if (registry) {
|
|
message = anyUnpack(val, registry);
|
|
if (message) {
|
|
desc = registry.getMessage(message.$typeName);
|
|
}
|
|
}
|
|
if (!desc || !message) {
|
|
throw new Error(`cannot encode message ${val.$typeName} to JSON: "${val.typeUrl}" is not in the type registry`);
|
|
}
|
|
let json = reflectToJson(reflect(desc, message), opts);
|
|
if (desc.typeName.startsWith("google.protobuf.") ||
|
|
json === null ||
|
|
Array.isArray(json) ||
|
|
typeof json !== "object") {
|
|
json = { value: json };
|
|
}
|
|
json["@type"] = val.typeUrl;
|
|
return json;
|
|
}
|
|
function durationToJson(val) {
|
|
if (Number(val.seconds) > 315576000000 ||
|
|
Number(val.seconds) < -315576000000) {
|
|
throw new Error(`cannot encode message ${val.$typeName} to JSON: value out of range`);
|
|
}
|
|
let text = val.seconds.toString();
|
|
if (val.nanos !== 0) {
|
|
let nanosStr = Math.abs(val.nanos).toString();
|
|
nanosStr = "0".repeat(9 - nanosStr.length) + nanosStr;
|
|
if (nanosStr.substring(3) === "000000") {
|
|
nanosStr = nanosStr.substring(0, 3);
|
|
}
|
|
else if (nanosStr.substring(6) === "000") {
|
|
nanosStr = nanosStr.substring(0, 6);
|
|
}
|
|
text += "." + nanosStr;
|
|
if (val.nanos < 0 && Number(val.seconds) == 0) {
|
|
text = "-" + text;
|
|
}
|
|
}
|
|
return text + "s";
|
|
}
|
|
function fieldMaskToJson(val) {
|
|
return val.paths
|
|
.map((p) => {
|
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
if (p.match(/_[0-9]?_/g) || p.match(/[A-Z]/g)) {
|
|
throw new Error(`cannot encode message ${val.$typeName} to JSON: lowerCamelCase of path name "` +
|
|
p +
|
|
'" is irreversible');
|
|
}
|
|
return protoCamelCase(p);
|
|
})
|
|
.join(",");
|
|
}
|
|
function structToJson(val) {
|
|
const json = {};
|
|
for (const [k, v] of Object.entries(val.fields)) {
|
|
json[k] = valueToJson(v);
|
|
}
|
|
return json;
|
|
}
|
|
function valueToJson(val) {
|
|
switch (val.kind.case) {
|
|
case "nullValue":
|
|
return null;
|
|
case "numberValue":
|
|
if (!Number.isFinite(val.kind.value)) {
|
|
throw new Error(`${val.$typeName} cannot be NaN or Infinity`);
|
|
}
|
|
return val.kind.value;
|
|
case "boolValue":
|
|
return val.kind.value;
|
|
case "stringValue":
|
|
return val.kind.value;
|
|
case "structValue":
|
|
return structToJson(val.kind.value);
|
|
case "listValue":
|
|
return listValueToJson(val.kind.value);
|
|
default:
|
|
throw new Error(`${val.$typeName} must have a value`);
|
|
}
|
|
}
|
|
function listValueToJson(val) {
|
|
return val.values.map(valueToJson);
|
|
}
|
|
function timestampToJson(val) {
|
|
const ms = Number(val.seconds) * 1000;
|
|
if (ms < Date.parse("0001-01-01T00:00:00Z") ||
|
|
ms > Date.parse("9999-12-31T23:59:59Z")) {
|
|
throw new Error(`cannot encode message ${val.$typeName} to JSON: must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive`);
|
|
}
|
|
if (val.nanos < 0) {
|
|
throw new Error(`cannot encode message ${val.$typeName} to JSON: nanos must not be negative`);
|
|
}
|
|
let z = "Z";
|
|
if (val.nanos > 0) {
|
|
const nanosStr = (val.nanos + 1000000000).toString().substring(1);
|
|
if (nanosStr.substring(3) === "000000") {
|
|
z = "." + nanosStr.substring(0, 3) + "Z";
|
|
}
|
|
else if (nanosStr.substring(6) === "000") {
|
|
z = "." + nanosStr.substring(0, 6) + "Z";
|
|
}
|
|
else {
|
|
z = "." + nanosStr + "Z";
|
|
}
|
|
}
|
|
return new Date(ms).toISOString().replace(".000Z", z);
|
|
}
|