"use strict"; // There are some things that cannot be implemented without `any`. // No `any` “leaks” when _using_ the library, though. /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); exports.repr = exports.format = exports.flatMap = exports.map = exports.nullOr = exports.undefinedOr = exports.recursive = exports.multi = exports.tuple = exports.tag = exports.taggedUnion = exports.field = exports.fields = exports.record = exports.array = exports.primitiveUnion = exports.string = exports.bigint = exports.number = exports.boolean = exports.unknown = exports.JSON = void 0; const CodecJSON = { parse(codec, jsonString) { let json; try { json = JSON.parse(jsonString); } catch (unknownError) { const error = unknownError; // `JSON.parse` always throws `Error` instances. return { tag: "DecoderError", error: { tag: "custom", message: `${error.name}: ${error.message}`, path: [], }, }; } return codec.decoder(json); }, stringify(codec, value, space) { return JSON.stringify(codec.encoder(value), null, space) ?? "null"; }, }; exports.JSON = CodecJSON; function identity(value) { return value; } exports.unknown = { decoder: (value) => ({ tag: "Valid", value }), encoder: identity, }; exports.boolean = { decoder: (value) => typeof value === "boolean" ? { tag: "Valid", value } : { tag: "DecoderError", error: { tag: "boolean", got: value, path: [] }, }, encoder: identity, }; exports.number = { decoder: (value) => typeof value === "number" ? { tag: "Valid", value } : { tag: "DecoderError", error: { tag: "number", got: value, path: [] }, }, encoder: identity, }; exports.bigint = { decoder: (value) => typeof value === "bigint" ? { tag: "Valid", value } : { tag: "DecoderError", error: { tag: "bigint", got: value, path: [] }, }, encoder: identity, }; exports.string = { decoder: (value) => typeof value === "string" ? { tag: "Valid", value } : { tag: "DecoderError", error: { tag: "string", got: value, path: [] }, }, encoder: identity, }; function primitiveUnion(variants) { return { decoder: (value) => variants.includes(value) ? { tag: "Valid", value: value } : { tag: "DecoderError", error: { tag: "unknown primitiveUnion variant", knownVariants: variants, got: value, path: [], }, }, encoder: identity, }; } exports.primitiveUnion = primitiveUnion; function unknownArray(value) { return Array.isArray(value) ? { tag: "Valid", value } : { tag: "DecoderError", error: { tag: "array", got: value, path: [] }, }; } function unknownRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) ? { tag: "Valid", value: value } : { tag: "DecoderError", error: { tag: "object", got: value, path: [] }, }; } function array(codec) { return { decoder: (value) => { const arrResult = unknownArray(value); if (arrResult.tag === "DecoderError") { return arrResult; } const arr = arrResult.value; const result = []; for (let index = 0; index < arr.length; index++) { const decoderResult = codec.decoder(arr[index]); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, path: [index, ...decoderResult.error.path], }, }; case "Valid": result.push(decoderResult.value); break; } } return { tag: "Valid", value: result }; }, encoder: (arr) => { const result = []; for (const item of arr) { result.push(codec.encoder(item)); } return result; }, }; } exports.array = array; function record(codec) { return { decoder: (value) => { const objectResult = unknownRecord(value); if (objectResult.tag === "DecoderError") { return objectResult; } const object = objectResult.value; const keys = Object.keys(object); const result = {}; for (const key of keys) { if (key === "__proto__") { continue; } const decoderResult = codec.decoder(object[key]); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, path: [key, ...decoderResult.error.path], }, }; case "Valid": result[key] = decoderResult.value; break; } } return { tag: "Valid", value: result }; }, encoder: (object) => { const result = {}; for (const [key, value] of Object.entries(object)) { if (key === "__proto__") { continue; } result[key] = codec.encoder(value); } return result; }, }; } exports.record = record; function fields(mapping, { allowExtraFields = true } = {}) { return { decoder: (value) => { const objectResult = unknownRecord(value); if (objectResult.tag === "DecoderError") { return objectResult; } const object = objectResult.value; const knownFields = new Set(); const result = {}; for (const [key, fieldOrCodec] of Object.entries(mapping)) { if (key === "__proto__") { continue; } const field_ = "codec" in fieldOrCodec ? fieldOrCodec : { codec: fieldOrCodec }; const { codec: { decoder }, renameFrom: encodedFieldName = key, optional: isOptional = false, } = field_; if (encodedFieldName === "__proto__") { continue; } knownFields.add(encodedFieldName); if (!(encodedFieldName in object)) { if (!isOptional) { return { tag: "DecoderError", error: { tag: "missing field", field: encodedFieldName, got: object, path: [], }, }; } continue; } const decoderResult = decoder(object[encodedFieldName]); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, path: [encodedFieldName, ...decoderResult.error.path], }, }; case "Valid": result[key] = decoderResult.value; break; } } if (!allowExtraFields) { const unknownFields = Object.keys(object).filter((key) => !knownFields.has(key)); if (unknownFields.length > 0) { return { tag: "DecoderError", error: { tag: "exact fields", knownFields: Array.from(knownFields), got: unknownFields, path: [], }, }; } } return { tag: "Valid", value: result }; }, encoder: (object) => { const result = {}; for (const [key, fieldOrCodec] of Object.entries(mapping)) { if (key === "__proto__") { continue; } const field_ = "codec" in fieldOrCodec ? fieldOrCodec : { codec: fieldOrCodec }; const { codec: { encoder }, renameFrom: encodedFieldName = key, optional: isOptional = false, } = field_; if (encodedFieldName === "__proto__" || (isOptional && !(key in object))) { continue; } const value = object[key]; result[encodedFieldName] = encoder(value); } return result; }, }; } exports.fields = fields; function field(codec, meta) { return { codec, ...meta, }; } exports.field = field; function taggedUnion(decodedCommonField, variants, { allowExtraFields = true } = {}) { if (decodedCommonField === "__proto__") { throw new Error("taggedUnion: decoded common field cannot be __proto__"); } const decoderMap = new Map(); // encodedName -> decoder const encoderMap = new Map(); // decodedName -> encoder let maybeEncodedCommonField = undefined; for (const [index, variant] of variants.entries()) { const field_ = variant[decodedCommonField]; const { renameFrom: encodedFieldName = decodedCommonField, } = field_; if (maybeEncodedCommonField === undefined) { maybeEncodedCommonField = encodedFieldName; } else if (maybeEncodedCommonField !== encodedFieldName) { throw new Error(`taggedUnion: Variant at index ${index}: Key ${JSON.stringify(decodedCommonField)}: Got a different encoded field name (${JSON.stringify(encodedFieldName)}) than before (${JSON.stringify(maybeEncodedCommonField)}).`); } const fullCodec = fields(variant, { allowExtraFields }); decoderMap.set(field_.tag.encoded, fullCodec.decoder); encoderMap.set(field_.tag.decoded, fullCodec.encoder); } if (typeof maybeEncodedCommonField !== "string") { throw new Error(`taggedUnion: Got unusable encoded common field: ${repr(maybeEncodedCommonField)}`); } const encodedCommonField = maybeEncodedCommonField; return { decoder: (value) => { const encodedNameResult = fields({ [encodedCommonField]: exports.unknown, }).decoder(value); if (encodedNameResult.tag === "DecoderError") { return encodedNameResult; } const encodedName = encodedNameResult.value[encodedCommonField]; const decoder = decoderMap.get(encodedName); if (decoder === undefined) { return { tag: "DecoderError", error: { tag: "unknown taggedUnion tag", knownTags: Array.from(decoderMap.keys()), got: encodedName, path: [encodedCommonField], }, }; } return decoder(value); }, encoder: (value) => { const decodedName = value[decodedCommonField]; const encoder = encoderMap.get(decodedName); if (encoder === undefined) { throw new Error(`taggedUnion: Unexpectedly found no encoder for decoded variant name: ${JSON.stringify(decodedName)} at key ${JSON.stringify(decodedCommonField)}`); } return encoder(value); }, }; } exports.taggedUnion = taggedUnion; function tag(decoded, options = {}) { const encoded = "renameTagFrom" in options ? options.renameTagFrom : decoded; return { codec: { decoder: (value) => value === encoded ? { tag: "Valid", value: decoded } : { tag: "DecoderError", error: { tag: "wrong tag", expected: encoded, got: value, path: [], }, }, encoder: () => encoded, }, renameFrom: options.renameFieldFrom, tag: { decoded, encoded }, }; } exports.tag = tag; function tuple(codecs) { return { decoder: (value) => { const arrResult = unknownArray(value); if (arrResult.tag === "DecoderError") { return arrResult; } const arr = arrResult.value; if (arr.length !== codecs.length) { return { tag: "DecoderError", error: { tag: "tuple size", expected: codecs.length, got: arr.length, path: [], }, }; } const result = []; for (const [index, codec] of codecs.entries()) { const decoderResult = codec.decoder(arr[index]); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, path: [index, ...decoderResult.error.path], }, }; case "Valid": result.push(decoderResult.value); break; } } return { tag: "Valid", value: result }; }, encoder: (value) => { const result = []; for (const [index, codec] of codecs.entries()) { result.push(codec.encoder(value[index])); } return result; }, }; } exports.tuple = tuple; function multi(types) { return { decoder: (value) => { if (value === undefined) { if (types.includes("undefined")) { return { tag: "Valid", value: { type: "undefined", value }, }; } } else if (value === null) { if (types.includes("null")) { return { tag: "Valid", value: { type: "null", value }, }; } } else if (typeof value === "boolean") { if (types.includes("boolean")) { return { tag: "Valid", value: { type: "boolean", value }, }; } } else if (typeof value === "number") { if (types.includes("number")) { return { tag: "Valid", value: { type: "number", value }, }; } } else if (typeof value === "bigint") { if (types.includes("bigint")) { return { tag: "Valid", value: { type: "bigint", value }, }; } } else if (typeof value === "string") { if (types.includes("string")) { return { tag: "Valid", value: { type: "string", value }, }; } } else if (typeof value === "symbol") { if (types.includes("symbol")) { return { tag: "Valid", value: { type: "symbol", value }, }; } } else if (typeof value === "function") { if (types.includes("function")) { return { tag: "Valid", value: { type: "function", value }, }; } } else if (Array.isArray(value)) { if (types.includes("array")) { return { tag: "Valid", value: { type: "array", value }, }; } } else { if (types.includes("object")) { return { tag: "Valid", value: { type: "object", value }, }; } } return { tag: "DecoderError", error: { tag: "unknown multi type", knownTypes: types, got: value, path: [], }, }; }, encoder: (value) => value.value, }; } exports.multi = multi; function recursive(callback) { return { decoder: (value) => callback().decoder(value), encoder: (value) => callback().encoder(value), }; } exports.recursive = recursive; function undefinedOr(codec) { return { decoder: (value) => { if (value === undefined) { return { tag: "Valid", value: undefined }; } const decoderResult = codec.decoder(value); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, orExpected: decoderResult.error.orExpected === "null" ? "null or undefined" : "undefined", }, }; case "Valid": return decoderResult; } }, encoder: (value) => value === undefined ? undefined : codec.encoder(value), }; } exports.undefinedOr = undefinedOr; function nullOr(codec) { return { decoder: (value) => { if (value === null) { return { tag: "Valid", value: null }; } const decoderResult = codec.decoder(value); switch (decoderResult.tag) { case "DecoderError": return { tag: "DecoderError", error: { ...decoderResult.error, orExpected: decoderResult.error.orExpected === "undefined" ? "null or undefined" : "null", }, }; case "Valid": return decoderResult; } }, encoder: (value) => (value === null ? null : codec.encoder(value)), }; } exports.nullOr = nullOr; function map(codec, transform) { return { decoder: (value) => { const decoderResult = codec.decoder(value); switch (decoderResult.tag) { case "DecoderError": return decoderResult; case "Valid": return { tag: "Valid", value: transform.decoder(decoderResult.value), }; } }, encoder: (value) => codec.encoder(transform.encoder(value)), }; } exports.map = map; function flatMap(codec, transform) { return { decoder: (value) => { const decoderResult = codec.decoder(value); switch (decoderResult.tag) { case "DecoderError": return decoderResult; case "Valid": return transform.decoder(decoderResult.value); } }, encoder: (value) => codec.encoder(transform.encoder(value)), }; } exports.flatMap = flatMap; function format(error, options) { const path = error.path.map((part) => `[${JSON.stringify(part)}]`).join(""); const variant = formatDecoderErrorVariant(error, options); const orExpected = error.orExpected === undefined ? "" : `\nOr expected: ${error.orExpected}`; return `At root${path}:\n${variant}${orExpected}`; } exports.format = format; function formatDecoderErrorVariant(variant, options) { const formatGot = (value) => { const formatted = repr(value, options); return options?.sensitive === true ? `${formatted}\n(Actual values are hidden in sensitive mode.)` : formatted; }; const removeBrackets = (formatted) => formatted.replace(/^\[|\s*\]$/g, ""); const primitiveList = (strings) => strings.length === 0 ? " (none)" : removeBrackets(repr(strings, { maxLength: Infinity, maxArrayChildren: Infinity, indent: options?.indent, })); switch (variant.tag) { case "boolean": case "number": case "bigint": case "string": return `Expected a ${variant.tag}\nGot: ${formatGot(variant.got)}`; case "array": case "object": return `Expected an ${variant.tag}\nGot: ${formatGot(variant.got)}`; case "unknown multi type": return `Expected one of these types: ${variant.knownTypes.length === 0 ? "never" : variant.knownTypes.join(", ")}\nGot: ${formatGot(variant.got)}`; case "unknown taggedUnion tag": return `Expected one of these tags:${primitiveList(variant.knownTags)}\nGot: ${formatGot(variant.got)}`; case "unknown primitiveUnion variant": return `Expected one of these variants:${primitiveList(variant.knownVariants)}\nGot: ${formatGot(variant.got)}`; case "missing field": return `Expected an object with a field called: ${JSON.stringify(variant.field)}\nGot: ${formatGot(variant.got)}`; case "wrong tag": return `Expected this string: ${JSON.stringify(variant.expected)}\nGot: ${formatGot(variant.got)}`; case "exact fields": return `Expected only these fields:${primitiveList(variant.knownFields)}\nFound extra fields:${removeBrackets(formatGot(variant.got))}`; case "tuple size": return `Expected ${variant.expected} items\nGot: ${variant.got}`; case "custom": return "got" in variant ? `${variant.message}\nGot: ${formatGot(variant.got)}` : variant.message; } } function repr(value, { depth = 0, indent = " ", maxArrayChildren = 5, maxObjectChildren = 5, maxLength = 100, sensitive = false, } = {}) { return reprHelper(value, { depth, maxArrayChildren, maxObjectChildren, maxLength, indent, sensitive, }, 0, []); } exports.repr = repr; function reprHelper(value, options, level, seen) { const { indent, maxLength, sensitive } = options; const type = typeof value; const toStringType = Object.prototype.toString .call(value) .replace(/^\[object\s+(.+)\]$/, "$1"); try { if (value == null || type === "number" || type === "bigint" || type === "boolean" || type === "symbol" || toStringType === "RegExp") { return sensitive ? toStringType.toLowerCase() : truncate(String(value) + (type === "bigint" ? "n" : ""), maxLength); } if (type === "string") { return sensitive ? type : truncate(JSON.stringify(value), maxLength); } if (typeof value === "function") { return `function ${truncate(JSON.stringify(value.name), maxLength)}`; } if (Array.isArray(value)) { const arr = value; if (arr.length === 0) { return "[]"; } if (seen.includes(arr)) { return `circular ${toStringType}(${arr.length})`; } if (options.depth < level) { return `${toStringType}(${arr.length})`; } const lastIndex = arr.length - 1; const items = []; const end = Math.min(options.maxArrayChildren - 1, lastIndex); for (let index = 0; index <= end; index++) { const item = index in arr ? reprHelper(arr[index], options, level + 1, [...seen, arr]) : ""; items.push(item); } if (end < lastIndex) { items.push(`(${lastIndex - end} more)`); } return `[\n${indent.repeat(level + 1)}${items.join(`,\n${indent.repeat(level + 1)}`)}\n${indent.repeat(level)}]`; } if (toStringType === "Object") { const object = value; const keys = Object.keys(object); // `class Foo {}` has `toStringType === "Object"` and `name === "Foo"`. const { name } = object.constructor; const prefix = name === "Object" ? "" : `${name} `; if (keys.length === 0) { return `${prefix}{}`; } if (seen.includes(object)) { return `circular ${name}(${keys.length})`; } if (options.depth < level) { return `${name}(${keys.length})`; } const numHidden = Math.max(0, keys.length - options.maxObjectChildren); const items = keys .slice(0, options.maxObjectChildren) .map((key2) => { const truncatedKey = truncate(JSON.stringify(key2), maxLength); const valueRepr = reprHelper(object[key2], options, level + 1, [ ...seen, object, ]); const separator = valueRepr.includes("\n") || truncatedKey.length + valueRepr.length + 2 <= maxLength // `2` accounts for the colon and space. ? " " : `\n${indent.repeat(level + 2)}`; return `${truncatedKey}:${separator}${valueRepr}`; }) .concat(numHidden > 0 ? `(${numHidden} more)` : []); return `${prefix}{\n${indent.repeat(level + 1)}${items.join(`,\n${indent.repeat(level + 1)}`)}\n${indent.repeat(level)}}`; } return toStringType; } catch (_error) { return toStringType; } } function truncate(str, maxLength) { const half = Math.floor(maxLength / 2); return str.length <= maxLength ? str : `${str.slice(0, half)}…${str.slice(-half)}`; }