From 68a41444997c4e475244a3f5646615e9d9c512ed Mon Sep 17 00:00:00 2001 From: schplitt Date: Mon, 10 Nov 2025 21:54:22 +0100 Subject: [PATCH 1/5] fix(string): add error message parameter to string function overloads --- packages/spur/src/leitplanken/string/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/spur/src/leitplanken/string/index.ts b/packages/spur/src/leitplanken/string/index.ts index a643fb4..2e80d07 100644 --- a/packages/spur/src/leitplanken/string/index.ts +++ b/packages/spur/src/leitplanken/string/index.ts @@ -19,9 +19,9 @@ export interface StringSchema. -export function string(): StringSchema -export function string(): StringSchema -export function string(): StringSchema { +export function string(errorMessage: string): StringSchema +export function string(errorMessage?: string): StringSchema +export function string(_errorMessage: string = 'Test error'): StringSchema { let optionalityBranchCheckableImport: BranchCheckableImport | undefined // eslint-disable-next-line ts/explicit-function-return-type From 8fb6b59093ba80a5b35278c83d6fa8b0d743b590 Mon Sep 17 00:00:00 2001 From: schplitt Date: Mon, 10 Nov 2025 21:56:27 +0100 Subject: [PATCH 2/5] make optional --- packages/spur/src/leitplanken/string/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spur/src/leitplanken/string/index.ts b/packages/spur/src/leitplanken/string/index.ts index 2e80d07..4bab0b7 100644 --- a/packages/spur/src/leitplanken/string/index.ts +++ b/packages/spur/src/leitplanken/string/index.ts @@ -19,7 +19,7 @@ export interface StringSchema. -export function string(errorMessage: string): StringSchema +export function string(errorMessage?: string): StringSchema export function string(errorMessage?: string): StringSchema export function string(_errorMessage: string = 'Test error'): StringSchema { let optionalityBranchCheckableImport: BranchCheckableImport | undefined From 8794d34fd5caaa94906745c386a8774da269dcb1 Mon Sep 17 00:00:00 2001 From: schplitt Date: Wed, 26 Nov 2025 23:04:38 +0100 Subject: [PATCH 3/5] chore: utils for error messages --- .../narro/src/helpers/createErrorFactory.ts | 16 +++++++++++ .../narro/src/helpers/stringifyIfNeeded.ts | 17 ++++++++++++ packages/narro/src/helpers/typeOf.ts | 11 ++++++++ packages/narro/src/types/helpers.ts | 5 ++++ packages/narro/src/types/report.ts | 5 ++-- packages/narro/src/types/schema.ts | 4 ++- packages/narro/src/utils/message.ts | 27 +++++++++++++++++++ 7 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 packages/narro/src/helpers/createErrorFactory.ts create mode 100644 packages/narro/src/helpers/stringifyIfNeeded.ts create mode 100644 packages/narro/src/helpers/typeOf.ts create mode 100644 packages/narro/src/types/helpers.ts create mode 100644 packages/narro/src/utils/message.ts diff --git a/packages/narro/src/helpers/createErrorFactory.ts b/packages/narro/src/helpers/createErrorFactory.ts new file mode 100644 index 0000000..ec46948 --- /dev/null +++ b/packages/narro/src/helpers/createErrorFactory.ts @@ -0,0 +1,16 @@ +import type { ErrorFactory } from '../types/helpers' +import type { ValueTypes } from './typeOf' +import { stringifyIfNeeded } from './stringifyIfNeeded' +import { typeOf } from './typeOf' + +export function createErrorFactory(expected: ValueTypes | { + value: string | number | boolean | null | undefined | (string | number | boolean | null | undefined)[] +}): ErrorFactory { + if (typeof expected === 'string') { + return (value: unknown) => `Expected type ${expected} but received type ${typeOf(value)}` + } + if (Array.isArray(expected.value)) { + return (value: unknown) => `Expected one of [${(expected.value as (string | number | boolean | null | undefined)[]).map(v => stringifyIfNeeded(v)).join(', ')}] but received ${stringifyIfNeeded(value)}` + } + return (value: unknown) => `Expected value ${stringifyIfNeeded(expected.value)} but received ${stringifyIfNeeded(value)}` +} diff --git a/packages/narro/src/helpers/stringifyIfNeeded.ts b/packages/narro/src/helpers/stringifyIfNeeded.ts new file mode 100644 index 0000000..ecc9587 --- /dev/null +++ b/packages/narro/src/helpers/stringifyIfNeeded.ts @@ -0,0 +1,17 @@ +import { typeOf } from './typeOf' + +export function stringifyIfNeeded(value: unknown): string { + const t = typeOf(value) + const needsStringification = t === 'object' || t === 'array' || t === 'function' || t === 'symbol' + if (needsStringification) { + return JSON.stringify(value) + } + if (t === 'string') { + return `"${value}"` + } + return String(value) +} + +export function formatString(value: string): string { + return `"${value}"` +} diff --git a/packages/narro/src/helpers/typeOf.ts b/packages/narro/src/helpers/typeOf.ts new file mode 100644 index 0000000..a1b05f0 --- /dev/null +++ b/packages/narro/src/helpers/typeOf.ts @@ -0,0 +1,11 @@ +export type ValueTypes = 'string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'object' | 'function' | 'null' | 'array' + +export function typeOf(value: unknown): ValueTypes { + if (value === null) { + return 'null' + } + if (Array.isArray(value)) { + return 'array' + } + return typeof value as ValueTypes +} diff --git a/packages/narro/src/types/helpers.ts b/packages/narro/src/types/helpers.ts new file mode 100644 index 0000000..cdac905 --- /dev/null +++ b/packages/narro/src/types/helpers.ts @@ -0,0 +1,5 @@ +export interface ParentObjectInformation { + keyPresent: boolean +} + +export type ErrorFactory = (value: unknown, info?: ParentObjectInformation) => string diff --git a/packages/narro/src/types/report.ts b/packages/narro/src/types/report.ts index 691a660..1e5e0bf 100644 --- a/packages/narro/src/types/report.ts +++ b/packages/narro/src/types/report.ts @@ -5,7 +5,7 @@ export type SchemaReport = SchemaReportSuccess | SchemaReportFailure */ export interface ObjectPropertyPath { pathType: 'objectProperty' - key: string | symbol | number + key: string } /** @@ -71,6 +71,7 @@ export interface SchemaReportFailure { childReports?: SchemaReport[] path?: SchemaPath - } + getErrorMessages: () => string[] + } } diff --git a/packages/narro/src/types/schema.ts b/packages/narro/src/types/schema.ts index c463731..00e8047 100644 --- a/packages/narro/src/types/schema.ts +++ b/packages/narro/src/types/schema.ts @@ -1,4 +1,5 @@ import type { CommonOptions, DefaultCommonOptions } from '../options/options' +import type { ErrorFactory } from './helpers' import type { SchemaReport } from './report' import type { Prettify } from './utils' @@ -19,11 +20,12 @@ export interface Checkable { * @returns A report indicating whether the input passed the check */ '~c': Check - + '~e': ErrorFactory } export interface SourceCheckable { '~id': symbol '~c': SourceCheck + '~e': ErrorFactory } export interface BranchCheckable { diff --git a/packages/narro/src/utils/message.ts b/packages/narro/src/utils/message.ts new file mode 100644 index 0000000..4bb75ea --- /dev/null +++ b/packages/narro/src/utils/message.ts @@ -0,0 +1,27 @@ +import type { SchemaReport, SchemaReportFailure } from '../types/report' + +/** + * Prettify a schema report into a human-readable string + * @param report The schema report to prettify + * @returns A human-readable string representation of the report + */ +export function prettifyReport(report: SchemaReportFailure): string { + throw new Error('Not implemented yet') + return '' +} +// here we define how we would like to show the report in a pretty way. +// via the scores we KNOW what failed and what passed, where it failed and where it passed +// also what the most likely cause of the failure is + +// we need a way to get the corresponding error message FROM everything that went wrong. +// however this is not so easy in an object as the error message for when an exactOptional fails it is that the key had the value undefined and not missing +// but this is checked on another level +// so we cannot just add a function to the chackable that returns the error message for a given value +// though we could just say in the error message that either OR went wrong but that doesnt really make sense + +// BUT we could create a generic input parameter for said function that has 1 the actual value AND a 2nd object +// that object has if it comes from an object and if the key was present or not + +// then we could use that to create better error messages +// we would only just need to collect that information during the checking phase +// but that would not be too hard either From 587e13f35ab96cdeb237242476578f525d0a1701 Mon Sep 17 00:00:00 2001 From: schplitt Date: Wed, 26 Nov 2025 23:05:45 +0100 Subject: [PATCH 4/5] feat: add error messages for various checkable types --- packages/narro/src/schemas/_shared/length.ts | 1 + packages/narro/src/schemas/_shared/maxLength.ts | 1 + packages/narro/src/schemas/_shared/minLength.ts | 1 + packages/narro/src/schemas/array/array.ts | 2 ++ packages/narro/src/schemas/boolean/boolean.ts | 2 ++ packages/narro/src/schemas/enum/enum.ts | 2 ++ packages/narro/src/schemas/literal/literal.ts | 2 ++ packages/narro/src/schemas/null/null.ts | 2 ++ packages/narro/src/schemas/number/max.ts | 1 + packages/narro/src/schemas/number/min.ts | 1 + packages/narro/src/schemas/number/number.ts | 2 ++ packages/narro/src/schemas/object/object.ts | 2 ++ packages/narro/src/schemas/string/endsWith.ts | 6 ++++-- packages/narro/src/schemas/string/startsWith.ts | 2 ++ packages/narro/src/schemas/string/string.ts | 2 ++ packages/narro/src/schemas/undefined/undefined.ts | 2 ++ packages/narro/src/utils/index.ts | 0 17 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 packages/narro/src/utils/index.ts diff --git a/packages/narro/src/schemas/_shared/length.ts b/packages/narro/src/schemas/_shared/length.ts index a53cc99..65412a3 100644 --- a/packages/narro/src/schemas/_shared/length.ts +++ b/packages/narro/src/schemas/_shared/length.ts @@ -6,6 +6,7 @@ export function createLengthCheckable(length: num return { '~id': lengthSymbol, '~c': (v: TInput) => v.length === length, + '~e': (v: unknown) => `Expected length ${length} but received length ${(v as string | any[]).length}`, } } diff --git a/packages/narro/src/schemas/_shared/maxLength.ts b/packages/narro/src/schemas/_shared/maxLength.ts index a8e09e3..6af19dd 100644 --- a/packages/narro/src/schemas/_shared/maxLength.ts +++ b/packages/narro/src/schemas/_shared/maxLength.ts @@ -6,6 +6,7 @@ export function createMaxLengthCheckable(maxLengt return { '~id': maxLengthSymbol, '~c': (v: TInput) => v.length <= maxLength, + '~e': (v: unknown) => `Expected length <= ${maxLength} but received length ${(v as string | any[]).length}`, } } diff --git a/packages/narro/src/schemas/_shared/minLength.ts b/packages/narro/src/schemas/_shared/minLength.ts index a7b166e..44c1940 100644 --- a/packages/narro/src/schemas/_shared/minLength.ts +++ b/packages/narro/src/schemas/_shared/minLength.ts @@ -6,6 +6,7 @@ export function createMinLengthCheckable(minLengt return { '~id': minLengthSymbol, '~c': (v: TInput) => v.length >= minLength, + '~e': (v: unknown) => `Expected length >= ${minLength} but received length ${(v as string | any[]).length}`, } } diff --git a/packages/narro/src/schemas/array/array.ts b/packages/narro/src/schemas/array/array.ts index 11dec17..b5c4388 100644 --- a/packages/narro/src/schemas/array/array.ts +++ b/packages/narro/src/schemas/array/array.ts @@ -1,10 +1,12 @@ import type { SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const arraySymbol = Symbol('array') export const arrayCheckable: SourceCheckable = { '~id': arraySymbol, '~c': (value): value is unknown[] => Array.isArray(value), + '~e': createErrorFactory('array'), } export default arrayCheckable diff --git a/packages/narro/src/schemas/boolean/boolean.ts b/packages/narro/src/schemas/boolean/boolean.ts index de7fa86..9c9fd79 100644 --- a/packages/narro/src/schemas/boolean/boolean.ts +++ b/packages/narro/src/schemas/boolean/boolean.ts @@ -1,4 +1,5 @@ import type { SourceCheck, SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' const booleanSymbol = Symbol('boolean') @@ -7,6 +8,7 @@ const checkBoolean: SourceCheck = (value): value is boolean => typeof v export const booleanCheckable: SourceCheckable = { '~id': booleanSymbol, '~c': checkBoolean, + '~e': createErrorFactory('boolean'), } export default booleanCheckable diff --git a/packages/narro/src/schemas/enum/enum.ts b/packages/narro/src/schemas/enum/enum.ts index f8cc2ea..48d4b9e 100644 --- a/packages/narro/src/schemas/enum/enum.ts +++ b/packages/narro/src/schemas/enum/enum.ts @@ -1,5 +1,6 @@ import type { Enum } from '.' import type { SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const enumSymbol = Symbol('enum') @@ -7,6 +8,7 @@ export function createEnumCheckable(values: TEnum): SourceCh return { '~id': enumSymbol, '~c': (input: unknown): input is TEnum[number] => values.includes(input as any), + '~e': createErrorFactory({ value: values }), } } diff --git a/packages/narro/src/schemas/literal/literal.ts b/packages/narro/src/schemas/literal/literal.ts index 88e15cd..57402cc 100644 --- a/packages/narro/src/schemas/literal/literal.ts +++ b/packages/narro/src/schemas/literal/literal.ts @@ -1,4 +1,5 @@ import type { SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const literalSymbol = Symbol('literal') @@ -6,6 +7,7 @@ export function createLiteralCheckable input === value, + '~e': createErrorFactory({ value }), } } diff --git a/packages/narro/src/schemas/null/null.ts b/packages/narro/src/schemas/null/null.ts index cfd74f4..b4e393f 100644 --- a/packages/narro/src/schemas/null/null.ts +++ b/packages/narro/src/schemas/null/null.ts @@ -1,4 +1,5 @@ import type { SourceCheck, SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const nullSymbol = Symbol('null') @@ -7,6 +8,7 @@ const checkNull: SourceCheck = value => value === null export const nullCheckable: SourceCheckable = { '~id': nullSymbol, '~c': checkNull, + '~e': createErrorFactory('null'), } export default nullCheckable diff --git a/packages/narro/src/schemas/number/max.ts b/packages/narro/src/schemas/number/max.ts index 79bb253..a8e36fa 100644 --- a/packages/narro/src/schemas/number/max.ts +++ b/packages/narro/src/schemas/number/max.ts @@ -6,6 +6,7 @@ export function createMaxCheckable(max: number): Checkable { return { '~id': maxSymbol, '~c': (v: number) => v <= max, + '~e': (v: unknown) => `Expected number <= ${max} but received ${v}`, } } diff --git a/packages/narro/src/schemas/number/min.ts b/packages/narro/src/schemas/number/min.ts index 1135032..57bba0b 100644 --- a/packages/narro/src/schemas/number/min.ts +++ b/packages/narro/src/schemas/number/min.ts @@ -6,6 +6,7 @@ export function createMinCheckable(min: number): Checkable { return { '~id': minSymbol, '~c': (v: number) => v >= min, + '~e': (v: unknown) => `Expected number >= ${min} but received ${v}`, } } diff --git a/packages/narro/src/schemas/number/number.ts b/packages/narro/src/schemas/number/number.ts index c3fa757..a252c75 100644 --- a/packages/narro/src/schemas/number/number.ts +++ b/packages/narro/src/schemas/number/number.ts @@ -1,4 +1,5 @@ import type { SourceCheck, SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const numberSymbol = Symbol('number') @@ -7,6 +8,7 @@ const checkNumber: SourceCheck = (v): v is number => typeof v === 'numbe export const numberCheckable: SourceCheckable = { '~id': numberSymbol, '~c': checkNumber, + '~e': createErrorFactory('number'), } export default numberCheckable diff --git a/packages/narro/src/schemas/object/object.ts b/packages/narro/src/schemas/object/object.ts index 1cf0658..58859b0 100644 --- a/packages/narro/src/schemas/object/object.ts +++ b/packages/narro/src/schemas/object/object.ts @@ -1,10 +1,12 @@ import type { SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const objectSymbol = Symbol('object') export const objectCheckable: SourceCheckable = { '~id': objectSymbol, '~c': (v): v is object => typeof v === 'object' && v !== null && !Array.isArray(v), + '~e': createErrorFactory('object'), } export default objectCheckable diff --git a/packages/narro/src/schemas/string/endsWith.ts b/packages/narro/src/schemas/string/endsWith.ts index af65bec..96f554d 100644 --- a/packages/narro/src/schemas/string/endsWith.ts +++ b/packages/narro/src/schemas/string/endsWith.ts @@ -1,11 +1,13 @@ import type { Checkable } from '../../types/schema' +import { formatString } from '../../helpers/stringifyIfNeeded' export const endsWithSymbol = Symbol('endsWith') -export function createEndsWithCheckable(start: string): Checkable { +export function createEndsWithCheckable(end: string): Checkable { return { '~id': endsWithSymbol, - '~c': (v: string) => v.endsWith(start), + '~c': (v: string) => v.endsWith(end), + '~e': (v: unknown) => `Expected string ending with ${formatString(end)} but received ${formatString(v as string)}`, } } diff --git a/packages/narro/src/schemas/string/startsWith.ts b/packages/narro/src/schemas/string/startsWith.ts index 00942bb..901c2df 100644 --- a/packages/narro/src/schemas/string/startsWith.ts +++ b/packages/narro/src/schemas/string/startsWith.ts @@ -1,4 +1,5 @@ import type { Checkable } from '../../types/schema' +import { formatString, stringifyIfNeeded } from '../../helpers/stringifyIfNeeded' export const startsWithSymbol = Symbol('startsWith') @@ -6,6 +7,7 @@ export function createStartsWithCheckable(start: string): Checkable { return { '~id': startsWithSymbol, '~c': (v: string) => v.startsWith(start), + '~e': (v: unknown) => `Expected string starting with ${formatString(start)} but received ${formatString(v as string)}`, } } diff --git a/packages/narro/src/schemas/string/string.ts b/packages/narro/src/schemas/string/string.ts index c2048d9..e912780 100644 --- a/packages/narro/src/schemas/string/string.ts +++ b/packages/narro/src/schemas/string/string.ts @@ -1,4 +1,5 @@ import type { SourceCheck, SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' const stringSymbol = Symbol('string') @@ -7,6 +8,7 @@ const checkString: SourceCheck = v => typeof v === 'string' export const stringCheckable: SourceCheckable = { '~id': stringSymbol, '~c': checkString, + '~e': createErrorFactory('string'), } export default stringCheckable diff --git a/packages/narro/src/schemas/undefined/undefined.ts b/packages/narro/src/schemas/undefined/undefined.ts index 33cabfb..1f3b2c0 100644 --- a/packages/narro/src/schemas/undefined/undefined.ts +++ b/packages/narro/src/schemas/undefined/undefined.ts @@ -1,4 +1,5 @@ import type { SourceCheck, SourceCheckable } from '../../types/schema' +import { createErrorFactory } from '../../helpers/createErrorFactory' export const undefinedSymbol = Symbol('undefined') @@ -7,6 +8,7 @@ const checkUndefined: SourceCheck = value => typeof value === 'undefi export const undefinedCheckable: SourceCheckable = { '~id': undefinedSymbol, '~c': checkUndefined, + '~e': createErrorFactory('undefined'), } export default undefinedCheckable diff --git a/packages/narro/src/utils/index.ts b/packages/narro/src/utils/index.ts new file mode 100644 index 0000000..e69de29 From d4630c5110d8659a5ab1679870320b266eccd122 Mon Sep 17 00:00:00 2001 From: schplitt Date: Tue, 2 Dec 2025 21:45:38 +0100 Subject: [PATCH 5/5] feat: enhance error handling with detailed messages for optionality checks --- packages/narro/src/build/build.ts | 11 +++++++- packages/narro/src/build/objectBuild.ts | 20 +++++++++---- packages/narro/src/build/unionBuild.ts | 25 ----------------- .../src/helpers/createBranchErrorMethod.ts | 28 +++++++++++++++++++ .../schemas/_shared/optionality/defaulted.ts | 5 ++++ .../_shared/optionality/exactOptional.ts | 8 ++++++ .../schemas/_shared/optionality/nullable.ts | 5 ++++ .../schemas/_shared/optionality/nullish.ts | 8 ++++++ .../schemas/_shared/optionality/optional.ts | 5 ++++ .../_shared/optionality/undefinable.ts | 8 ++++++ .../narro/src/schemas/undefined/undefined.ts | 11 ++++++-- 11 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 packages/narro/src/helpers/createBranchErrorMethod.ts diff --git a/packages/narro/src/build/build.ts b/packages/narro/src/build/build.ts index a1b2712..2b375f7 100644 --- a/packages/narro/src/build/build.ts +++ b/packages/narro/src/build/build.ts @@ -1,3 +1,4 @@ +import type { ErrorFactory } from '../types/helpers' import type { SchemaReport, SchemaReportFailure } from '../types/report' import type { BranchCheckable, BranchCheckableImport, Checkable, CheckableImport, EvaluableSchema, SourceCheckable, SourceCheckableImport } from '../types/schema' import { deduplicateCheckables, mergeOptionality } from './utils' @@ -30,17 +31,24 @@ export function buildSchema( function safeParse(input: unknown): SchemaReport { let sourceReport: SchemaReport + const errorFactories: ErrorFactory[] = [] + const sourceResult = sourceCheckable['~c'](input) // when the source is not of the type we expect, we cannot continue with the child checkables const failedIds = new Set() failedIds.add(sourceCheckable['~id']) if (!sourceResult) { + // add the error factory of the source checkable to the list of error factories + errorFactories.push(sourceCheckable['~e']) // here we build the sourceReport from the checkables sourceReport = { success: false, metaData: { failedIds, score: 0, + getErrorMessages: () => { + return errorFactories.map(ef => ef(input)) + }, }, } } @@ -59,7 +67,8 @@ export function buildSchema( (acc as SchemaReportFailure).metaData.failedIds = new Set() } (acc as SchemaReportFailure).metaData.failedIds.add(checkable['~id']) - + // add the error factory of the failed checkable to the list of error factories + errorFactories.push(checkable['~e']) acc.success = false } return acc diff --git a/packages/narro/src/build/objectBuild.ts b/packages/narro/src/build/objectBuild.ts index 63fafe4..92afc46 100644 --- a/packages/narro/src/build/objectBuild.ts +++ b/packages/narro/src/build/objectBuild.ts @@ -2,6 +2,7 @@ import type { ObjectShape } from '../options/objectOptions' import type { ObjectEntries } from '../schemas/object' import type { SchemaReport, SchemaReportFailure } from '../types/report' import type { BranchCheckable, BranchCheckableImport, EvaluableSchema, SourceCheckable, SourceCheckableImport } from '../types/schema' +import { createBranchErrorMethod } from '../helpers/createBranchErrorMethod' import { exactOptionalSymbol } from '../schemas/_shared/optionality/exactOptional' import { nullishSymbol } from '../schemas/_shared/optionality/nullish' import { undefinableSymbol } from '../schemas/_shared/optionality/undefinable' @@ -83,6 +84,7 @@ export function buildObjectSchema( metaData: { score: 0, failedIds, + getErrorMessages: () => [sourceCheckable['~e'](input)], }, } } @@ -145,7 +147,6 @@ export function buildObjectSchema( delete sourceReport.data } - // ---- PUT INTO OWN SHAPE FUNCTION ---- else { // if it passed, we apply the shape transform switch (shapeTransform) { @@ -170,16 +171,22 @@ export function buildObjectSchema( break } } - if (!hasExtraKeys) + if (!hasExtraKeys) { break + } // we have an extra key, so we fail the report (sourceReport as any as SchemaReportFailure).success = false // we add the source checkable id to the failed ids if (!(sourceReport as any as SchemaReportFailure).metaData.failedIds) { (sourceReport as any as SchemaReportFailure).metaData.failedIds = new Set() } - // TODO?: think about if we want extra IDs for the shapes - (sourceReport as any as SchemaReportFailure).metaData.failedIds.add(sourceCheckable['~id']) + (sourceReport as any as SchemaReportFailure).metaData.score -= 1; + (sourceReport as any as SchemaReportFailure).metaData.getErrorMessages = () => [ + `Strict object has extra keys not defined in schema`, + ] + + // TODO?: think about if we want extra IDs for the shapes as we have no identification later what went wrong here + // we remove the value as it is now invalid delete (sourceReport as any as SchemaReportFailure).data break @@ -187,8 +194,6 @@ export function buildObjectSchema( } } - // ---- PUT INTO OWN SHAPE FUNCTION END ---- - return mergeOptionality(input, sourceReport, optionalityBranchCheckable) } @@ -252,9 +257,12 @@ function validatePropertyCandidates( continue } + // as we check that optionality here we assume that + const failureMeta: SchemaReportFailure['metaData'] = { score: candidate.metaData.score - failedIds.length, failedIds: new Set(failedIds), + getErrorMessages: createBranchErrorMethod(failedIds, (source)[key as keyof typeof source], { keyPresent: key in source }), } if (candidate.metaData.passedIds && candidate.metaData.passedIds.size > 0) { diff --git a/packages/narro/src/build/unionBuild.ts b/packages/narro/src/build/unionBuild.ts index 67a3209..dd0e9e0 100644 --- a/packages/narro/src/build/unionBuild.ts +++ b/packages/narro/src/build/unionBuild.ts @@ -95,28 +95,3 @@ export function buildUnionSchema( }, } } - -// in unions schema it might make sense to consolidate all the union reports and the union reports of the union reports into the top level array -// Are there any issues that could occurre from doing this? -// it could make the trace harder to follow as the union is then not in the schema that originally produced that report -// though those unions dont really add any "real" value anyway as it does not REALLY add value to know that the undefined was added through string().undefinable() -// we could add another part to the metadata afterwards to add through which the expected values were, error message and from which call chain it originated from -// if the above is done, there is nothing "lost" in the trace and we can consolidate all union reports into one array at the top level - -// Implementation plan: -// - implement union report consolidation in the union build function -// - add tests in a new file unionBuild.test.ts to verify that the consolidation works as expected -// - add logic to check if the checkIds in objectBuild fails, there is another union schema to promote (starting with the one with the highest score) and execute the checkIds on that schema -// if it passes, promote that report as the selected one and demote the one that failed - -// no better, to have correctness, we should check the report and all its union reports in the object and change them accordingly -// afterwards we promote the passed one with the highest score or the first one if multiple have the same score add all other reports to its union reports -// so we build an array of all the reports (originally passed one + its union reports) and then check each of them if they pass the checkIds, adjust their score accordingly, delete data if necessary -// afterwards we select the best report again and return that -// if none passed, we return the original report (which is the one with the highest score) - -// Plan summary: -// - unionBuild.ts: consolidate unionReports recursively into a flat array, reselect best report after consolidation, and add a comment for future feature where we ensure metadata continues to track original branches. -// - unionBuild.ts: when a promoted report fails downstream (e.g. object checkIds), iterate promoted + stored union reports to find the highest scoring passing candidate, demote failing ones, and update unionReports accordingly. -// - objectBuild.ts: extend checkIds handling to walk the selected union report set, run checkIds against each candidate, adjust scores, remove invalid data, and bubble updated unionReports back into the chosen result. -// - tests/__tests__/narro: add unionBuild.test.ts covering union report consolidation edge cases and object schema interactions; add targeted object schema tests for optionality/union promotion scenarios. diff --git a/packages/narro/src/helpers/createBranchErrorMethod.ts b/packages/narro/src/helpers/createBranchErrorMethod.ts new file mode 100644 index 0000000..e1b6c23 --- /dev/null +++ b/packages/narro/src/helpers/createBranchErrorMethod.ts @@ -0,0 +1,28 @@ +import type { ErrorFactory, ParentObjectInformation } from '../types/helpers' +import { exactOptionalErrorFactory, exactOptionalSymbol } from '../schemas/_shared/optionality/exactOptional' +import { nullishErrorFactory, nullishSymbol } from '../schemas/_shared/optionality/nullish' +import { undefinableErrorFactory, undefinableSymbol } from '../schemas/_shared/optionality/undefinable' +import { undefinedBranchErrorFactory, undefinedSymbol } from '../schemas/undefined/undefined' + +const idToErrorFactory: Record = { + [exactOptionalSymbol]: exactOptionalErrorFactory, + [nullishSymbol]: nullishErrorFactory, + [undefinableSymbol]: undefinableErrorFactory, + [undefinedSymbol]: undefinedBranchErrorFactory, +} + +export function createBranchErrorMethod( + ids: symbol[], + value: unknown, + info: ParentObjectInformation | undefined, +): () => string[] { + const errorFactories: ErrorFactory[] = [] + for (const id of ids) { + const f = idToErrorFactory[id] + if (f) { + errorFactories.push(f) + } + } + + return () => errorFactories.map(f => f(value, info)) +} diff --git a/packages/narro/src/schemas/_shared/optionality/defaulted.ts b/packages/narro/src/schemas/_shared/optionality/defaulted.ts index f5a3719..7a62ecd 100644 --- a/packages/narro/src/schemas/_shared/optionality/defaulted.ts +++ b/packages/narro/src/schemas/_shared/optionality/defaulted.ts @@ -1,8 +1,12 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { SchemaReportFailure, SchemaReportSuccess } from '../../../types/report' import type { BranchCheckable, DefaultInput } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const defaultedSymbol = Symbol('defaulted') +export const defaultedErrorFactory: ErrorFactory = value => `Expected property to be defaulted (missing, undefined, or null) but received value ${stringifyIfNeeded(value)}` + export function createDefaultedCheckable(d: DefaultInput): BranchCheckable { return { '~id': defaultedSymbol, @@ -28,6 +32,7 @@ export function createDefaultedCheckable(d: DefaultInput): Bra metaData: { failedIds: new Set([defaultedSymbol]), score: 0, + getErrorMessages: () => [defaultedErrorFactory(v)], }, } satisfies SchemaReportFailure }, diff --git a/packages/narro/src/schemas/_shared/optionality/exactOptional.ts b/packages/narro/src/schemas/_shared/optionality/exactOptional.ts index 5fb0c59..019feb9 100644 --- a/packages/narro/src/schemas/_shared/optionality/exactOptional.ts +++ b/packages/narro/src/schemas/_shared/optionality/exactOptional.ts @@ -1,7 +1,13 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { BranchCheckable } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const exactOptionalSymbol = Symbol('exactOptional') +export const exactOptionalErrorFactory: ErrorFactory = (value, info) => { + return `Expected property to be exactly optional (i.e., either absent or undefined) but received value ${stringifyIfNeeded(value)}${info?.keyPresent ? ' with key present' : ''}` +} + export const exactOptionalCheckable: BranchCheckable = { // is the same as undefinable (unless for object properties which has its own logic) // own logic for object needed!! @@ -15,6 +21,7 @@ export const exactOptionalCheckable: BranchCheckable = { metaData: { passedIds: new Set([exactOptionalSymbol]), score: 1, + }, } } @@ -24,6 +31,7 @@ export const exactOptionalCheckable: BranchCheckable = { metaData: { failedIds: new Set([exactOptionalSymbol]), score: 0, + getErrorMessages: () => [exactOptionalErrorFactory(v)], }, } }, diff --git a/packages/narro/src/schemas/_shared/optionality/nullable.ts b/packages/narro/src/schemas/_shared/optionality/nullable.ts index 4abb601..e7f8837 100644 --- a/packages/narro/src/schemas/_shared/optionality/nullable.ts +++ b/packages/narro/src/schemas/_shared/optionality/nullable.ts @@ -1,7 +1,11 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { BranchCheckable } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const nullableSymbol = Symbol('nullable') +export const nullableErrorFactory: ErrorFactory = value => `Expected property to be nullable (explicit null) but received value ${stringifyIfNeeded(value)}` + export const nullableCheckable: BranchCheckable = { '~id': nullableSymbol, '~c': (v) => { @@ -24,6 +28,7 @@ export const nullableCheckable: BranchCheckable = { metaData: { failedIds: new Set([nullableSymbol]), score: 0, + getErrorMessages: () => [nullableErrorFactory(v)], }, } }, diff --git a/packages/narro/src/schemas/_shared/optionality/nullish.ts b/packages/narro/src/schemas/_shared/optionality/nullish.ts index 112769a..4e2d308 100644 --- a/packages/narro/src/schemas/_shared/optionality/nullish.ts +++ b/packages/narro/src/schemas/_shared/optionality/nullish.ts @@ -1,7 +1,14 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { BranchCheckable } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const nullishSymbol = Symbol('nullish') +export const nullishErrorFactory: ErrorFactory = (value, info) => { + const keyInfo = info ? (info.keyPresent ? ' with key present' : ' with key missing') : '' + return `Expected property to be nullish (key present with value null or undefined) but received value ${stringifyIfNeeded(value)}${keyInfo}` +} + export const nullishCheckable: BranchCheckable = { '~id': nullishSymbol, '~c': (v) => { @@ -24,6 +31,7 @@ export const nullishCheckable: BranchCheckable = { metaData: { failedIds: new Set([nullishSymbol]), score: 0, + getErrorMessages: () => [nullishErrorFactory(v)], }, } }, diff --git a/packages/narro/src/schemas/_shared/optionality/optional.ts b/packages/narro/src/schemas/_shared/optionality/optional.ts index e047316..40185e4 100644 --- a/packages/narro/src/schemas/_shared/optionality/optional.ts +++ b/packages/narro/src/schemas/_shared/optionality/optional.ts @@ -1,7 +1,11 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { BranchCheckable } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const optionalSymbol = Symbol('optional') +export const optionalErrorFactory: ErrorFactory = value => `Expected property to be optional (missing or undefined) but received value ${stringifyIfNeeded(value)}` + export const optionalCheckable: BranchCheckable = { '~id': optionalSymbol, '~c': (v) => { @@ -24,6 +28,7 @@ export const optionalCheckable: BranchCheckable = { metaData: { failedIds: new Set([optionalSymbol]), score: 0, + getErrorMessages: () => [optionalErrorFactory(v)], }, } }, diff --git a/packages/narro/src/schemas/_shared/optionality/undefinable.ts b/packages/narro/src/schemas/_shared/optionality/undefinable.ts index 83e0cb4..2ab95ec 100644 --- a/packages/narro/src/schemas/_shared/optionality/undefinable.ts +++ b/packages/narro/src/schemas/_shared/optionality/undefinable.ts @@ -1,7 +1,14 @@ +import type { ErrorFactory } from '../../../types/helpers' import type { BranchCheckable } from '../../../types/schema' +import { stringifyIfNeeded } from '../../../helpers/stringifyIfNeeded' export const undefinableSymbol = Symbol('undefinable') +export const undefinableErrorFactory: ErrorFactory = (value, info) => { + const keyInfo = info ? (info.keyPresent ? ' with key present' : ' with key missing') : '' + return `Expected property to be undefinable (key present with value undefined) but received value ${stringifyIfNeeded(value)}${keyInfo}` +} + export const undefinableCheckable: BranchCheckable = { '~id': undefinableSymbol, '~c': (v) => { @@ -24,6 +31,7 @@ export const undefinableCheckable: BranchCheckable = { metaData: { failedIds: new Set([undefinableSymbol]), score: 0, + getErrorMessages: () => [undefinableErrorFactory(v)], }, } }, diff --git a/packages/narro/src/schemas/undefined/undefined.ts b/packages/narro/src/schemas/undefined/undefined.ts index 1f3b2c0..9bd0a61 100644 --- a/packages/narro/src/schemas/undefined/undefined.ts +++ b/packages/narro/src/schemas/undefined/undefined.ts @@ -1,14 +1,21 @@ +import type { ErrorFactory } from '../../types/helpers' import type { SourceCheck, SourceCheckable } from '../../types/schema' -import { createErrorFactory } from '../../helpers/createErrorFactory' export const undefinedSymbol = Symbol('undefined') const checkUndefined: SourceCheck = value => typeof value === 'undefined' +export const undefinedBranchErrorFactory: ErrorFactory = (_, info) => { + if (info && !info.keyPresent) { + return 'Expected property to be undefined with key present but key is missing' + } + return 'Expected property to be undefined with key present' +} + export const undefinedCheckable: SourceCheckable = { '~id': undefinedSymbol, '~c': checkUndefined, - '~e': createErrorFactory('undefined'), + '~e': undefinedBranchErrorFactory, } export default undefinedCheckable