Files
dev/third_party/pi/packages/chord/src/json.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

136 lines
5.2 KiB
TypeScript

export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
type IsAny<T> = 0 extends 1 & T ? true : false;
/** Strict-JSON representation of an application data type. Unknown payloads become JsonValue. */
export type JsonRepresentation<T> = IsAny<T> extends true
? JsonValue
: unknown extends T
? JsonValue
: T extends null | boolean | number | string
? T
: T extends readonly (infer TItem)[]
? JsonRepresentation<TItem>[]
: T extends object
? { [TKey in keyof T]: JsonRepresentation<T[TKey]> }
: never;
const DATA_DESCRIPTOR: PropertyDescriptor = {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
};
export type CopyJsonOptions = {
/** Omit undefined object properties while preserving strict array semantics. */
readonly omitUndefinedProperties?: boolean;
};
/** Copy a value into an alias-free strict-JSON tree owned by the caller. */
export function copyJson(value: unknown, options?: CopyJsonOptions): JsonValue {
return copy(value, undefined, options?.omitUndefinedProperties === true);
}
function copy(value: unknown, ancestors: Set<object> | undefined, omitUndefinedProperties: boolean): JsonValue {
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number") {
if (Number.isFinite(value)) return value;
throw new TypeError("Value contains a non-finite number and is not strict JSON");
}
if (typeof value !== "object")
throw new TypeError(`Value contains a non-JSON ${typeof value}; expected strict JSON`);
const active = ancestors ?? new Set<object>();
if (active.has(value)) throw new TypeError("Value contains cycles and is not strict JSON");
active.add(value);
try {
if (Array.isArray(value)) {
// Indices plus `length` only: extra, symbol, and missing keys all change the count.
if (Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1) {
throw new TypeError("Value must contain strict JSON dense plain arrays");
}
const result = new Array<JsonValue>(value.length);
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, index);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Value must contain strict JSON enumerable indexed data properties");
}
defineData(result, String(index), copy(descriptor.value, active, omitUndefinedProperties));
}
return result;
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Value must contain strict JSON plain objects or arrays");
}
const result = Object.create(prototype) as Record<string, JsonValue>;
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Value contains a symbol key and is not strict JSON");
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
if (!descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Value must contain strict JSON enumerable data properties");
}
if (descriptor.value === undefined && omitUndefinedProperties) continue;
defineData(result, key, copy(descriptor.value, active, omitUndefinedProperties));
}
return result;
} finally {
active.delete(value);
}
}
function defineData(target: object, key: PropertyKey, value: JsonValue): void {
DATA_DESCRIPTOR.value = value;
Object.defineProperty(target, key, DATA_DESCRIPTOR);
DATA_DESCRIPTOR.value = undefined;
}
/** Return whether a value is finite strict JSON with plain objects and no cycles. */
export function isJsonValue(value: unknown): value is JsonValue {
return check(value, new Set<object>());
}
function check(value: unknown, ancestors: Set<object>): boolean {
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) {
if (Object.getPrototypeOf(value) !== Array.prototype) return false;
const keys = Reflect.ownKeys(value);
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string")) return false;
if (ancestors.has(value)) return false;
ancestors.add(value);
try {
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (
descriptor === undefined ||
!descriptor.enumerable ||
!("value" in descriptor) ||
!check(descriptor.value, ancestors)
) {
return false;
}
}
return true;
} finally {
ancestors.delete(value);
}
}
if (typeof value !== "object" || value === null) return false;
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) return false;
if (Reflect.ownKeys(value).some((key) => typeof key !== "string")) return false;
if (ancestors.has(value)) return false;
ancestors.add(value);
try {
for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
if (!descriptor.enumerable || !("value" in descriptor) || !check(descriptor.value, ancestors)) {
return false;
}
}
return true;
} finally {
ancestors.delete(value);
}
}