import { assert, sortObjectByKey, isPlainObject } from '../../util/util.js'; import { JSONWithUndefined } from '../params_utils.js'; // JSON can't represent various values and by default stores them as `null`. // Instead, storing them as a magic string values in JSON. const jsUndefinedMagicValue = '_undef_'; const jsNaNMagicValue = '_nan_'; const jsPositiveInfinityMagicValue = '_posinfinity_'; const jsNegativeInfinityMagicValue = '_neginfinity_'; // -0 needs to be handled separately, because -0 === +0 returns true. Not // special casing +0/0, since it behaves intuitively. Assuming that if -0 is // being used, the differentiation from +0 is desired. const jsNegativeZeroMagicValue = '_negzero_'; // bigint values are not defined in JSON, so need to wrap them up as strings const jsBigIntMagicPattern = /^(\d+)n$/; const toStringMagicValue = new Map([ [undefined, jsUndefinedMagicValue], [NaN, jsNaNMagicValue], [Number.POSITIVE_INFINITY, jsPositiveInfinityMagicValue], [Number.NEGATIVE_INFINITY, jsNegativeInfinityMagicValue], // No -0 handling because it is special cased. ]); const fromStringMagicValue = new Map([ [jsUndefinedMagicValue, undefined], [jsNaNMagicValue, NaN], [jsPositiveInfinityMagicValue, Number.POSITIVE_INFINITY], [jsNegativeInfinityMagicValue, Number.NEGATIVE_INFINITY], // -0 is handled in this direction because there is no comparison issue. [jsNegativeZeroMagicValue, -0], ]); function stringifyFilter(_k: string, v: unknown): unknown { // Make sure no one actually uses a magic value as a parameter. if (typeof v === 'string') { assert( !fromStringMagicValue.has(v), `${v} is a magic value for stringification, so cannot be used` ); assert( v !== jsNegativeZeroMagicValue, `${v} is a magic value for stringification, so cannot be used` ); assert( v.match(jsBigIntMagicPattern) === null, `${v} matches bigint magic pattern for stringification, so cannot be used` ); } const isObject = v !== null && typeof v === 'object' && !Array.isArray(v); if (isObject) { assert( isPlainObject(v), `value must be a plain object but it appears to be a '${ Object.getPrototypeOf(v).constructor.name }` ); } assert(typeof v !== 'function', `${v} can not be a function`); if (Object.is(v, -0)) { return jsNegativeZeroMagicValue; } if (typeof v === 'bigint') { return `${v}n`; } return toStringMagicValue.has(v) ? toStringMagicValue.get(v) : v; } export function stringifyParamValue(value: JSONWithUndefined): string { return JSON.stringify(value, stringifyFilter); } /** * Like stringifyParamValue but sorts dictionaries by key, for hashing. */ export function stringifyParamValueUniquely(value: JSONWithUndefined): string { return JSON.stringify(value, (k, v) => { if (typeof v === 'object' && v !== null) { return sortObjectByKey(v); } return stringifyFilter(k, v); }); } // 'any' is part of the JSON.parse reviver interface, so cannot be avoided. // eslint-disable-next-line @typescript-eslint/no-explicit-any function parseParamValueReviver(_k: string, v: any): any { if (fromStringMagicValue.has(v)) { return fromStringMagicValue.get(v); } if (typeof v === 'string') { const match: RegExpMatchArray | null = v.match(jsBigIntMagicPattern); if (match !== null) { // [0] is the entire match, and following entries are the capture groups return BigInt(match[1]); } } return v; } export function parseParamValue(s: string): JSONWithUndefined { return JSON.parse(s, parseParamValueReviver); }