Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/narro/src/build/build.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -30,17 +31,24 @@ export function buildSchema<TOutput>(
function safeParse(input: unknown): SchemaReport<TOutput> {
let sourceReport: SchemaReport<TOutput>

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<symbol>()
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))
},
},
}
}
Expand All @@ -59,7 +67,8 @@ export function buildSchema<TOutput>(
(acc as SchemaReportFailure).metaData.failedIds = new Set<symbol>()
}
(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
Expand Down
20 changes: 14 additions & 6 deletions packages/narro/src/build/objectBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -83,6 +84,7 @@ export function buildObjectSchema<TOutput extends object>(
metaData: {
score: 0,
failedIds,
getErrorMessages: () => [sourceCheckable['~e'](input)],
},
}
}
Expand Down Expand Up @@ -145,7 +147,6 @@ export function buildObjectSchema<TOutput extends object>(
delete sourceReport.data
}

// ---- PUT INTO OWN SHAPE FUNCTION ----
else {
// if it passed, we apply the shape transform
switch (shapeTransform) {
Expand All @@ -170,25 +171,29 @@ export function buildObjectSchema<TOutput extends object>(
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<symbol>()
}
// 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
}
}
}

// ---- PUT INTO OWN SHAPE FUNCTION END ----

return mergeOptionality(input, sourceReport, optionalityBranchCheckable)
}

Expand Down Expand Up @@ -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<symbol>(failedIds),
getErrorMessages: createBranchErrorMethod(failedIds, (source)[key as keyof typeof source], { keyPresent: key in source }),
}

if (candidate.metaData.passedIds && candidate.metaData.passedIds.size > 0) {
Expand Down
25 changes: 0 additions & 25 deletions packages/narro/src/build/unionBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,28 +95,3 @@ export function buildUnionSchema<TOutput>(
},
}
}

// 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.
28 changes: 28 additions & 0 deletions packages/narro/src/helpers/createBranchErrorMethod.ts
Original file line number Diff line number Diff line change
@@ -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<symbol, ErrorFactory> = {
[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))
}
16 changes: 16 additions & 0 deletions packages/narro/src/helpers/createErrorFactory.ts
Original file line number Diff line number Diff line change
@@ -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)}`
}
17 changes: 17 additions & 0 deletions packages/narro/src/helpers/stringifyIfNeeded.ts
Original file line number Diff line number Diff line change
@@ -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}"`
}
11 changes: 11 additions & 0 deletions packages/narro/src/helpers/typeOf.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/narro/src/schemas/_shared/length.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function createLengthCheckable<TInput extends string | any[]>(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}`,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/narro/src/schemas/_shared/maxLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function createMaxLengthCheckable<TInput extends string | any[]>(maxLengt
return {
'~id': maxLengthSymbol,
'~c': (v: TInput) => v.length <= maxLength,
'~e': (v: unknown) => `Expected length <= ${maxLength} but received length ${(v as string | any[]).length}`,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/narro/src/schemas/_shared/minLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function createMinLengthCheckable<TInput extends string | any[]>(minLengt
return {
'~id': minLengthSymbol,
'~c': (v: TInput) => v.length >= minLength,
'~e': (v: unknown) => `Expected length >= ${minLength} but received length ${(v as string | any[]).length}`,
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/narro/src/schemas/_shared/optionality/defaulted.ts
Original file line number Diff line number Diff line change
@@ -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<TOutput>(d: DefaultInput<TOutput>): BranchCheckable<TOutput> {
return {
'~id': defaultedSymbol,
Expand All @@ -28,6 +32,7 @@ export function createDefaultedCheckable<TOutput>(d: DefaultInput<TOutput>): Bra
metaData: {
failedIds: new Set([defaultedSymbol]),
score: 0,
getErrorMessages: () => [defaultedErrorFactory(v)],
},
} satisfies SchemaReportFailure
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<undefined> = {
// is the same as undefinable (unless for object properties which has its own logic)
// own logic for object needed!!
Expand All @@ -15,6 +21,7 @@ export const exactOptionalCheckable: BranchCheckable<undefined> = {
metaData: {
passedIds: new Set([exactOptionalSymbol]),
score: 1,

},
}
}
Expand All @@ -24,6 +31,7 @@ export const exactOptionalCheckable: BranchCheckable<undefined> = {
metaData: {
failedIds: new Set([exactOptionalSymbol]),
score: 0,
getErrorMessages: () => [exactOptionalErrorFactory(v)],
},
}
},
Expand Down
5 changes: 5 additions & 0 deletions packages/narro/src/schemas/_shared/optionality/nullable.ts
Original file line number Diff line number Diff line change
@@ -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<null> = {
'~id': nullableSymbol,
'~c': (v) => {
Expand All @@ -24,6 +28,7 @@ export const nullableCheckable: BranchCheckable<null> = {
metaData: {
failedIds: new Set([nullableSymbol]),
score: 0,
getErrorMessages: () => [nullableErrorFactory(v)],
},
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/narro/src/schemas/_shared/optionality/nullish.ts
Original file line number Diff line number Diff line change
@@ -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<null | undefined> = {
'~id': nullishSymbol,
'~c': (v) => {
Expand All @@ -24,6 +31,7 @@ export const nullishCheckable: BranchCheckable<null | undefined> = {
metaData: {
failedIds: new Set([nullishSymbol]),
score: 0,
getErrorMessages: () => [nullishErrorFactory(v)],
},
}
},
Expand Down
5 changes: 5 additions & 0 deletions packages/narro/src/schemas/_shared/optionality/optional.ts
Original file line number Diff line number Diff line change
@@ -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<undefined> = {
'~id': optionalSymbol,
'~c': (v) => {
Expand All @@ -24,6 +28,7 @@ export const optionalCheckable: BranchCheckable<undefined> = {
metaData: {
failedIds: new Set([optionalSymbol]),
score: 0,
getErrorMessages: () => [optionalErrorFactory(v)],
},
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/narro/src/schemas/_shared/optionality/undefinable.ts
Original file line number Diff line number Diff line change
@@ -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<undefined> = {
'~id': undefinableSymbol,
'~c': (v) => {
Expand All @@ -24,6 +31,7 @@ export const undefinableCheckable: BranchCheckable<undefined> = {
metaData: {
failedIds: new Set([undefinableSymbol]),
score: 0,
getErrorMessages: () => [undefinableErrorFactory(v)],
},
}
},
Expand Down
2 changes: 2 additions & 0 deletions packages/narro/src/schemas/array/array.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { SourceCheckable } from '../../types/schema'
import { createErrorFactory } from '../../helpers/createErrorFactory'

export const arraySymbol = Symbol('array')

export const arrayCheckable: SourceCheckable<unknown[]> = {
'~id': arraySymbol,
'~c': (value): value is unknown[] => Array.isArray(value),
'~e': createErrorFactory('array'),
}

export default arrayCheckable
Loading
Loading