UNPKG

@freeword/meta

Version:

Meta package for Freeword: exports all core types, constants, and utilities from the src/ directory.

464 lines 20 kB
import _ /**/ from 'lodash'; import { DateTime } from 'luxon'; import { ZodIssueCode as ZIC } from 'zod'; import { TS_EARLIEST, TS_FARTHEST } from "../Consts.js"; // import { jsonify, adorn, setNormalProp, objectish, arrayish, inspectify, shortenWithEllipsis, smush, toSentence, hardcapList, decorate, } from "../utils/BaseUtils.js"; const EmptyTypeDescriptions = { undefined: `undefined`, null: `null`, nan: `not-a-number`, void: `void`, never: `never`, }; const TypeDescriptions = { string: `text`, number: `a number`, integer: `an integer`, float: `a decimal`, boolean: `true/false`, date: `a date`, bigint: `a bigint`, symbol: `a jssym`, function: `a function`, array: `an array`, object: `an object`, unknown: `an idk`, promise: `an async`, map: `a js Map`, set: `a js Set`, ...EmptyTypeDescriptions, }; export function customErrorMap(issue, ctx) { if (issue.code === ZIC.invalid_union) { const message = unionizeIssue(issue, ctx.data, ctx.defaultError, { step: 'unionize', unionized: true, ctx }); return { message }; } return { message: customMessage(issue, ctx.data, ctx.defaultError, { step: 'ceMap', ctx }) }; } export const PRESENCE_REQD = new Set([ZIC.invalid_type, ZIC.too_small]); export const SKIP_VALPART = new Set([ZIC.unrecognized_keys]); function customMessage(issue, subj, defaultError, opts = {}) { const message = makeCustomMessage(issue, subj, defaultError, opts); // console.warn(inspectify(subj, { maxlen: 1000 }), defaultError, '\nresult customMessage:\n', message, '\n') return message; } const INSTANCE_OF_RE = /(Input not )?instance of */; const DOUBLE_TROUBLE_RE = / *is a .+/; function disappointedinyou(badprop) { if (badprop === UNSET_VALUE) { return 'is unset'; } if (badprop === undefined) { return 'is missing'; } if (badprop === null) { return 'is nil'; } const actualClass = badprop?.constructor?.name ?? (typeof badprop) ?? '(anon)'; // nulls shouldn't get here return `is a ${actualClass}`; } function fixInstanceofMessage(message, _issue, badprop, defaultError, _opts = {}) { const bemorelikeyoursister = disappointedinyou(badprop); if (!message) { return bemorelikeyoursister + ' (' + defaultError + ')'; } if (DOUBLE_TROUBLE_RE.test(message)) { return message; } // sometimes it loops through twice if (INSTANCE_OF_RE.test(message)) { return message.replace(INSTANCE_OF_RE, bemorelikeyoursister + ', need a '); } return bemorelikeyoursister + ', needs ' + message; } export function makeCustomMessage(issue, subj, defaultError, opts = {}) { const { code, message } = issue; const badprop = _.hasIn(opts.ctx, 'data') ? opts.ctx?.data : getBadprop(issue, subj, opts); // console.warn('custom', 'message', message, code, typeof badprop, badprop?.constructor?.name, '\nbad prop\n', inspectify(badprop, { maxlen: 1000 }), inspectify(subj, { maxlen: 1000 }), '\nopts', opts, '\nzod issue\n', issue) // if (code === ZIC.unrecognized_keys) { return unrecognizedKeysAdvice(issue, badprop); } if (isVacant(badprop) && (PRESENCE_REQD.has(code))) { return 'is ' + displayVacancy(badprop); } if (code === ZIC.invalid_literal) { return `should be the value ${display(issue.expected)}`; } if (code === ZIC.not_multiple_of) { return `should be an exact multiple of ${display(issue.multipleOf)}`; } if (code === ZIC.not_finite) { return `should be finite`; } if (code === ZIC.invalid_type) { return invalidTypeAdvice(issue, badprop, defaultError, opts); } if (code === ZIC.invalid_union) { return unionizeIssue(issue, subj, defaultError, opts); } if (code === ZIC.invalid_enum_value) { return (issue.options.length === 1) ? `should be ${display(issue.options.join())}` : `should be one of ${joinChoices(issue.options)}`; } if (code === ZIC.too_big) { return smush(' ', boundsAdvice(issue, badprop, issue.maximum), message); } if (code === ZIC.too_small) { return smush(' ', boundsAdvice(issue, badprop, issue.minimum), message); } // if (code === ZIC.invalid_string) { return invalidStringAdvice(issue, badprop, defaultError, opts); } if (code === ZIC.custom && issue.params?.msg) { return issue.params.msg; } // if (code === ZIC.custom) { return fixInstanceofMessage(message, issue, badprop, defaultError, opts); // if (! message) { return defaultError } // if (INSTANCE_OF_RE.test(message)) { return fixInstanceofMessage(message, issue, badprop, defaultError, opts) } // return message ?? defaultError } if (code === ZIC.invalid_union_discriminator) { const tags = issue.options.join('/'); const tagfield = _.last(issue.path); console.warn('invalid_union_discriminator', jsonify(issue), subj, opts, badprop, defaultError); return `union tag ${tagfield} value '' should be one of ${tags}`; } // console.warn('please add this case to ZodReporting.customMessage', jsonify(issue), subj, opts, badprop, defaultError); // if (code === ZIC.invalid_arguments) { return defaultError; } if (code === ZIC.invalid_return_type) { return defaultError; } if (code === ZIC.invalid_date) { return defaultError; } return defaultError; } function unrecognizedKeysAdvice(issue, badprop) { const badprops = _.pick(badprop, issue.keys); const kvs = displayKVs(badprops, issue.keys); const splayedKVs = (_.size(issue.keys) <= 1) ? kvs : ('{' + kvs + '}'); return smush('.', issue.path.join('.'), splayedKVs); } function invalidTypeAdvice(issue, _badprop, _defaultError, _opts = {}) { return `is ${TypeDescriptions[issue.received]} but should be ${TypeDescriptions[issue.expected]}`; } function invalidStringAdvice(issue, badprop, defaultError, opts = {}) { if (issue.message && (!/^(Invalid|Expected|Required|Expected)/.test(issue.message))) { return issue.message; } const { validation } = issue; if (validation === 'regex') { return opts.checker?.hasDescription ? `should be a ${opts.checker?.checkname}` : 'should match pattern'; } if (_.isString(validation)) { return `should be a ${validation}`; } if ('includes' in validation) { const where = validation.position ? ` at ${validation.position}` : ''; return `should contain ${display(validation.includes)}${where}`; } if ('startsWith' in validation) { return `should start with ${display(validation.startsWith)}`; } if ('endsWith' in validation) { return `should end with ${display(validation.endsWith)}`; } console.warn('unfamiliar invalid_string issue', jsonify(issue), badprop, defaultError); return defaultError; } function segAndSep(seg, idx) { if (_.isNumber(seg)) { return `[${seg}]`; } const dotmaybe = ((idx === 0) ? '' : '.'); if (/^([A-Za-z]\w*)$/.test(seg)) { return dotmaybe + seg; } return dotmaybe + inspectify(seg); } function dotpathFor(segs) { return _.map(segs, (seg, idx) => segAndSep(seg, idx)).join(''); } /* eslint-disable no-param-reassign */ // function unionizeIssue(issue: Z.ZodInvalidUnionIssue, badprop: any, badpropStr: string): { message: IssueDetails } { function unionizeIssue(issue, subj, _defaultError, opts = {}) { const subIssues = _.flatten(_.map(issue.unionErrors, 'issues')); adorn(issue, 'unionErrors', issue.unionErrors); // quiet, you const dotpath = dotpathFor(issue.path); const subMessages = _.uniq(_.map(subIssues, (subIssue) => { const badprop = getBadprop(subIssue, subj, opts); const subDotpath = subIssue.path.join('.'); const subPathing = (subDotpath === dotpath || (!subDotpath)) ? '' : subDotpath + ' should '; const submsg = customMessage(subIssue, subj, subIssue.message, opts); subIssue.message = submsg; if (subIssue.code === ZIC.invalid_type) { // collapse all should be a X but got a Y into one short message. return subPathing + ((isVacant(badprop) ? ('not be ' + displayVacancy(badprop)) : ('be ' + TypeDescriptions[subIssue.expected]))); } return subPathing + submsg; })); if (subMessages.length === 1) { return 'should ' + subMessages.join(); } const msg = joinUnionedMessages(subMessages); return 'should either ' + msg; } /* eslint-enable no-param-reassign */ function boundsAdvice(issue, badprop, expected) { const { code } = issue; if (issue.message && (badprop !== undefined)) { return ''; } // message will be appended if (issue.type === 'date') { const expectedStr = displayDate(expected); const direction = (issue.code === ZIC.too_small) ? 'after' : 'before'; return (issue.inclusive) ? `should be on or ${direction} ${expectedStr}` : `should be strictly ${direction} ${expectedStr}`; } const expectedStr = display(expected); if (issue.type === 'set' || issue.type === 'array') { // `have no items` / `have one item` / `have 4 items` / `have at most one item` / `have at least one item` if (issue.exact) { if (expected === 0) { return 'should be empty'; } return `has ${display(_.size(badprop))} items but should have exactly ` + nItems(expected); } if (code === ZIC.too_big) { if (expected === 0) { return 'should be empty'; } return `has ${display(_.size(badprop))} but should have ${nItems(expected)} or fewer`; } // too small if (expected <= 1) { return 'should not be empty'; } return `has ${display(_.size(badprop))} items but should have ${display(expected)} or more`; } if (issue.type === 'string') { if (issue.exact) { if (expected === 0) { return 'should be empty'; } if (expected === 1) { return 'should have exactly one character'; } return `has length ${display(_.size(badprop))} vs ${display(expected)} needed`; } if (code === ZIC.too_big) { if (expected === 0) { return 'be empty'; } return `is too long: ${display(_.size(badprop))} vs ${display(expected)} available`; } if (_.isEmpty(badprop) || (expected <= 1)) { return 'should not be empty'; } return `is too short: ${display(_.size(badprop))} vs ${display(expected)}`; } if (issue.exact) { return `be ${expectedStr}`; } if (expected === Number.MAX_SAFE_INTEGER || expected === Number.MIN_SAFE_INTEGER) { return 'should not be unusually large'; } const direction = (issue.code === ZIC.too_small) ? 'more' : 'less'; return (issue.inclusive) ? `should be ${expectedStr} or ${direction}` : `should be strictly ${direction} than ${expectedStr}`; } function nItems(expected) { if (expected === 0) { return 'no items'; } if (expected === 1) { return 'one'; } return `${display(expected)}`; } const UNSET_VALUE = Symbol('UNSET_VALUE'); export function getBadprop(issue, subj, opts = {}) { if (opts.step === 'ceMap') { return subj; } const { code, path } = issue; if (code === ZIC.unrecognized_keys) { const within = _.isEmpty(path) ? subj : _.get(subj, path); return _.pick(within, issue.keys); } if (_.isEmpty(path)) { return subj; } if ((!objectish(subj)) && (!arrayish(subj))) { return subj; } const { received } = issue; if (received && (code !== ZIC.invalid_type)) { return received; } if (!_.hasIn(subj, path)) { // see if it's a default value const fieldChecker = _.get(opts.checker?.shape, path); if (_.isFunction(fieldChecker?._def?.defaultValue)) { try { return fieldChecker._def.defaultValue(opts); } catch (err) { /** noop */ } return '(bad default)'; } if (fieldChecker?._def?.typeName === 'ZodDefault') { return fieldChecker._def.defaultValue; } // if we can only find undefined, but it's an invalid type that isn't undefined, return the typename, better than nothing if (received && (code === ZIC.invalid_type) && (received !== 'undefined')) { return received; } // Give up. Using UNSET_VALUE shows that we investigated and found nothing, vs somehow skipped this step return UNSET_VALUE; } return _.get(subj, path); } export function display(val, opts = {}) { const { len = 180, brace = true } = opts; const core = shortenWithEllipsis(inspectify(val, { ...opts, maxlen: len + 10 }), len).replaceAll(/\\/g, '~^'); return brace ? frenchbrace(core) : core; } export function frenchbrace(str) { return ${str}»`; } function joinMessages(subMessages) { return toSentence(subMessages, { joiner: '; ', yadayada: ', …' }); } function joinUnionedMessages(subMessages) { return toSentence(subMessages, { joiner: '; ', yadayada: ', …', conj: 'or' }).replaceAll(/((should )+(either )?(be +)?)/g, ''); // .replaceAll(/(should *)+( *(either))* /g, 'should ') } function joinBadprops(badprops) { return toSentence(badprops, { joiner: '; ', yadayada: ', …' }); } function joinChoices(choices) { return toSentence(choices, { conj: 'or', max: 3, yadayada: '…', joiner: ',' }); } function displayKVs(obj, keys, opts = {}) { const kvs = _.map(keys, (key) => [key, _.get(obj, key)]); const displayed = _.map(kvs, ([key, val]) => (String(key) + '=' + display(val, opts))); return toSentence(displayed, { yadayada: ', …' }); } function displayVacancy(val) { if (val === undefined) { return 'missing'; } if (val === '') { return 'blank'; } if (val === null) { return 'nil'; } if (_.isNaN(val)) { return 'an invalid number'; } if (val === UNSET_VALUE) { return 'unset'; } return 'empty'; } export function isVacant(badprop) { return (badprop === '') || _.isUndefined(badprop) || _.isNull(badprop) || _.isNaN(badprop) || (badprop === UNSET_VALUE); } function displayDate(timestamp) { try { const millis = timestamp?.valueOf?.(); if (_.isFinite(millis) && (millis >= TS_EARLIEST) && (millis <= TS_FARTHEST)) { // we don't tolerate small timestamps return display(DateTime.fromMillis(millis).toISO(), { naked: true }); } return '(~BOGUS DATE ' + display(timestamp) + '~)'; } catch (err2) { console.error('error while handling error'); return String(timestamp); } } /* eslint-disable no-param-reassign */ export function repairError(err, subj, checker, story = {}) { // console.warn('repairError', err, subj, checker, story) // repairIssues(err.issues, subj, checker, story); // all issues now have { message: { msg, badprop, val } } const [rawUnknownKeyIssues, mostIssues] = _.partition(err.issues, ({ code }) => (code === ZIC.unrecognized_keys)); // const rawPathed = _.groupBy(mostIssues, ({ path }) => (_.isEmpty(path) ? 'it' : dotpathFor(path))); const pathed = _.mapValues(rawPathed, (issues) => assemblePathedIssue(issues[0]?.path ?? [], issues, err)); // const _unknown = assembleUnknownKeyIssue(rawUnknownKeyIssues); if (_unknown) { pathed._unknown = _unknown; } // const message = _.map(pathed, ({ message: msg, path, propmsg }) => (`${dotpathFor(path)} ${smush(' ', propmsg, msg)}`.trim())).join(';; '); // setNormalProp(err, 'message', smush(' issues: ', checker.description, message)); // err.messages = _.mapValues(pathed, 'message'); err.badprops = _.mapValues(pathed, 'badprop'); err.success = false; err.ok = false; err.extensions = { ...err.extensions, ...story, subj, badprops: err.badprops, messages: err.messages }; decorate(err, { checker, issues: err.issues, addIssue: err.addIssue, addIssues: err.addIssues, errors: undefined }); decorate(err.extensions, { pathed, checker, issues: err.issues, ok: false }); return err; } function badpropTree(issues) { const subtrees = _.map(issues, ({ path, badprop }) => (_.isEmpty(path) ? badprop : { [dotpathFor(path)]: badprop })); return _.merge({}, ...subtrees); } function assembleUnknownKeyIssue(ekIssues) { if (_.isEmpty(ekIssues)) { return null; } if (ekIssues.length === 1 && ekIssues[0]?.keys?.length === 1) { return { ...ekIssues[0], message: `unknown property ${ekIssues[0].message}` }; } const dotkeys = _.flatMap(ekIssues, ({ keys, path }) => (_.map(keys, (key) => [...path, key].join('.')))); const badprop = badpropTree(ekIssues); const badpropStr = joinBadprops(_.map(ekIssues, 'message')); const issue = { keys: dotkeys, path: [], code: ZIC.unrecognized_keys, message: 'unknown properties ' + badpropStr, badprop, badpropStr, }; return issue; } export function assemblePathedIssue(path, issues, _subj) { // if the item is missing, report that simply and move on -- I don't need to know that an empty string flunked a regex const shegone = _.find(issues, ({ code, badprop }) => (PRESENCE_REQD.has(code) && isVacant(badprop))); if (shegone) { const { badprop, message, badpropStr } = shegone; return { badprop, badpropStr, propmsg: badpropStr, message, path, issues: [shegone], code: shegone.code }; } // const exemplar = issues[0]; const { badprop, message, badpropStr } = exemplar; const code = exemplar.code; // // if there is only one issue, the path - badpropStr - message dance is easy if (issues.length === 1) { const propmsg = SKIP_VALPART.has(code) ? '' : badpropStr; // const subIssue: ZodSubIssue = { ...exemplar, message, code } return { propmsg, message, badprop, badpropStr, path, issues: [exemplar], code }; } // with many issues, roll them up under one path - badpropStr const assembledMessage = joinMessages(_.map(issues, 'message')); return { code, path, propmsg: badpropStr, badprop, badpropStr, issues, message: assembledMessage, }; } // When a custom message is provided, the customErrordMap may not be called, // so we have to try to infer the value and fix the message. export function repairIssues(issues, subj, checker, story) { // const issues = _.map(this.issues, (issue) => forceProtoIssue(issue, subj)) _.each(issues, (issue) => { const opts = { checker, step: 'repair', story }; if ('unionErrors' in issue) { adorn(issue, 'unionErrors', issue.unionErrors); } const badprop = getBadprop(issue, subj, opts); issue.badprop = (badprop === UNSET_VALUE) ? undefined : badprop; issue.badpropStr = (badprop === UNSET_VALUE) ? (issue.code === 'custom' ? '(unset)' : '') : display(badprop); issue.message = customMessage(issue, subj, issue.message, opts); if (issue.code === ZIC.invalid_enum_value && _.isArray(issue.options)) { issue.options = hardcapList(issue.options, { max: 5 }); // up to five options, or first three, '…', and last one } }); } /* eslint-enable no-param-reassign */ // function _repairError<RT, ZE extends TKZodError<RT>>(this: ZE, subj: any, checker: Zchecker, story: Story = {}): ZE { // return repairError(this, subj, checker, story) // } // (ZLib.ZodError as any).prototype._repair = _repairError // ZLib.setErrorMap(customErrorMap as any) //# sourceMappingURL=ZodReporting.js.map