diff --git a/packages/arktype/test/deep-equal.test.ts b/packages/arktype/test/deep-equal.test.ts index adcf6176..8d0b224e 100644 --- a/packages/arktype/test/deep-equal.test.ts +++ b/packages/arktype/test/deep-equal.test.ts @@ -307,8 +307,8 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/zod❳: ark.deepEqual.writeab const r_k_1_keys = Object.keys(r_k_) const length1 = l_k_1_keys.length if (length1 !== r_k_1_keys.length) return false - for (let ix = 0; ix < length1; ix++) { - const key1 = l_k_1_keys[ix] + for (let ix1 = 0; ix1 < length1; ix1++) { + const key1 = l_k_1_keys[ix1] const l_k___k_ = l_k_[key1] const r_k___k_ = r_k_[key1] if (l_k___k_ !== r_k___k_) return false @@ -341,8 +341,8 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/zod❳: ark.deepEqual.writeab const r_k_1_keys = Object.keys(r_k_) const length1 = l_k_1_keys.length if (length1 !== r_k_1_keys.length) return false - for (let ix = 0; ix < length1; ix++) { - const key1 = l_k_1_keys[ix] + for (let ix1 = 0; ix1 < length1; ix1++) { + const key1 = l_k_1_keys[ix1] const l_k___k_ = l_k_[key1] const r_k___k_ = r_k_[key1] const length2 = l_k___k_.length diff --git a/packages/json-schema-test/src/generator-options.ts b/packages/json-schema-test/src/generator-options.ts index 348c4849..03819ac4 100644 --- a/packages/json-schema-test/src/generator-options.ts +++ b/packages/json-schema-test/src/generator-options.ts @@ -5,55 +5,6 @@ import * as Bounds from './generator-bounds.js' import { byTag } from './generator-seed.js' import { TypeNames } from '@traversable/json-schema-types' -export type ArrayParams = { - minLength?: number - maxLength?: number -} - -export type IntegerParams = { - minimum?: number - maximum?: number - multipleOf?: number -} - -export type NumberParams = { - minimum?: number - maximum?: number - minExcluded?: boolean - maxExcluded?: boolean - multipleOf?: number -} - -export type BigIntParams = { - minimum?: bigint - maximum?: bigint - multipleOf?: bigint -} - -export type StringParams = { - /* prefix?: string, postfix?: string, pattern?: string, substring?: string, length?: number */ - minLength?: number - maxLength?: number -} - -export type Params = { - array?: ArrayParams - boolean?: {} - const?: {} - enum?: {} - integer?: IntegerParams - intersection?: {} - never?: {} - null?: {} - number?: NumberParams - object?: {} - record?: {} - string?: StringParams - tuple?: {} - union?: {} - unknown?: {} -} - export interface Options extends Partial>, Constraints {} export type InferConfigType = S extends Options ? T : never diff --git a/packages/json-schema-types/src/exports.ts b/packages/json-schema-types/src/exports.ts index c847f353..c3747ffb 100644 --- a/packages/json-schema-types/src/exports.ts +++ b/packages/json-schema-types/src/exports.ts @@ -3,6 +3,7 @@ export * from './version.js' export * from './types.js' export * from './utils.js' export * as F from './functor.js' +export type F = import('./types.js').F export type { Algebra, CompilerIndex, Index } from './functor.js' export { CompilerFunctor, Functor, fold, defaultCompilerIndex, defaultIndex } from './functor.js' diff --git a/packages/json-schema-types/src/types.ts b/packages/json-schema-types/src/types.ts index 4c76d96e..2a4f0f16 100644 --- a/packages/json-schema-types/src/types.ts +++ b/packages/json-schema-types/src/types.ts @@ -158,7 +158,6 @@ export type Scalar = export type Nullary = | Never - // | Unknown | Scalar | Enum | Const diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json index 8004db9f..6d5a6116 100644 --- a/packages/json-schema/package.json +++ b/packages/json-schema/package.json @@ -44,12 +44,13 @@ "test": "vitest" }, "dependencies": { - "@prettier/sync": "catalog:", "@traversable/json-schema-types": "workspace:^", "@traversable/registry": "workspace:^" }, "devDependencies": { "@jsonjoy.com/util": "^1.6.0", + "@prettier/sync": "catalog:", + "@sinclair/typebox": "catalog:", "@traversable/json-schema-test": "workspace:^", "@types/lodash.clonedeep": "^4.5.9", "lodash.clonedeep": "^4.5.0", diff --git a/packages/json-schema/src/__generated__/__manifest__.ts b/packages/json-schema/src/__generated__/__manifest__.ts index b4036704..889ce143 100644 --- a/packages/json-schema/src/__generated__/__manifest__.ts +++ b/packages/json-schema/src/__generated__/__manifest__.ts @@ -40,12 +40,13 @@ export default { "test": "vitest" }, "dependencies": { - "@prettier/sync": "catalog:", "@traversable/json-schema-types": "workspace:^", "@traversable/registry": "workspace:^" }, "devDependencies": { "@jsonjoy.com/util": "^1.6.0", + "@prettier/sync": "catalog:", + "@sinclair/typebox": "catalog:", "@traversable/json-schema-test": "workspace:^", "@types/lodash.clonedeep": "^4.5.9", "lodash.clonedeep": "^4.5.0", diff --git a/packages/json-schema/src/deep-equal.ts b/packages/json-schema/src/deep-equal.ts index f316d311..cf776b61 100644 --- a/packages/json-schema/src/deep-equal.ts +++ b/packages/json-schema/src/deep-equal.ts @@ -17,6 +17,7 @@ import { deepEqualInlinePrimitiveCheck as inlinePrimitiveCheck, deepEqualIsPrimitive as isPrimitive, deepEqualSchemaOrdering as schemaOrdering, + Invariant, } from '@traversable/json-schema-types' export interface Scope extends JsonSchema.Index { @@ -86,7 +87,7 @@ function StrictlyEqualOrFail(l: (string | number)[], r: (string | number)[], ix: return `if (${X} !== ${Y}) return false;` } -function enumEquals(x: JsonSchema.Enum): Builder { +function enumDeepEqual(x: JsonSchema.Enum): Builder { return function continueEnumEquals(LEFT, RIGHT, IX) { return ( x.enum.every((v) => typeof v === 'number') ? SameNumberOrFail(LEFT, RIGHT, IX) @@ -96,7 +97,7 @@ function enumEquals(x: JsonSchema.Enum): Builder { } } -function arrayEquals(x: JsonSchema.Array): Builder { +function arrayDeepEqual(x: JsonSchema.Array): Builder { return function continueArrayEquals(LEFT_PATH, RIGHT_PATH, IX) { const LEFT = joinPath(LEFT_PATH, IX.isOptional) const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) @@ -116,9 +117,10 @@ function arrayEquals(x: JsonSchema.Array): Builder { } } -function recordEquals(x: JsonSchema.Record): Builder { +function recordDeepEqual(x: JsonSchema.Record): Builder { return function continueRecordEquals(LEFT_PATH, RIGHT_PATH, IX) { const LENGTH = ident('length', IX.bindings) + const IX_IDENT = ident('ix', IX.bindings) const KEY_IDENT = ident('key', IX.bindings) const LEFT = joinPath(LEFT_PATH, IX.isOptional) const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) @@ -136,8 +138,8 @@ function recordEquals(x: JsonSchema.Record): Builder { ].join('\n')).join('\n') const FOR_LOOP = [ - `for (let ix = 0; ix < ${LENGTH}; ix++) {`, - `const ${KEY_IDENT} = ${LEFT_KEYS_IDENT}[ix];`, + `for (let ${IX_IDENT} = 0; ${IX_IDENT} < ${LENGTH}; ${IX_IDENT}++) {`, + `const ${KEY_IDENT} = ${LEFT_KEYS_IDENT}[${IX_IDENT}];`, `const ${LEFT_VALUE_IDENT} = ${LEFT}[${KEY_IDENT}];`, `const ${RIGHT_VALUE_IDENT} = ${RIGHT}[${KEY_IDENT}];`, !x.patternProperties ? null : PATTERN_PROPERTIES, @@ -155,7 +157,7 @@ function recordEquals(x: JsonSchema.Record): Builder { } } -function unionEquals( +function unionDeepEqual( x: JsonSchema.Union, input: JsonSchema.Union ): Builder { @@ -165,17 +167,17 @@ function unionEquals( return x.anyOf[0] } else { if (!areAllObjects(input.anyOf)) { - return nonDisjunctiveEquals(x, input) + return inclusiveUnionDeepEqual(x, input) } else { const withTags = getTags(input.anyOf) return withTags === null - ? nonDisjunctiveEquals(x, input) - : disjunctiveEquals(x, withTags) + ? inclusiveUnionDeepEqual(x, input) + : exclusiveUnionDeepEqual(x, withTags) } } } -function nonDisjunctiveEquals( +function inclusiveUnionDeepEqual( x: JsonSchema.Union, input: JsonSchema.Union ): Builder { @@ -219,7 +221,7 @@ function nonDisjunctiveEquals( } } -function disjunctiveEquals( +function exclusiveUnionDeepEqual( x: JsonSchema.Union, [discriminant, TAGGED]: Discriminated ): Builder { @@ -245,7 +247,7 @@ function disjunctiveEquals( } } -function intersectionEquals(x: JsonSchema.Intersection): Builder { +function intersectionDeepEqual(x: JsonSchema.Intersection): Builder { return function continueIntersectionEquals(LEFT_PATH, RIGHT_PATH, IX) { const LEFT = joinPath(LEFT_PATH, IX.isOptional) const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) @@ -253,11 +255,13 @@ function intersectionEquals(x: JsonSchema.Intersection): Builder { } } -function tupleEquals( +function tupleDeepEqual( x: JsonSchema.Tuple, input: JsonSchema.Tuple ): Builder { - return function continueTupleEquals(LEFT_PATH, RIGHT_PATH, IX) { + if (!JsonSchema.isTuple(input)) { + return Invariant.IllegalState('deepEqual', 'expected input to be a tuple schema', input) + } else return function continueTupleEquals(LEFT_PATH, RIGHT_PATH, IX) { const LEFT = joinPath(LEFT_PATH, false) // `false` because `*_PATH` already takes optionality into account const RIGHT = joinPath(RIGHT_PATH, false) // `false` because `*_PATH` already takes optionality into account // if we got `{ prefixItems: [] }`, just check that the lengths are the same @@ -319,7 +323,7 @@ function optionalEquals( } } -function objectEquals(x: JsonSchema.Object, input: JsonSchema.Object): Builder { +function objectDeepEqual(x: JsonSchema.Object, input: JsonSchema.Object): Builder { return function continueObjectEquals(LEFT_PATH, RIGHT_PATH, IX) { const LEFT = joinPath(LEFT_PATH, false) // `false` because `*_PATH` already takes optionality into account const RIGHT = joinPath(RIGHT_PATH, false) // `false` because `*_PATH` already takes optionality into account @@ -359,13 +363,13 @@ function objectEquals(x: JsonSchema.Object, input: JsonSchema.Object((x, _, input) => { switch (true) { default: return (void (x satisfies never), SameValueOrFail) - case x == null: return function continueJsonNullEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } - case typeof x === 'number': return function continueJsonNumberEquals(l, r, ix) { return SameNumberOrFail(l, r, ix) } - case typeof x === 'string': return function continueJsonStringEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } - case typeof x === 'boolean': return function continueJsonBooleanEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case x == null: return function constNullDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case typeof x === 'number': return function constNumberDeepEqual(l, r, ix) { return SameNumberOrFail(l, r, ix) } + case typeof x === 'string': return function constStringDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case typeof x === 'boolean': return function constBooleanDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } case Json.isArray(x): { if (!Json.isArray(input)) throw Error('illegal state') - return function continueJsonArrayEquals(LEFT_PATH, RIGHT_PATH, IX): string { + return function constArrayDeepEqual(LEFT_PATH, RIGHT_PATH, IX): string { const LEFT = joinPath(LEFT_PATH, IX.isOptional) // `false` because `*_PATH` already takes optionality into account const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) // `false` because `*_PATH` already takes optionality into account const LENGTH = ident('length', IX.bindings) @@ -394,7 +398,7 @@ const foldJson = Json.fold((x, _, input) => { } case Json.isObject(x): { if (!Json.isObject(input)) throw Error('illegal state') - return function continueJsonObjectEquals(LEFT_PATH, RIGHT_PATH, IX): string { + return function constObjectDeepEqual(LEFT_PATH, RIGHT_PATH, IX): string { const LEFT = joinPath(LEFT_PATH, IX.isOptional) // `false` because `*_PATH` already takes optionality into account const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) // `false` because `*_PATH` already takes optionality into account @@ -427,20 +431,20 @@ const fold = JsonSchema.fold((x, _, input) => { switch (true) { default: return (void (x satisfies never), SameValueOrFail) case JsonSchema.isConst(x): return foldJson(x.const as Json.Unary) - case JsonSchema.isNever(x): return function continueNeverEquals(l, r, ix) { return SameValueOrFail(l, r, ix) } - case JsonSchema.isNull(x): return function continueNullEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } - case JsonSchema.isBoolean(x): return function continueBooleanEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } - case JsonSchema.isInteger(x): return function continueIntegerEquals(l, r, ix) { return SameNumberOrFail(l, r, ix) } - case JsonSchema.isNumber(x): return function continueNumberEquals(l, r, ix) { return SameNumberOrFail(l, r, ix) } - case JsonSchema.isString(x): return function continueStringEquals(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } - case JsonSchema.isEnum(x): return enumEquals(x) - case JsonSchema.isArray(x): return arrayEquals(x) - case JsonSchema.isRecord(x): return recordEquals(x) - case JsonSchema.isIntersection(x): return intersectionEquals(x) - case JsonSchema.isTuple(x): return tupleEquals(x, input as JsonSchema.Tuple) - case JsonSchema.isUnion(x): return unionEquals(x, input as JsonSchema.Union) - case JsonSchema.isObject(x): return objectEquals(x, input as JsonSchema.Object) - case JsonSchema.isUnknown(x): return function continueUnknownEquals(l, r, ix) { return SameValueOrFail(l, r, ix) } + case JsonSchema.isNever(x): return function neverDeepEqual(l, r, ix) { return SameValueOrFail(l, r, ix) } + case JsonSchema.isNull(x): return function nullDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case JsonSchema.isBoolean(x): return function booleanDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case JsonSchema.isInteger(x): return function integerDeepEqual(l, r, ix) { return SameNumberOrFail(l, r, ix) } + case JsonSchema.isNumber(x): return function numberDeepEqual(l, r, ix) { return SameNumberOrFail(l, r, ix) } + case JsonSchema.isString(x): return function stringDeepEqual(l, r, ix) { return StrictlyEqualOrFail(l, r, ix) } + case JsonSchema.isEnum(x): return enumDeepEqual(x) + case JsonSchema.isArray(x): return arrayDeepEqual(x) + case JsonSchema.isRecord(x): return recordDeepEqual(x) + case JsonSchema.isIntersection(x): return intersectionDeepEqual(x) + case JsonSchema.isTuple(x): return tupleDeepEqual(x, input as JsonSchema.Tuple) + case JsonSchema.isUnion(x): return unionDeepEqual(x, input as JsonSchema.Union) + case JsonSchema.isObject(x): return objectDeepEqual(x, input as JsonSchema.Object) + case JsonSchema.isUnknown(x): return function unknownDeepEqual(l, r, ix) { return SameValueOrFail(l, r, ix) } } }) @@ -573,7 +577,7 @@ function deepEqual_writeable(schema: JsonSchema, options?: deepEqual.Options): s const FUNCTION_NAME = options?.functionName ?? 'deepEqual' const inputType = toType(schema, options) const TYPE = options?.typeName ?? inputType - const ROOT_CHECK = requiresObjectIs(schema) ? `if (Object.is(l, r)) return true` : `if (l === r) return true` + const ROOT_EQUAL = requiresObjectIs(schema) ? `if (Object.is(l, r)) return true` : `if (l === r) return true` const BODY = compiled.length === 0 ? null : compiled return ( JsonSchema.isNullary(schema) @@ -587,7 +591,7 @@ function deepEqual_writeable(schema: JsonSchema, options?: deepEqual.Options): s : [ options?.typeName === undefined ? null : inputType, `function ${FUNCTION_NAME} (l: ${TYPE}, r: ${TYPE}) {`, - ROOT_CHECK, + ROOT_EQUAL, BODY, `return true;`, `}` diff --git a/packages/json-schema/src/diff.ts b/packages/json-schema/src/diff.ts new file mode 100644 index 00000000..18c874f5 --- /dev/null +++ b/packages/json-schema/src/diff.ts @@ -0,0 +1,546 @@ +import { Json } from '@traversable/json' +import { + Equal, + escape, + ident, + joinPath, + Object_keys, + Object_entries, + stringifyLiteral, +} from '@traversable/registry' +import type { Discriminated, TypeName } from '@traversable/json-schema-types' +import { + check, + JsonSchema, + toType, + areAllObjects, + getTags, + deepEqualInlinePrimitiveCheck as inlinePrimitiveCheck, + deepEqualIsPrimitive as isPrimitive, + deepEqualSchemaOrdering as schemaOrdering, + Invariant, +} from '@traversable/json-schema-types' + +interface Add { op: 'add', path: string, value: unknown } +interface Replace { op: 'replace', path: string, value: unknown } +interface Remove { op: 'remove', path: string } + +type Edit = Add | Replace | Remove +type Diff = (x: T, y: T) => Edit[] + +export type Path = (string | number)[] + +export interface Scope extends JsonSchema.Index { + bindings: Map + path: (string | number)[] +} + +export type Builder = (left: Path, right: Path, index: Scope) => string + +const diff_unfuzzable = [ + 'never', + 'union', + 'unknown', +] satisfies TypeName[] + +const defaultIndex = () => ({ + ...JsonSchema.defaultIndex, + bindings: new Map(), + path: [], +}) satisfies Scope + +function jsonPointer(path: (string | number)[]): string { + if (path.length === 0) return `""` + else if (path.length === 1 && path[0] === "") return `"/"` + else { + const [head, ...tail] = path + let out = [ + ...head === "" ? [head] : ["", head], + ...tail + ].map((s) => { + s = String(s) + if (s.indexOf("~") === -1 && s.indexOf("/") === -1) return s + let chars = [...s], + out = "", + char: string | undefined + while ((char = chars.shift()) !== undefined) { + out += + char === "/" ? "~1" : + char === "~" ? "~0" : + char + } + return escape(out) + }).join("/") + return out.includes('${') ? `\`${out}\`` : `"${out}"` + } +} + +function requiresObjectIs(x: unknown): boolean { + return JsonSchema.isNever(x) + || JsonSchema.isInteger(x) + || JsonSchema.isNumber(x) + || JsonSchema.isEnum(x) + || JsonSchema.isUnion(x) && x.anyOf.some(requiresObjectIs) + || JsonSchema.isUnknown(x) +} + +function StrictlyEqualOrDiff(x: (string | number)[], y: (string | number)[], ix: Scope) { + const X = joinPath(x, ix.isOptional) + const Y = joinPath(y, ix.isOptional) + return [ + `if (${X} !== ${Y}) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + `}`, + ].join('\n') +} + +function SameNumberOrDiff(x: (string | number)[], y: (string | number)[], ix: Scope) { + const X = joinPath(x, ix.isOptional) + const Y = joinPath(y, ix.isOptional) + return [ + `if (${X} !== ${Y} && (${X} === ${X} || ${Y} === ${Y})) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + `}`, + ].join('\n') +} + +function SameValueOrDiff(x: (string | number)[], y: (string | number)[], ix: Scope) { + const X = joinPath(x, ix.isOptional) + const Y = joinPath(y, ix.isOptional) + return [ + `if (!Object.is(${X}, ${Y})) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + `}`, + ].join('\n') +} + +function diffNever(...args: Parameters) { return SameValueOrDiff(...args) } +function diffUnknown(...args: Parameters) { return SameValueOrDiff(...args) } +function diffNull(...args: Parameters) { return StrictlyEqualOrDiff(...args) } +function diffBoolean(...args: Parameters) { return StrictlyEqualOrDiff(...args) } +function diffInteger(...args: Parameters) { return SameNumberOrDiff(...args) } +function diffNumber(...args: Parameters) { return SameNumberOrDiff(...args) } +function diffString(...args: Parameters) { return StrictlyEqualOrDiff(...args) } + +const foldJson = Json.fold((x) => { + switch (true) { + default: return (void (x satisfies never), SameValueOrDiff) + case x == null: return diffNull + case x === true: + case x === false: return diffBoolean + case typeof x === 'number': return diffNumber + case typeof x === 'string': return diffString + case Json.isArray(x): return function diffConstArray(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + return x.length === 0 + ? [ + `if (${X_PATH}.length !== ${Y_PATH}.length) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + : x.map( + (continuation, I) => continuation( + [X_PATH, I], + [Y_PATH, I], + { ...IX, dataPath: [...IX.dataPath, I] } + ) + ).join('\n') + } + case Json.isObject(x): return function diffConstObject(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + const BODY = Object_entries(x).map( + ([k, continuation]) => continuation( + [X_PATH, k], + [Y_PATH, k], + { ...IX, dataPath: [...IX.dataPath, k] } + ) + ) + return BODY.length === 0 + ? [ + `if (Object.keys(${X_PATH}).length !== Object.keys(${Y_PATH}).length) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + : BODY.join('\n') + } + } +}) + +function createEnumDiff(x: JsonSchema.Enum): Builder { + return function diffEnum(X, Y, IX) { + return ( + x.enum.every((v) => typeof v === 'number') ? SameNumberOrDiff(X, Y, IX) + : x.enum.some((v) => typeof v === 'number') ? SameValueOrDiff(X, Y, IX) + : StrictlyEqualOrDiff(X, Y, IX) + ) + } +} + +function createArrayDiff(x: JsonSchema.Array): Builder { + return function diffArray(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + const X_ITEM_IDENT = `${ident(X_PATH, IX.bindings)}_item` + const Y_ITEM_IDENT = `${ident(Y_PATH, IX.bindings)}_item` + const LENGTH_IDENT = ident('length', IX.bindings) + const IX_IDENT = ident('ix', IX.bindings) + const DOT = IX.isOptional ? '?.' : '.' + const PATH = [...IX.dataPath, `\${${IX_IDENT}}`] + return [ + `const ${LENGTH_IDENT} = Math.min(${X_PATH}${DOT}length, ${Y_PATH}${DOT}length);`, + `let ${IX_IDENT} = 0;`, + `for (; ${IX_IDENT} < ${LENGTH_IDENT}; ${IX_IDENT}++) {`, + ` const ${X_ITEM_IDENT} = ${X_PATH}[${IX_IDENT}];`, + ` const ${Y_ITEM_IDENT} = ${Y_PATH}[${IX_IDENT}];`, + x.items([X_ITEM_IDENT], [Y_ITEM_IDENT], { ...IX, dataPath: PATH }), + `}`, + `if (${LENGTH_IDENT} < ${X_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${X_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ op: "remove", path: ${jsonPointer(PATH)} })`, + ` }`, + `}`, + `if (${LENGTH_IDENT} < ${Y_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${Y_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ op: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, + ` }`, + `}`, + ].join('\n') + } +} + +function createRecordDiff(x: JsonSchema.Record): Builder { + return function diffRecord(X, Y, IX) { + const KEY_IDENT = ident('key', IX.bindings) + const SEEN_IDENT = ident('seen', IX.bindings) + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + const PATH = [...IX.dataPath, `\${${KEY_IDENT}}`] + const patternEntries = !x.patternProperties ? null : Object_entries(x.patternProperties) + const PATTERN_PROPERTIES = !patternEntries ? null + : patternEntries.map(([k, continuation], I) => { + return [ + `${I === 0 ? '' : 'else'} if(/${k.length === 0 ? '^$' : k}/.test(${KEY_IDENT})) {`, + continuation([`${X_PATH}[${KEY_IDENT}]`], [`${Y_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + '}', + ].join('\n') + }).join('\n') + const ADDITIONAL_PROPERTIES = !x.additionalProperties ? null + : [ + x.patternProperties ? 'else {' : null, + x.additionalProperties([`${X_PATH}[${KEY_IDENT}]`], [`${Y_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + x.patternProperties ? '}' : null, + ].filter((_) => _ !== null).join('\n') + return [ + `const ${SEEN_IDENT} = new Set()`, + `for (let ${KEY_IDENT} in ${X_PATH}) {`, + ` ${SEEN_IDENT}.add(${KEY_IDENT})`, + ` if (!(${KEY_IDENT} in ${Y_PATH})) {`, + ` diff.push({ op: "remove", path: ${jsonPointer(PATH)} })`, + ` continue`, + ` }`, + PATTERN_PROPERTIES, + ADDITIONAL_PROPERTIES, + `}`, + `for (let ${KEY_IDENT} in ${Y_PATH}) {`, + ` if (${SEEN_IDENT}.has(${KEY_IDENT})) {`, + ` continue`, + ` }`, + ` if (!(${KEY_IDENT} in ${X_PATH})) {`, + ` diff.push({ op: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, + ` continue`, + ` }`, + PATTERN_PROPERTIES, + ADDITIONAL_PROPERTIES, + `}`, + ].filter((_) => _ !== null).join('\n') + } +} + +function createDiffOptional(continuation: Builder): Builder { + return function diffOptional(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + return [ + `if (${Y_PATH} === undefined && ${X_PATH} !== undefined) {`, + ` diff.push({ op: "remove", path: ${jsonPointer(IX.dataPath)} })`, + `}`, + `else if (${X_PATH} === undefined && ${Y_PATH} !== undefined) {`, + ` diff.push({ op: "add", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + `else if (${X_PATH} !== undefined && ${Y_PATH} !== undefined) {`, + continuation([X_PATH], [Y_PATH], { ...IX, isOptional: false }), + `}`, + ].join('\n') + } +} + +function createObjectDiff(x: JsonSchema.Object): Builder { + return function diffObject(X, Y, IX) { + const X_PATH = joinPath(X, false) // `false` because `*_PATH` already takes optionality into account + const Y_PATH = joinPath(Y, false) // `false` because `*_PATH` already takes optionality into account + const BODY = Object.entries(x.properties).map(([key, continuation]) => { + const isOptional = !x.required || !x.required.includes(key) + const index = { ...IX, dataPath: [...IX.dataPath, key], isOptional } + return isOptional + ? createDiffOptional(continuation)( + [X_PATH, key], + [Y_PATH, key], + index + ) + : continuation( + [X_PATH, key], + [Y_PATH, key], + index + ) + }) + + return BODY.length === 0 + ? [ + `if (${X_PATH} !== ${Y_PATH}) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + : BODY.join('\n') + } +} + +function createTupleDiff(x: JsonSchema.Tuple): Builder { + return function diffTuple(X, Y, IX) { + const X_PATH = joinPath(X, false) // `false` because `*_PATH` already takes optionality into account + const Y_PATH = joinPath(Y, false) // `false` because `*_PATH` already takes optionality into account + if (x.prefixItems.length === 0) { + return [ + `if (${X_PATH}.length !== ${Y_PATH}.length) {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + } + else return x.prefixItems.map( + (continuation, I) => continuation( + [X_PATH, I], + [Y_PATH, I], + { ...IX, dataPath: [...IX.dataPath, I] } + ) + ).join('\n') + } +} + +function createIntersectionDiff(x: JsonSchema.Intersection): Builder { + return function diffUnion(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + return x.allOf.length === 0 + ? diffUnknown(X, Y, IX) + : x.allOf.map((continuation) => continuation([X_PATH], [Y_PATH], IX)).join('\n') + } +} + +function createUnionDiff( + x: JsonSchema.Union, + input: JsonSchema.F +): Builder { + if (!JsonSchema.isUnion(input)) { + return Invariant.IllegalState('diff', 'expected input to be a union schema', input) + } + if (x.anyOf.length === 0) { + return diffNever + } else if (x.anyOf.length === 1) { + return x.anyOf[0] + } else { + if (!areAllObjects(input.anyOf)) { + return createInclusiveUnionDiff(x, input) + } else { + const withTags = getTags(input.anyOf) + return withTags === null + ? createInclusiveUnionDiff(x, input) + : createExclusiveUnionDiff(x, withTags) + } + } +} + +function createInclusiveUnionDiff( + x: JsonSchema.Union, + input: JsonSchema.Union +): Builder { + return function diffUnion(X, Y, IX) { + const X_PATH = joinPath(X, IX.isOptional) + const Y_PATH = joinPath(Y, IX.isOptional) + const sorted = input.anyOf + .map((option, i) => [option, i] satisfies [any, any]) + .toSorted(schemaOrdering) + const PREDICATES = sorted.map(([option]) => { + if (isPrimitive(option)) { + return null + } else { + const FUNCTION_NAME = ident('check', IX.bindings) + return { + FUNCTION_NAME: FUNCTION_NAME, + PREDICATE: check.writeable(option, { functionName: FUNCTION_NAME, stripTypes: true }) + } + } + }) + const CHECKS = sorted.map(([option, I], i, xs) => { + const continuation = x.anyOf[I] + const isFirst = i === 0 + const isLast = i === xs.length - 1 + const BODY = continuation([X_PATH], [Y_PATH], IX) + const IF_ELSEIF = isFirst ? 'if ' : 'else if ' + if (isPrimitive(option)) { + const CHECK = isLast ? null : inlinePrimitiveCheck( + option, + { path: X, ident: X_PATH }, + { path: Y, ident: Y_PATH }, + false + ) + return [ + `${IF_ELSEIF} (${CHECK}) {`, + BODY, + `}`, + ].filter((_) => _ !== null).join('\n') + } else { + const PREDICATE = PREDICATES[i] + const FUNCTION_NAME = PREDICATE === null ? null : PREDICATE.FUNCTION_NAME + const CHECK = `${FUNCTION_NAME}(${X_PATH}) && ${FUNCTION_NAME}(${Y_PATH})` + return [ + `${IF_ELSEIF} (${CHECK}) {`, + BODY, + `}` + ].filter((_) => _ !== null).join('\n') + } + }) + return [ + ...PREDICATES.map((_) => _ === null ? null : _.PREDICATE), + CHECKS.join('\n'), + `else {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].filter((_) => _ !== null).join('\n') + } +} + +function createExclusiveUnionDiff( + x: JsonSchema.Union, + [discriminant, TAGGED]: Discriminated +): Builder { + return function diffUnion(X, Y, IX) { + const X_PATH = joinPath(X, false) + const Y_PATH = joinPath(Y, false) + return [ + ...TAGGED.map(({ tag }, i) => { + const TAG = stringifyLiteral(tag) + const continuation = x.anyOf[i] + const X_ACCESSOR = joinPath([X_PATH, discriminant], IX.isOptional) + const Y_ACCESSOR = joinPath([Y_PATH, discriminant], IX.isOptional) + const IF_ELSEIF = i === 0 ? 'if ' : 'else if ' + return [ + `${IF_ELSEIF} (${X_ACCESSOR} === ${TAG} && ${Y_ACCESSOR} === ${TAG}) {`, + continuation([X_PATH], [Y_PATH], IX), + `}`, + ].join('\n') + }), + `else {`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + } +} + +const fold = JsonSchema.fold((x, _, input) => { + switch (true) { + default: return (void (x satisfies never), SameValueOrDiff) + case JsonSchema.isConst(x): return foldJson(x.const as Json.Unary) + case JsonSchema.isNever(x): return diffNever + case JsonSchema.isNull(x): return diffNull + case JsonSchema.isBoolean(x): return diffBoolean + case JsonSchema.isInteger(x): return diffInteger + case JsonSchema.isNumber(x): return diffNumber + case JsonSchema.isString(x): return diffString + case JsonSchema.isEnum(x): return createEnumDiff(x) + case JsonSchema.isArray(x): return createArrayDiff(x) + case JsonSchema.isRecord(x): return createRecordDiff(x) + case JsonSchema.isTuple(x): return createTupleDiff(x) + case JsonSchema.isObject(x): return createObjectDiff(x) + case JsonSchema.isUnion(x): return createUnionDiff(x, input) + case JsonSchema.isIntersection(x): return createIntersectionDiff(x) + case JsonSchema.isUnknown(x): return diffUnknown + } +}) + +export declare namespace diff { + export type Options = toType.Options & { + /** + * Configure the name of the generated diff function + * @default "diff" + */ + functionName?: string + /** + * Whether to remove TypeScript type annotations from the generated output + * @default false + */ + stripTypes?: boolean + } + export { + Diff, + Edit, + // Delete, + // Insert, + // Update, + Add, + Replace, + Remove, + } +} + +diff.writeable = diff_writeable +diff.unfuzzable = diff_unfuzzable + +export function diff>(schema: S): Diff +export function diff(schema: JsonSchema) { + const index = defaultIndex() + const ROOT_CHECK = requiresObjectIs(schema) ? `if (Object.is(x, y)) return diff` : `if (x === y) return diff` + const BODY = fold(schema)(['x'], ['y'], index) + return JsonSchema.isNullary(schema) + ? globalThis.Function('x', 'y', [ + `const diff = []`, + BODY, + 'return diff' + ].join('\n')) + : globalThis.Function('x', 'y', [ + `const diff = []`, + ROOT_CHECK, + BODY, + 'return diff' + ].join('\n')) +} + +function diff_writeable(schema: JsonSchema, options?: diff.Options): string { + const index = { ...defaultIndex(), ...options } satisfies Scope + const compiled = fold(schema)(['x'], ['y'], index) + const FUNCTION_NAME = options?.functionName ?? 'diff' + const inputType = options?.stripTypes ? '' : toType(schema, options) + const TYPE = options?.stripTypes ? '' : `: ${options?.typeName ?? inputType}` + const ROOT_DIFF = requiresObjectIs(schema) ? `if (Object.is(x, y)) return diff` : `if (x === y) return diff` + const BODY = compiled.length === 0 ? null : compiled + return ( + JsonSchema.isNullary(schema) + ? [ + options?.typeName === undefined ? null : inputType, + `function ${FUNCTION_NAME} (x${TYPE}, y${TYPE}) {`, + `let diff = []`, + BODY, + `return diff;`, + `}`, + ] + : [ + options?.typeName === undefined ? null : inputType, + `function ${FUNCTION_NAME} (x${TYPE}, y${TYPE}) {`, + `let diff = []`, + ROOT_DIFF, + BODY, + `return diff;`, + `}` + ] + ).filter((_) => _ !== null).join('\n') +} diff --git a/packages/json-schema/src/exports.ts b/packages/json-schema/src/exports.ts index d4fb0e68..b594959d 100644 --- a/packages/json-schema/src/exports.ts +++ b/packages/json-schema/src/exports.ts @@ -2,3 +2,4 @@ export * from '@traversable/json-schema-types' export { VERSION } from './version.js' export { deepEqual } from './deep-equal.js' export { deepClone } from './deep-clone.js' +export { diff } from './diff.js' diff --git a/packages/json-schema/test/deep-clone.object.bench.ts b/packages/json-schema/test/deep-clone.object.bench.ts index 26bf6117..4fec3660 100644 --- a/packages/json-schema/test/deep-clone.object.bench.ts +++ b/packages/json-schema/test/deep-clone.object.bench.ts @@ -9,17 +9,6 @@ type Type = { city: string } -/** - * @example - * function handRolled(x: Type): Type { - * return { - * street1: x.street1, - * ...x.street2 !== undefined && { street2: x.street2 }, - * city: x.city, - * } - * } - */ - const JsonSchema_deepClone = JsonSchema.deepClone({ type: 'object', required: ['street1', 'city'], @@ -86,19 +75,6 @@ boxplot(() => { } }).gc('inner') - /** - * @example - * bench('handRolled', function* () { - * yield { - * [0]() { return data }, - * bench(x: Type) { - * do_not_optimize( - * handRolled(x) - * ) - * } - * } - * }).gc('inner') - */ }) }) }) diff --git a/packages/json-schema/test/deep-clone.real-world-example.bench.ts b/packages/json-schema/test/deep-clone.real-world-example.bench.ts index d23b48d0..e0cfb22e 100644 --- a/packages/json-schema/test/deep-clone.real-world-example.bench.ts +++ b/packages/json-schema/test/deep-clone.real-world-example.bench.ts @@ -46,6 +46,7 @@ boxplot(() => { summary(() => { group('〖🏁️〗››› JsonSchema.deepClone: real-world example', () => { barplot(() => { + bench('Lodash', function* () { yield { [0]() { return data }, @@ -67,6 +68,7 @@ boxplot(() => { } } }).gc('inner') + }) }) }) diff --git a/packages/json-schema/test/deep-clone.test.ts b/packages/json-schema/test/deep-clone.test.ts index 9663c9ad..adb6d2fe 100644 --- a/packages/json-schema/test/deep-clone.test.ts +++ b/packages/json-schema/test/deep-clone.test.ts @@ -43,7 +43,7 @@ const Schema = { } -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/zod❳: JsonSchema.deepClone.writeable', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.deepClone.writeable', () => { vi.test('〖⛳️〗› ❲JsonSchema.deepClone.writeable❳: Schema.never', () => { vi.expect.soft(format( JsonSchema.deepClone.writeable( @@ -2046,7 +2046,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traversable/zod❳: JsonSchema.deepClone. }) }) -vi.describe('〖⛳️〗‹‹‹ ❲@traversable/zod❳: JsonSchema.deepClone', () => { +vi.describe('〖⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.deepClone', () => { vi.test('〖⛳️〗› ❲JsonSchema.deepClone❳: Schema.array', () => { const clone_01 = JsonSchema.deepClone( Schema.array( diff --git a/packages/json-schema/test/deep-clone.tuple.bench.ts b/packages/json-schema/test/deep-clone.tuple.bench.ts index d8c7f42e..e3550361 100644 --- a/packages/json-schema/test/deep-clone.tuple.bench.ts +++ b/packages/json-schema/test/deep-clone.tuple.bench.ts @@ -8,24 +8,6 @@ type Type = [ { street1: string; street2?: string; city: string } ] -/** - * @example - * function handRolled(x: Type) { - * return [ - * { - * street1: x[0].street1, - * ...x[0].street2 !== undefined && { street2: x[0].street2 }, - * city: x[0].city - * }, - * { - * street1: x[1].street1, - * ...x[1].street2 !== undefined && { street2: x[1].street2 }, - * city: x[1].city - * }, - * ] - * } - */ - const deepClone = JsonSchema.deepClone({ type: 'array', prefixItems: [ @@ -69,6 +51,7 @@ boxplot(() => { summary(() => { group('〖🏁️〗››› JsonSchema.deepClone: tuple', () => { barplot(() => { + bench('Lodash', function* () { yield { [0]() { return data }, @@ -113,19 +96,6 @@ boxplot(() => { } }).gc('inner') - /** - * @example - * bench('handRolled', function* () { - * yield { - * [0]() { return data }, - * bench(x: Type) { - * do_not_optimize( - * handRolled(x) - * ) - * } - * } - * }).gc('inner') - */ }) }) }) diff --git a/packages/json-schema/test/deep-equal.test.ts b/packages/json-schema/test/deep-equal.test.ts index ee41c2e5..d7e4eab7 100644 --- a/packages/json-schema/test/deep-equal.test.ts +++ b/packages/json-schema/test/deep-equal.test.ts @@ -7,7 +7,9 @@ const format = (src: string) => prettier.format(src, { parser: 'typescript', sem vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Never', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ not: {} }) + JsonSchema.deepEqual.writeable( + { not: {} } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: never, r: never) { @@ -30,7 +32,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Unknown', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({}) + JsonSchema.deepEqual.writeable( + {} + ) )).toMatchInlineSnapshot (` "function deepEqual(l: unknown, r: unknown) { @@ -44,7 +48,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Null', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ type: 'null' }) + JsonSchema.deepEqual.writeable( + { type: 'null' } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: null, r: null) { @@ -57,7 +63,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Boolean', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ type: 'boolean' }) + JsonSchema.deepEqual.writeable( + { type: 'boolean' } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: boolean, r: boolean) { @@ -70,7 +78,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Integer', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ type: 'integer' }) + JsonSchema.deepEqual.writeable( + { type: 'integer' } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: number, r: number) { @@ -83,7 +93,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Number', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ type: 'number' }) + JsonSchema.deepEqual.writeable( + { type: 'number' } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: number, r: number) { @@ -96,7 +108,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.String', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ type: 'string' }) + JsonSchema.deepEqual.writeable( + { type: 'string' } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: string, r: string) { @@ -109,7 +123,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Enum', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ enum: [] }) + JsonSchema.deepEqual.writeable( + { enum: [] } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: never, r: never) { @@ -119,7 +135,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { " `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ enum: [1] }) + JsonSchema.deepEqual.writeable( + { enum: [1] } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: 1, r: 1) { @@ -129,7 +147,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { " `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ enum: ["1", false, 2] }) + JsonSchema.deepEqual.writeable( + { enum: ["1", false, 2] } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: "1" | false | 2, r: "1" | false | 2) { @@ -142,7 +162,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Const', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ const: true }) + JsonSchema.deepEqual.writeable( + { const: true } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: true, r: true) { @@ -153,7 +175,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ const: [] }) + JsonSchema.deepEqual.writeable( + { const: [] } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: [], r: []) { @@ -166,7 +190,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ const: [true] }) + JsonSchema.deepEqual.writeable( + { const: [true] } + ) )).toMatchInlineSnapshot (` "function deepEqual(l: [true], r: [true]) { @@ -248,18 +274,20 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ - type: 'array', - items: { + JsonSchema.deepEqual.writeable( + { type: 'array', items: { type: 'array', items: { - type: 'string' + type: 'array', + items: { + type: 'string' + } } } } - }) + ) )).toMatchInlineSnapshot (` "function deepEqual( @@ -292,47 +320,50 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ - type: 'array', - items: { - type: 'object', - required: ['a'], - properties: { - a: { - type: 'array', - items: { - type: 'object', - required: ['b'], - properties: { - b: { - type: 'array', - items: { - type: 'string' - } - }, - c: { type: 'string' } + JsonSchema.deepEqual.writeable( + { + type: 'array', + items: { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'array', + items: { + type: 'object', + required: ['b'], + properties: { + b: { + type: 'array', + items: { + type: 'string' + } + }, + c: { type: 'string' } + } } - } - }, - d: { - type: 'array', - items: { - type: 'object', - required: ['f'], - properties: { - e: { - type: 'array', - items: { - type: 'string' - } - }, - f: { type: 'string' } + }, + d: { + type: 'array', + items: { + type: 'object', + required: ['f'], + properties: { + e: { + type: 'array', + items: { + type: 'string' + } + }, + f: { type: 'string' } + } } } } } - } - }, { typeName: 'Type' }), + }, + { typeName: 'Type' } + ) )).toMatchInlineSnapshot (` "type Type = Array<{ @@ -494,12 +525,14 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Record', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ - type: 'object', - additionalProperties: { - type: 'boolean' + JsonSchema.deepEqual.writeable( + { + type: 'object', + additionalProperties: { + type: 'boolean' + } } - }) + ) )).toMatchInlineSnapshot (` "function deepEqual(l: Record, r: Record) { @@ -520,10 +553,15 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ - type: 'object', - patternProperties: { "abc": { type: 'boolean' } }, - }, { typeName: 'Type' }) + JsonSchema.deepEqual.writeable( + { + type: 'object', + patternProperties: { + "abc": { type: 'boolean' } + }, + }, + { typeName: 'Type' } + ) )).toMatchInlineSnapshot (` "type Type = { abc: boolean } @@ -549,15 +587,17 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { vi.test('〖️⛳️〗› ❲JsonSchema.deepEqual.writeable❳: JsonSchema.Object', () => { vi.expect.soft(format( - JsonSchema.deepEqual.writeable({ - type: 'object', - required: [], - properties: { - a: { - type: 'boolean' + JsonSchema.deepEqual.writeable( + { + type: 'object', + required: [], + properties: { + a: { + type: 'boolean' + } } } - }) + ) )).toMatchInlineSnapshot (` "function deepEqual(l: { a?: boolean }, r: { a?: boolean }) { diff --git a/packages/json-schema/test/diff.fuzz.test.ts b/packages/json-schema/test/diff.fuzz.test.ts new file mode 100644 index 00000000..8977ca83 --- /dev/null +++ b/packages/json-schema/test/diff.fuzz.test.ts @@ -0,0 +1,171 @@ +import * as vi from 'vitest' +import * as fc from 'fast-check' +import prettier from '@prettier/sync' + +import { JsonSchema } from '@traversable/json-schema' +import { JsonSchemaTest } from '@traversable/json-schema-test' +import type { Insert, Update, Delete } from '@sinclair/typebox/value' +import { Diff as oracle } from '@sinclair/typebox/value' + +const format = (src: string) => prettier.format(src, { parser: 'typescript', semi: false }) + +type LoggerDeps = { + schema: JsonSchema + left: unknown + right: unknown + error: unknown +} + +function logger({ schema, left, right, error }: LoggerDeps) { + console.group('FAILURE: property test for JsonSchema.diff') + console.error('ERROR:', error) + console.debug('schema:', JSON.stringify(schema, null, 2)) + console.debug('diffFn:', format(JsonSchema.diff.writeable(schema))) + console.debug('diff:', JSON.stringify(adapt(JsonSchema.diff(schema)(left, right)).sort(sort), null, 2)) + console.debug('oracle:', JSON.stringify(oracle(left, right).sort(sort), null, 2)) + console.debug('left:', left) + console.debug('right:', right) + console.groupEnd() +} + +const Builder = { + additionalProperties: JsonSchemaTest.SeedGenerator({ + exclude: JsonSchema.diff.unfuzzable, + record: { additionalPropertiesOnly: true }, + number: { noNaN: true }, + }), + patternProperties: JsonSchemaTest.SeedGenerator({ + exclude: JsonSchema.diff.unfuzzable, + record: { patternPropertiesOnly: true }, + number: { noNaN: true }, + }) +} + +const adapter = { + add({ path, value }: JsonSchema.diff.Add) { + return { type: 'insert', path, value } satisfies Insert + }, + replace({ path, value }: JsonSchema.diff.Replace) { + return { type: 'update', path, value } satisfies Update + }, + remove({ path }: JsonSchema.diff.Remove) { + return { type: 'delete', path } satisfies Delete + }, +} + +function adapt(xs: JsonSchema.diff.Edit[]) { + return xs.map((x) => adapter[x.op](x as never)) +} + +function sort(x: T, y: T) { + return x.path < y.path ? -1 : y.path < x.path ? 1 : 0 +} + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { + vi.test('〖⛳️〗› ❲JsonSchema.diff❳: equal data (additionalProperties only)', () => { + fc.assert( + fc.property( + Builder.additionalProperties['*'], + (seed) => { + const schema = JsonSchemaTest.seedToSchema(seed) + const diff = JsonSchema.diff(schema) + const arbitrary = JsonSchemaTest.seedToValidDataGenerator(seed) + const duplicate = fc.clone(arbitrary, 2) + const [left, right] = fc.sample(duplicate, 1)[0] + try { + vi.assert.deepEqual( + adapt(diff(left, right)).sort(sort), + oracle(left, right).sort(sort) + ) + } catch (error) { + logger({ schema, left, right, error }) + vi.expect.fail('diff(left, right) !== oracle(left, right) (additionalPropertiesOnly)') + } + } + ), { + endOnFailure: true, + examples: [], + // numRuns: 10_000, + }) + }) + + vi.test('〖⛳️〗› ❲JsonSchema.diff❳: equal data (patternProperties only)', () => { + fc.assert( + fc.property( + Builder.patternProperties['*'], + (seed) => { + const schema = JsonSchemaTest.seedToSchema(seed) + const diff = JsonSchema.diff(schema) + const arbitrary = JsonSchemaTest.seedToValidDataGenerator(seed) + const duplicate = fc.clone(arbitrary, 2) + const [left, right] = fc.sample(duplicate, 1)[0] + try { + vi.assert.deepEqual( + adapt(diff(left, right)).sort(sort), + oracle(left, right).sort(sort) + ) + } catch (error) { + logger({ schema, left, right, error }) + vi.expect.fail('diff(left, right) !== oracle(left, right) (additionalPropertiesOnly)') + } + } + ), { + endOnFailure: true, + examples: [], + // numRuns: 10_000, + }) + }) + + // vi.test.skip('〖⛳️〗› ❲JsonSchema.diff❳: unequal data (additionalProperties only)', () => { + // fc.assert( + // fc.property( + // Builder.additionalProperties['*'], + // (seed) => { + // const schema = JsonSchemaTest.seedToSchema(seed) + // const diff = JsonSchema.diff(schema) + // const arbitrary = JsonSchemaTest.seedToValidDataGenerator(seed) + // const [left, right] = fc.sample(arbitrary, 2) + // try { + // vi.assert.deepEqual( + // adapt(diff(left, right)).sort(sort), + // oracle(left, right).sort(sort) + // ) + // } catch (error) { + // logger({ schema, left, right, error }) + // vi.expect.fail('diff(left, right) !== oracle(left, right) (additionalPropertiesOnly)') + // } + // } + // ), { + // endOnFailure: true, + // examples: [], + // numRuns: 10_000, + // }) + // }) + + // vi.test.skip('〖⛳️〗› ❲JsonSchema.diff❳: unequal data (patternProperties only)', () => { + // fc.assert( + // fc.property( + // Builder.patternProperties['*'], + // (seed) => { + // const schema = JsonSchemaTest.seedToSchema(seed) + // const diff = JsonSchema.diff(schema) + // const arbitrary = JsonSchemaTest.seedToValidDataGenerator(seed) + // const [left, right] = fc.sample(arbitrary, 2) + // try { + // vi.assert.deepEqual( + // adapt(diff(left, right)).sort(sort), + // oracle(left, right).sort(sort) + // ) + // } catch (error) { + // logger({ schema, left, right, error }) + // vi.expect.fail('diff(left, right) !== oracle(left, right) (additionalPropertiesOnly)') + // } + // } + // ), { + // endOnFailure: true, + // examples: [], + // numRuns: 10_000, + // }) + // }) + +}) diff --git a/packages/json-schema/test/diff.test.ts b/packages/json-schema/test/diff.test.ts new file mode 100644 index 00000000..b8d8c31f --- /dev/null +++ b/packages/json-schema/test/diff.test.ts @@ -0,0 +1,2473 @@ +import * as vi from 'vitest' +import type { Insert, Update, Delete } from '@sinclair/typebox/value' +import { Diff as oracle } from '@sinclair/typebox/value' +import prettier from '@prettier/sync' +import { fn } from '@traversable/registry' +import { JsonSchema } from '@traversable/json-schema' + +const format = (src: string) => prettier.format(src, { parser: 'typescript', semi: false }) + +const adapter = { + add({ path, value }: JsonSchema.diff.Add) { + return { type: 'insert', path, value } satisfies Insert + }, + replace({ path, value }: JsonSchema.diff.Replace) { + return { type: 'update', path, value } satisfies Update + }, + remove({ path }: JsonSchema.diff.Remove) { + return { type: 'delete', path } satisfies Delete + }, +} + +function adapt(xs: JsonSchema.diff.Edit[]) { + return xs.map((x) => adapter[x.op](x as never)) +} + +function sort(x: T, y: T) { + return x.path < y.path ? -1 : y.path < x.path ? 1 : 0 +} + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.diff.writeable', () => { + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Never', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { not: {} } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: never, y: never) { + let diff = [] + if (!Object.is(x, y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Null', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { type: 'null' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: null, y: null) { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Boolean', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { type: 'boolean' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: boolean, y: boolean) { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Integer', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { type: 'integer' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: number, y: number) { + let diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Number', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { type: 'number' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: number, y: number) { + let diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.String', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { type: 'string' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: string, y: string) { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Enum', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: never, y: never) { + let diff = [] + if (!Object.is(x, y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0, y: 0) { + let diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0, 1] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0 | 1, y: 0 | 1) { + let diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0, 'two'] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0 | "two", y: 0 | "two") { + let diff = [] + if (!Object.is(x, y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: ['one', 'two'] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: "one" | "two", y: "one" | "two") { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Const', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: null } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: null, y: null) { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: false } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: false, y: false) { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: 0 } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0, y: 0) { + let diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: 'hey' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: "hey", y: "hey") { + let diff = [] + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [], y: []) { + let diff = [] + if (x === y) return diff + if (x.length !== y.length) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [0, false] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [0, false], y: [0, false]) { + let diff = [] + if (x === y) return diff + if (x[0] !== y[0] && (x[0] === x[0] || y[0] === y[0])) { + diff.push({ op: "replace", path: "/0", value: y[0] }) + } + if (x[1] !== y[1]) { + diff.push({ op: "replace", path: "/1", value: y[1] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [[], [[false]]] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [[], [[false]]], y: [[], [[false]]]) { + let diff = [] + if (x === y) return diff + if (x[0].length !== y[0].length) { + diff.push({ op: "replace", path: "/0", value: y[0] }) + } + if (x[1][0][0] !== y[1][0][0]) { + diff.push({ op: "replace", path: "/1/0/0", value: y[1][0][0] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: {} } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: {}, y: {}) { + let diff = [] + if (x === y) return diff + if (Object.keys(x).length !== Object.keys(y).length) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + const: { + abc: 123, + def: false + } + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: { abc: 123; def: false }, y: { abc: 123; def: false }) { + let diff = [] + if (x === y) return diff + if (x.abc !== y.abc && (x.abc === x.abc || y.abc === y.abc)) { + diff.push({ op: "replace", path: "/abc", value: y.abc }) + } + if (x.def !== y.def) { + diff.push({ op: "replace", path: "/def", value: y.def }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + items: { + const: { + abc: 123, + def: false + } + } + } + ) + )).toMatchInlineSnapshot + (` + "function diff( + x: Array<{ abc: 123; def: false }>, + y: Array<{ abc: 123; def: false }>, + ) { + let diff = [] + if (x === y) return diff + const length = Math.min(x.length, y.length) + let ix = 0 + for (; ix < length; ix++) { + const x_item = x[ix] + const y_item = y[ix] + if ( + x_item.abc !== y_item.abc && + (x_item.abc === x_item.abc || y_item.abc === y_item.abc) + ) { + diff.push({ op: "replace", path: \`/\${ix}/abc\`, value: y_item.abc }) + } + if (x_item.def !== y_item.def) { + diff.push({ op: "replace", path: \`/\${ix}/def\`, value: y_item.def }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ op: "remove", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) + } + } + return diff + } + " + `) + + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Array', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + items: { + type: 'object', + required: ['street1', 'city'], + properties: { + street1: { type: 'string' }, + street2: { type: 'string' }, + city: { type: 'string' }, + } + } + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = Array<{ street1: string; street2?: string; city: string }> + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + const length = Math.min(x.length, y.length) + let ix = 0 + for (; ix < length; ix++) { + const x_item = x[ix] + const y_item = y[ix] + if (x_item.street1 !== y_item.street1) { + diff.push({ + op: "replace", + path: \`/\${ix}/street1\`, + value: y_item.street1, + }) + } + if (y_item?.street2 === undefined && x_item?.street2 !== undefined) { + diff.push({ op: "remove", path: \`/\${ix}/street2\` }) + } else if (x_item?.street2 === undefined && y_item?.street2 !== undefined) { + diff.push({ op: "add", path: \`/\${ix}/street2\`, value: y_item?.street2 }) + } else if (x_item?.street2 !== undefined && y_item?.street2 !== undefined) { + if (x_item?.street2 !== y_item?.street2) { + diff.push({ + op: "replace", + path: \`/\${ix}/street2\`, + value: y_item?.street2, + }) + } + } + if (x_item.city !== y_item.city) { + diff.push({ op: "replace", path: \`/\${ix}/city\`, value: y_item.city }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ op: "remove", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) + } + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + items: { + type: 'array', + items: { + type: 'array', + items: { type: 'string' } + } + } + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = Array>> + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + const length = Math.min(x.length, y.length) + let ix = 0 + for (; ix < length; ix++) { + const x_item = x[ix] + const y_item = y[ix] + const length1 = Math.min(x_item.length, y_item.length) + let ix1 = 0 + for (; ix1 < length1; ix1++) { + const x_item_item = x_item[ix1] + const y_item_item = y_item[ix1] + const length2 = Math.min(x_item_item.length, y_item_item.length) + let ix2 = 0 + for (; ix2 < length2; ix2++) { + const x_item_item_item = x_item_item[ix2] + const y_item_item_item = y_item_item[ix2] + if (x_item_item_item !== y_item_item_item) { + diff.push({ + op: "replace", + path: \`/\${ix}/\${ix1}/\${ix2}\`, + value: y_item_item_item, + }) + } + } + if (length2 < x_item_item.length) { + for (; ix2 < x_item_item.length; ix2++) { + diff.push({ op: "remove", path: \`/\${ix}/\${ix1}/\${ix2}\` }) + } + } + if (length2 < y_item_item.length) { + for (; ix2 < y_item_item.length; ix2++) { + diff.push({ + op: "add", + path: \`/\${ix}/\${ix1}/\${ix2}\`, + value: y_item_item[ix2], + }) + } + } + } + if (length1 < x_item.length) { + for (; ix1 < x_item.length; ix1++) { + diff.push({ op: "remove", path: \`/\${ix}/\${ix1}\` }) + } + } + if (length1 < y_item.length) { + for (; ix1 < y_item.length; ix1++) { + diff.push({ op: "add", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) + } + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ op: "remove", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) + } + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Record', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'object', + patternProperties: { + abc: { type: 'string' }, + def: { type: 'number' } + }, + additionalProperties: { + type: 'array', + items: { type: 'string' } + } + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = Record> & { abc: string; def: number } + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + const seen = new Set() + for (let key in x) { + seen.add(key) + if (!(key in y)) { + diff.push({ op: "remove", path: \`/\${key}\` }) + continue + } + if (/abc/.test(key)) { + if (x[key] !== y[key]) { + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) + } + } else if (/def/.test(key)) { + if (x[key] !== y[key] && (x[key] === x[key] || y[key] === y[key])) { + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) + } + } else { + const length = Math.min(x[key].length, y[key].length) + let ix = 0 + for (; ix < length; ix++) { + const x_key__item = x[key][ix] + const y_key__item = y[key][ix] + if (x_key__item !== y_key__item) { + diff.push({ + op: "replace", + path: \`/\${key}/\${ix}\`, + value: y_key__item, + }) + } + } + if (length < x[key].length) { + for (; ix < x[key].length; ix++) { + diff.push({ op: "remove", path: \`/\${key}/\${ix}\` }) + } + } + if (length < y[key].length) { + for (; ix < y[key].length; ix++) { + diff.push({ op: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) + } + } + } + } + for (let key in y) { + if (seen.has(key)) { + continue + } + if (!(key in x)) { + diff.push({ op: "add", path: \`/\${key}\`, value: y[key] }) + continue + } + if (/abc/.test(key)) { + if (x[key] !== y[key]) { + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) + } + } else if (/def/.test(key)) { + if (x[key] !== y[key] && (x[key] === x[key] || y[key] === y[key])) { + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) + } + } else { + const length = Math.min(x[key].length, y[key].length) + let ix = 0 + for (; ix < length; ix++) { + const x_key__item = x[key][ix] + const y_key__item = y[key][ix] + if (x_key__item !== y_key__item) { + diff.push({ + op: "replace", + path: \`/\${key}/\${ix}\`, + value: y_key__item, + }) + } + } + if (length < x[key].length) { + for (; ix < x[key].length; ix++) { + diff.push({ op: "remove", path: \`/\${key}/\${ix}\` }) + } + } + if (length < y[key].length) { + for (; ix < y[key].length; ix++) { + diff.push({ op: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) + } + } + } + } + return diff + } + " + `) + + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Object', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'object', + required: ['firstName', 'addresses'], + properties: { + firstName: { type: 'string' }, + lastName: { type: 'string' }, + addresses: { + type: 'array', + items: { + type: 'object', + required: ['street1', 'city'], + properties: { + street1: { type: 'string' }, + street2: { type: 'string' }, + city: { type: 'string' }, + } + } + } + } + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = { + firstName: string + lastName?: string + addresses: Array<{ street1: string; street2?: string; city: string }> + } + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (x.firstName !== y.firstName) { + diff.push({ op: "replace", path: "/firstName", value: y.firstName }) + } + if (y?.lastName === undefined && x?.lastName !== undefined) { + diff.push({ op: "remove", path: "/lastName" }) + } else if (x?.lastName === undefined && y?.lastName !== undefined) { + diff.push({ op: "add", path: "/lastName", value: y?.lastName }) + } else if (x?.lastName !== undefined && y?.lastName !== undefined) { + if (x?.lastName !== y?.lastName) { + diff.push({ op: "replace", path: "/lastName", value: y?.lastName }) + } + } + const length = Math.min(x.addresses.length, y.addresses.length) + let ix = 0 + for (; ix < length; ix++) { + const x_addresses_item = x.addresses[ix] + const y_addresses_item = y.addresses[ix] + if (x_addresses_item.street1 !== y_addresses_item.street1) { + diff.push({ + op: "replace", + path: \`/addresses/\${ix}/street1\`, + value: y_addresses_item.street1, + }) + } + if ( + y_addresses_item?.street2 === undefined && + x_addresses_item?.street2 !== undefined + ) { + diff.push({ op: "remove", path: \`/addresses/\${ix}/street2\` }) + } else if ( + x_addresses_item?.street2 === undefined && + y_addresses_item?.street2 !== undefined + ) { + diff.push({ + op: "add", + path: \`/addresses/\${ix}/street2\`, + value: y_addresses_item?.street2, + }) + } else if ( + x_addresses_item?.street2 !== undefined && + y_addresses_item?.street2 !== undefined + ) { + if (x_addresses_item?.street2 !== y_addresses_item?.street2) { + diff.push({ + op: "replace", + path: \`/addresses/\${ix}/street2\`, + value: y_addresses_item?.street2, + }) + } + } + if (x_addresses_item.city !== y_addresses_item.city) { + diff.push({ + op: "replace", + path: \`/addresses/\${ix}/city\`, + value: y_addresses_item.city, + }) + } + } + if (length < x.addresses.length) { + for (; ix < x.addresses.length; ix++) { + diff.push({ op: "remove", path: \`/addresses/\${ix}\` }) + } + } + if (length < y.addresses.length) { + for (; ix < y.addresses.length; ix++) { + diff.push({ op: "add", path: \`/addresses/\${ix}\`, value: y.addresses[ix] }) + } + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Tuple', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [], + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [], y: []) { + let diff = [] + if (x === y) return diff + if (x.length !== y.length) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [ + { type: 'string' } + ], + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [string], y: [string]) { + let diff = [] + if (x === y) return diff + if (x[0] !== y[0]) { + diff.push({ op: "replace", path: "/0", value: y[0] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [ + { type: 'string' }, + { type: 'number' }, + ], + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [string, number], y: [string, number]) { + let diff = [] + if (x === y) return diff + if (x[0] !== y[0]) { + diff.push({ op: "replace", path: "/0", value: y[0] }) + } + if (x[1] !== y[1] && (x[1] === x[1] || y[1] === y[1])) { + diff.push({ op: "replace", path: "/1", value: y[1] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [ + { + type: 'array', + prefixItems: [ + { type: 'string' }, + ], + }, + { + type: 'array', + prefixItems: [ + { + type: 'array', + prefixItems: [ + { type: 'string' }, + { type: 'number' } + ] + } + ] + }, + ], + } + ) + )).toMatchInlineSnapshot + (` + "function diff( + x: [[string], [[string, number]]], + y: [[string], [[string, number]]], + ) { + let diff = [] + if (x === y) return diff + if (x[0][0] !== y[0][0]) { + diff.push({ op: "replace", path: "/0/0", value: y[0][0] }) + } + if (x[1][0][0] !== y[1][0][0]) { + diff.push({ op: "replace", path: "/1/0/0", value: y[1][0][0] }) + } + if ( + x[1][0][1] !== y[1][0][1] && + (x[1][0][1] === x[1][0][1] || y[1][0][1] === y[1][0][1]) + ) { + diff.push({ op: "replace", path: "/1/0/1", value: y[1][0][1] }) + } + return diff + } + " + `) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Union', () => { + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + anyOf: [] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = never + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (!Object.is(x, y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + anyOf: [ + { type: 'string' } + ] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = string + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + anyOf: [ + { type: 'number' }, + { type: 'string' }, + { + type: 'array', + items: { + type: 'number' + } + }, + { + type: 'object', + required: ['street1', 'city'], + properties: { + street1: { type: 'string' }, + street2: { type: 'string' }, + city: { type: 'string' }, + } + } + ] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = + | number + | string + | Array + | { street1: string; street2?: string; city: string } + function diff(x: Type, y: Type) { + let diff = [] + if (Object.is(x, y)) return diff + function check(value) { + return ( + Array.isArray(value) && value.every((value) => Number.isFinite(value)) + ) + } + function check1(value) { + return ( + !!value && + typeof value === "object" && + typeof value.street1 === "string" && + (!Object.hasOwn(value, "street2") || typeof value.street2 === "string") && + typeof value.city === "string" + ) + } + if (typeof x === "number" && typeof y === "number") { + if (x !== y && (x === x || y === y)) { + diff.push({ op: "replace", path: "", value: y }) + } + } else if (typeof x === "string" && typeof y === "string") { + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + } else if (check(x) && check(y)) { + const length = Math.min(x.length, y.length) + let ix = 0 + for (; ix < length; ix++) { + const x_item = x[ix] + const y_item = y[ix] + if (x_item !== y_item && (x_item === x_item || y_item === y_item)) { + diff.push({ op: "replace", path: \`/\${ix}\`, value: y_item }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ op: "remove", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) + } + } + } else if (check1(x) && check1(y)) { + if (x.street1 !== y.street1) { + diff.push({ op: "replace", path: "/street1", value: y.street1 }) + } + if (y?.street2 === undefined && x?.street2 !== undefined) { + diff.push({ op: "remove", path: "/street2" }) + } else if (x?.street2 === undefined && y?.street2 !== undefined) { + diff.push({ op: "add", path: "/street2", value: y?.street2 }) + } else if (x?.street2 !== undefined && y?.street2 !== undefined) { + if (x?.street2 !== y?.street2) { + diff.push({ op: "replace", path: "/street2", value: y?.street2 }) + } + } + if (x.city !== y.city) { + diff.push({ op: "replace", path: "/city", value: y.city }) + } + } else { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + anyOf: [ + { type: 'string' } + ] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = string + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + anyOf: [ + { + type: 'object', + required: ['tag', 'onA'], + properties: { + tag: { const: 'A' }, + onA: { type: 'number' } + } + }, + { + type: 'object', + required: ['tag', 'onB'], + properties: { + tag: { const: 'B' }, + onB: { type: 'string' } + } + }, + ] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = { tag: "A"; onA: number } | { tag: "B"; onB: string } + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (x.tag === "A" && y.tag === "A") { + if (x.tag !== y.tag) { + diff.push({ op: "replace", path: "/tag", value: y.tag }) + } + if (x.onA !== y.onA && (x.onA === x.onA || y.onA === y.onA)) { + diff.push({ op: "replace", path: "/onA", value: y.onA }) + } + } else if (x.tag === "B" && y.tag === "B") { + if (x.tag !== y.tag) { + diff.push({ op: "replace", path: "/tag", value: y.tag }) + } + if (x.onB !== y.onB) { + diff.push({ op: "replace", path: "/onB", value: y.onB }) + } + } else { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Intersection', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + allOf: [] + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: unknown, y: unknown) { + let diff = [] + if (x === y) return diff + if (!Object.is(x, y)) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + allOf: [ + { type: 'string' } + ] + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: string, y: string) { + let diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ op: "replace", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + allOf: [ + { + type: 'object', + required: ['abc', 'ghi'], + properties: { + abc: { type: 'string' }, + def: { type: 'number' }, + ghi: { type: 'integer' }, + } + }, + { + type: 'object', + required: ['jkl', 'pqr'], + properties: { + jkl: { type: 'null' }, + def: { type: 'boolean' }, + pqr: { + type: 'array', + items: { type: 'string' }, + }, + } + } + ] + }, + { typeName: 'Type' } + ) + )).toMatchInlineSnapshot + (` + "type Type = { abc: string; def?: number; ghi: number } & { + jkl: null + def?: boolean + pqr: Array + } + function diff(x: Type, y: Type) { + let diff = [] + if (x === y) return diff + if (x.abc !== y.abc) { + diff.push({ op: "replace", path: "/abc", value: y.abc }) + } + if (y?.def === undefined && x?.def !== undefined) { + diff.push({ op: "remove", path: "/def" }) + } else if (x?.def === undefined && y?.def !== undefined) { + diff.push({ op: "add", path: "/def", value: y?.def }) + } else if (x?.def !== undefined && y?.def !== undefined) { + if (x?.def !== y?.def && (x?.def === x?.def || y?.def === y?.def)) { + diff.push({ op: "replace", path: "/def", value: y?.def }) + } + } + if (x.ghi !== y.ghi && (x.ghi === x.ghi || y.ghi === y.ghi)) { + diff.push({ op: "replace", path: "/ghi", value: y.ghi }) + } + if (x.jkl !== y.jkl) { + diff.push({ op: "replace", path: "/jkl", value: y.jkl }) + } + if (y?.def === undefined && x?.def !== undefined) { + diff.push({ op: "remove", path: "/def" }) + } else if (x?.def === undefined && y?.def !== undefined) { + diff.push({ op: "add", path: "/def", value: y?.def }) + } else if (x?.def !== undefined && y?.def !== undefined) { + if (x?.def !== y?.def) { + diff.push({ op: "replace", path: "/def", value: y?.def }) + } + } + const length = Math.min(x.pqr.length, y.pqr.length) + let ix = 0 + for (; ix < length; ix++) { + const x_pqr_item = x.pqr[ix] + const y_pqr_item = y.pqr[ix] + if (x_pqr_item !== y_pqr_item) { + diff.push({ op: "replace", path: \`/pqr/\${ix}\`, value: y_pqr_item }) + } + } + if (length < x.pqr.length) { + for (; ix < x.pqr.length; ix++) { + diff.push({ op: "remove", path: \`/pqr/\${ix}\` }) + } + } + if (length < y.pqr.length) { + for (; ix < y.pqr.length; ix++) { + diff.push({ op: "add", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) + } + } + return diff + } + " + `) + }) + +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.diff', () => { + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Never', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { not: {} } + ), + adapt + ) + + const x_1 = 0 as never + const y_1 = 1 as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Null', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'null' } + ), + adapt, + ) + + const x_1 = null as never + const y_1 = undefined as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Boolean', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'boolean' } + ), + adapt + ) + + const x_1 = true + const y_1 = false + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Integer', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'integer' } + ), + adapt, + ) + + const x_1 = 0 + const y_1 = 1 + // const y_1 = NaN + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Number', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'number' } + ), + adapt, + ) + + const x_1 = 0.25 + const y_1 = 1.5 + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.String', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'string' } + ), + adapt, + ) + + const x_1 = '' + const y_1 = ' ' + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Enum', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { enum: ['A', 'B'] } + ), + adapt + ) + + const x_1 = 'A' + const y_1 = 'B' + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Const', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { const: null } + ), + adapt, + ) + + const x_1 = null + const y_1 = undefined as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + + const diff_2 = fn.flow( + JsonSchema.diff( + { const: 0 } + ), + adapt, + ) + + const x_2 = 0 + const y_2 = 1 as never + + vi.assert.deepEqual( + diff_2(x_2, x_2).sort(sort), + oracle(x_2, x_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, y_2).sort(sort), + oracle(y_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(x_2, y_2).sort(sort), + oracle(x_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, x_2).sort(sort), + oracle(y_2, x_2).sort(sort) + ) + + const diff_3 = fn.flow( + JsonSchema.diff( + { const: false } + ), + adapt, + ) + + const x_3 = false + const y_3 = true as never + + vi.assert.deepEqual( + diff_3(x_3, x_3).sort(sort), + oracle(x_3, x_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(y_3, y_3).sort(sort), + oracle(y_3, y_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(x_3, y_3).sort(sort), + oracle(x_3, y_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(y_3, x_3).sort(sort), + oracle(y_3, x_3).sort(sort) + ) + + const diff_4 = fn.flow( + JsonSchema.diff( + { const: [] } + ), + adapt, + ) + + const x_4 = [] as [] + const y_4 = 'hi' as never + + vi.assert.deepEqual( + diff_4(x_4, x_4).sort(sort), + oracle(x_4, x_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(y_4, y_4).sort(sort), + oracle(y_4, y_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(x_4, y_4).sort(sort), + oracle(x_4, y_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(y_4, x_4).sort(sort), + oracle(y_4, x_4).sort(sort) + ) + + const diff_5 = fn.flow( + JsonSchema.diff( + { const: [1] } + ), + adapt, + ) + + const x_5 = [1] as [1] + const y_5 = [2] as never + + vi.assert.deepEqual( + diff_5(x_5, x_5).sort(sort), + oracle(x_5, x_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, y_5).sort(sort), + oracle(y_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(x_5, y_5).sort(sort), + oracle(x_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, x_5).sort(sort), + oracle(y_5, x_5).sort(sort) + ) + + const diff_6 = fn.flow( + JsonSchema.diff( + { const: { a: 1 } } + ), + adapt, + ) + + const x_6 = { a: 1 } as const + const y_6 = { a: 2 } as never + + vi.assert.deepEqual( + diff_6(x_6, x_6).sort(sort), + oracle(x_6, x_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(y_6, y_6).sort(sort), + oracle(y_6, y_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(x_6, y_6).sort(sort), + oracle(x_6, y_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(y_6, x_6).sort(sort), + oracle(y_6, x_6).sort(sort) + ) + + const diff_7 = fn.flow( + JsonSchema.diff( + { const: { a: 1, b: 2 } } + ), + adapt, + ) + + const x_7 = { a: 1, b: 2 } as const + const y_7 = { a: 2, b: 2 } as never + + vi.assert.deepEqual( + diff_7(x_7, x_7).sort(sort), + oracle(x_7, x_7).sort(sort) + ) + + vi.assert.deepEqual( + diff_7(y_7, y_7).sort(sort), + oracle(y_7, y_7).sort(sort) + ) + + vi.assert.deepEqual( + diff_7(x_7, y_7).sort(sort), + oracle(x_7, y_7).sort(sort) + ) + + vi.assert.deepEqual( + diff_7(y_7, x_7).sort(sort), + oracle(y_7, x_7).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Array', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'array', + items: { type: 'string' } + } + ), + adapt, + ) + + const x_1 = Array.of() + const y_1 = [''] + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Record', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'object', + additionalProperties: { + type: 'array', + items: { type: 'string' } + }, + patternProperties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ), + adapt, + ) + + const x_1 = { abc: 'hey', def: 0, x: [] } as never + const y_1 = { abc: 'ho', def: 1, x: ['suuuup', ''], y: ['hello', ''] } as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Object', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'object', + required: ['abc', 'def'], + properties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ), + adapt, + ) + + const x_1 = { abc: 'hey', def: 0 } + const y_1 = { abc: 'ho', def: 1 } + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Tuple', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'array', + prefixItems: [], + } + ), + adapt, + ) + + const x_1: [] = [] + const y_1 = 'hi' as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + + const diff_2 = fn.flow( + JsonSchema.diff( + { + type: 'array', + prefixItems: [ + { type: 'string' } + ], + } + ), + adapt, + ) + + const x_2 = ['hey'] as [string] + const y_2 = ['ho'] as [string] + + vi.assert.deepEqual( + diff_2(x_2, x_2).sort(sort), + oracle(x_2, x_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, y_2).sort(sort), + oracle(y_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(x_2, y_2).sort(sort), + oracle(x_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, x_2).sort(sort), + oracle(y_2, x_2).sort(sort) + ) + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Union', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + anyOf: [], + } + ), + adapt, + ) + + const x_1 = 0 as never + const y_1 = 1 as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + + const diff_2 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' } + ], + } + ), + adapt, + ) + + const x_2 = 'hey' + const y_2 = 'ho' + + vi.assert.deepEqual( + diff_2(x_2, x_2).sort(sort), + oracle(x_2, x_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, y_2).sort(sort), + oracle(y_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(x_2, y_2).sort(sort), + oracle(x_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, x_2).sort(sort), + oracle(y_2, x_2).sort(sort) + ) + + const diff_3 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + ], + } + ), + adapt, + ) + + const x_3 = 'hey' + const y_3 = 0 + + vi.assert.deepEqual( + diff_3(x_3, x_3).sort(sort), + oracle(x_3, x_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(y_3, y_3).sort(sort), + oracle(y_3, y_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(x_3, y_3).sort(sort), + oracle(x_3, y_3).sort(sort) + ) + + vi.assert.deepEqual( + diff_3(y_3, x_3).sort(sort), + oracle(y_3, x_3).sort(sort) + ) + + const diff_4 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + { + type: 'array', + items: { type: 'string' } + }, + ], + } + ), + adapt, + ) + + const x_4 = 'hey' + const y_4 = 0 + const z_4 = ['hey'] + + vi.assert.deepEqual( + diff_4(x_4, x_4).sort(sort), + oracle(x_4, x_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(y_4, y_4).sort(sort), + oracle(y_4, y_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(z_4, z_4).sort(sort), + oracle(z_4, z_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(x_4, y_4).sort(sort), + oracle(x_4, y_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(y_4, x_4).sort(sort), + oracle(y_4, x_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(x_4, z_4).sort(sort), + oracle(x_4, z_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(z_4, x_4).sort(sort), + oracle(z_4, x_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(z_4, y_4).sort(sort), + oracle(z_4, y_4).sort(sort) + ) + + vi.assert.deepEqual( + diff_4(y_4, z_4).sort(sort), + oracle(y_4, z_4).sort(sort) + ) + + const diff_5 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { + type: 'array', + items: { type: 'string' } + }, + { + type: 'object', + required: ['abc'], + properties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ], + } + ), + adapt, + ) + + const w_5 = ['hey'] + const x_5 = ['ho', 'let\'s', 'go'] + const y_5 = { abc: 'yay' } + const z_5 = { abc: 'yo', def: 1 } + + vi.assert.deepEqual( + diff_5(w_5, w_5).sort(sort), + oracle(w_5, w_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(x_5, x_5).sort(sort), + oracle(x_5, x_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, y_5).sort(sort), + oracle(y_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(z_5, z_5).sort(sort), + oracle(z_5, z_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(w_5, x_5).sort(sort), + oracle(w_5, x_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(x_5, w_5).sort(sort), + oracle(x_5, w_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(w_5, y_5).sort(sort), + oracle(w_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, w_5).sort(sort), + oracle(y_5, w_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(w_5, z_5).sort(sort), + oracle(w_5, z_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(z_5, w_5).sort(sort), + oracle(z_5, w_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(x_5, y_5).sort(sort), + oracle(x_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, x_5).sort(sort), + oracle(y_5, x_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(x_5, z_5).sort(sort), + oracle(x_5, z_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(z_5, x_5).sort(sort), + oracle(z_5, x_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(z_5, y_5).sort(sort), + oracle(z_5, y_5).sort(sort) + ) + + vi.assert.deepEqual( + diff_5(y_5, z_5).sort(sort), + oracle(y_5, z_5).sort(sort) + ) + + const diff_6 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { + type: 'object', + required: ['tag', 'onA'], + properties: { + tag: { const: 'A' }, + onA: { type: 'number' } + } + }, + { + type: 'object', + required: ['tag', 'onB'], + properties: { + tag: { const: 'B' }, + onB: { type: 'string' } + } + } + ], + } + ), + adapt, + ) + + const w_6 = { tag: 'A', onA: 0 } as const + const x_6 = { tag: 'A', onA: 1 } as const + const y_6 = { tag: 'B', onB: 'one' } as const + const z_6 = { tag: 'B', onB: 'two' } as const + + vi.assert.deepEqual( + diff_6(w_6, w_6).sort(sort), + oracle(w_6, w_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(x_6, x_6).sort(sort), + oracle(x_6, x_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(y_6, y_6).sort(sort), + oracle(y_6, y_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(z_6, z_6).sort(sort), + oracle(z_6, z_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(w_6, x_6).sort(sort), + oracle(w_6, x_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(x_6, w_6).sort(sort), + oracle(x_6, w_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(z_6, y_6).sort(sort), + oracle(z_6, y_6).sort(sort) + ) + + vi.assert.deepEqual( + diff_6(y_6, z_6).sort(sort), + oracle(y_6, z_6).sort(sort) + ) + + vi.expect.soft( + diff_6(w_6, y_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onB": "one", + "tag": "B", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(y_6, w_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onA": 0, + "tag": "A", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(w_6, z_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onB": "two", + "tag": "B", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(z_6, w_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onA": 0, + "tag": "A", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(x_6, y_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onB": "one", + "tag": "B", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(y_6, x_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onA": 1, + "tag": "A", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(x_6, z_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onB": "two", + "tag": "B", + }, + }, + ] + `) + + vi.expect.soft( + diff_6(z_6, x_6) + ).toMatchInlineSnapshot + (` + [ + { + "path": "", + "type": "update", + "value": { + "onA": 1, + "tag": "A", + }, + }, + ] + `) + + }) + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Intersection', () => { + const diff_1 = fn.flow( + JsonSchema.diff( + { + allOf: [], + } + ), + adapt, + ) + + const x_1 = 0 as never + const y_1 = 1 as never + + vi.assert.deepEqual( + diff_1(x_1, x_1).sort(sort), + oracle(x_1, x_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, y_1).sort(sort), + oracle(y_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(x_1, y_1).sort(sort), + oracle(x_1, y_1).sort(sort) + ) + + vi.assert.deepEqual( + diff_1(y_1, x_1).sort(sort), + oracle(y_1, x_1).sort(sort) + ) + + const diff_2 = fn.flow( + JsonSchema.diff( + { + allOf: [ + { + type: 'object', + required: ['abc'], + properties: { + abc: { type: 'integer' }, + def: { type: 'string' }, + } + }, + { + type: 'object', + required: ['ghi'], + properties: { + ghi: { type: 'number' }, + jkl: { type: 'boolean' }, + } + } + ], + } + ), + adapt, + ) + + const x_2 = { abc: 0, def: '', ghi: 1.1, jkl: false } + const y_2 = { abc: 1, def: 'hey', ghi: 0.1, jkl: true } + + vi.assert.deepEqual( + diff_2(x_2, x_2).sort(sort), + oracle(x_2, x_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, y_2).sort(sort), + oracle(y_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(x_2, y_2).sort(sort), + oracle(x_2, y_2).sort(sort) + ) + + vi.assert.deepEqual( + diff_2(y_2, x_2).sort(sort), + oracle(y_2, x_2).sort(sort) + ) + }) + +}) diff --git a/packages/json-schema/test/to-type.integration.test.ts b/packages/json-schema/test/to-type.integration.test.ts index 8217d7f2..464ed550 100644 --- a/packages/json-schema/test/to-type.integration.test.ts +++ b/packages/json-schema/test/to-type.integration.test.ts @@ -43,7 +43,7 @@ vi.describe('〖⛳️〗‹‹‹ ❲@traverable/json-schema❳: integration te ...types, ].join('\n') - vi.test('〖⛳️〗› ❲@traverable/zod❳: it writes', () => { + vi.test('〖⛳️〗› ❲@traverable/json-schema❳: it writes', () => { vi.assert.isTrue(fs.existsSync(PATH.target)) fs.writeFileSync(PATH.target, format(content)) vi.assert.isTrue(fs.existsSync(PATH.target)) diff --git a/packages/zod/src/to-string.ts b/packages/zod/src/to-string.ts index 6b6a361f..ccdd2789 100644 --- a/packages/zod/src/to-string.ts +++ b/packages/zod/src/to-string.ts @@ -109,7 +109,7 @@ export function toString(schema: z.ZodType | z.core.$ZodType, options?: toString switch (true) { default: return x satisfies never /** @deprecated */ - case tagged('promise')(x): return Warn.Deprecated('promise', 'toString')(`${z}.promise(${x._zod.def.innerType})`) + case tagged('promise')(x): return `${z}.promise(${x._zod.def.innerType})` /// leaves, a.k.a. "nullary" types case tagged('custom')(x): return `${z}.custom()` case tagged('never')(x): return `${z}.never()` diff --git a/packages/zod/test/to-type.test.ts b/packages/zod/test/to-type.test.ts index 640d6528..462fefdb 100644 --- a/packages/zod/test/to-type.test.ts +++ b/packages/zod/test/to-type.test.ts @@ -26,7 +26,6 @@ vi.describe("〖️⛳️〗‹‹‹ ❲@traversable/zod❳: zx.toType", () => " `) - vi.expect.soft(format( zx.toType( z.object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efa5c8a1..4f863905 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ catalogs: specifier: ^0.5.2 version: 0.5.5 '@sinclair/typebox': - specifier: ^0.34.38 + specifier: 0.34.38 version: 0.34.38 '@types/lodash.isequal': specifier: ^4.5.8 @@ -304,9 +304,6 @@ importers: packages/json-schema: dependencies: - '@prettier/sync': - specifier: 'catalog:' - version: 0.5.5(prettier@3.6.2) '@traversable/json-schema-types': specifier: workspace:^ version: link:../json-schema-types/dist @@ -317,6 +314,12 @@ importers: '@jsonjoy.com/util': specifier: ^1.6.0 version: 1.6.0(tslib@2.8.1) + '@prettier/sync': + specifier: 'catalog:' + version: 0.5.5(prettier@3.6.2) + '@sinclair/typebox': + specifier: 'catalog:' + version: 0.34.38 '@traversable/json-schema-test': specifier: workspace:^ version: link:../json-schema-test/dist diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f85eccd0..6bb2a646 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - examples/*/ - bin - packages/*/ + catalog: '@ark/attest': ^0.44.8 '@babel/cli': ^7.25.9 @@ -10,21 +11,20 @@ catalog: '@babel/plugin-transform-modules-commonjs': ^7.25.9 '@changesets/changelog-github': ^0.5.0 '@changesets/cli': ^2.27.9 - '@types/node': ^22.9.0 '@prettier/sync': ^0.5.2 + '@sinclair/typebox': 0.34.38 + '@types/lodash.isequal': ^4.5.8 '@types/madge': ^5.0.3 + '@types/node': ^22.9.0 '@vitest/coverage-v8': 3.2.4 '@vitest/ui': 3.2.4 + arktype: ^2.1.20 babel-plugin-annotate-pure-calls: ^0.4.0 fast-check: ^4.1.1 + lodash.isequal: ^4.5.0 madge: ^8.0.0 tinybench: ^3.0.4 typescript: 5.8.3 - vitest: 3.2.4 - - '@sinclair/typebox': ^0.34.38 - arktype: ^2.1.20 valibot: 1.1.0 + vitest: 3.2.4 zod: ^4.0.14 - '@types/lodash.isequal': ^4.5.8 - 'lodash.isequal': ^4.5.0 diff --git a/tsconfig.base.json b/tsconfig.base.json index b4c61481..1c0e3ea4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,13 +36,9 @@ "@traversable/arktype/*": ["packages/arktype/*.js"], "@traversable/json": ["packages/json/src/index.js"], "@traversable/json-schema": ["packages/json-schema/src/index.js"], - "@traversable/json-schema-test": [ - "packages/json-schema-test/src/index.js" - ], + "@traversable/json-schema-test": ["packages/json-schema-test/src/index.js"], "@traversable/json-schema-test/*": ["packages/json-schema-test/*.js"], - "@traversable/json-schema-types": [ - "packages/json-schema-types/src/index.js" - ], + "@traversable/json-schema-types": ["packages/json-schema-types/src/index.js"], "@traversable/json-schema-types/*": ["packages/json-schema-types/*.js"], "@traversable/json-schema/*": ["packages/json-schema/*.js"], "@traversable/json/*": ["packages/json/src/*.js"], @@ -53,32 +49,18 @@ "@traversable/schema-codec/*": ["packages/schema-codec/src/*.js"], "@traversable/schema-compiler": ["packages/schema-compiler/src/index.js"], "@traversable/schema-compiler/*": ["packages/schema-compiler/*.js"], - "@traversable/schema-deep-equal": [ - "packages/schema-deep-equal/src/index.js" - ], - "@traversable/schema-deep-equal/*": [ - "packages/schema-deep-equal/src/*.js" - ], + "@traversable/schema-deep-equal": ["packages/schema-deep-equal/src/index.js"], + "@traversable/schema-deep-equal/*": ["packages/schema-deep-equal/src/*.js"], "@traversable/schema-errors": ["packages/schema-errors/src/index.js"], "@traversable/schema-errors/*": ["packages/schema-errors/*.js"], "@traversable/schema-seed": ["packages/schema-seed/src/index.js"], "@traversable/schema-seed/*": ["packages/schema-seed/src/*.js"], - "@traversable/schema-to-json-schema": [ - "packages/schema-to-json-schema/src/index.js" - ], - "@traversable/schema-to-json-schema/*": [ - "packages/schema-to-json-schema/src/*.js" - ], - "@traversable/schema-to-string": [ - "packages/schema-to-string/src/index.js" - ], + "@traversable/schema-to-json-schema": ["packages/schema-to-json-schema/src/index.js"], + "@traversable/schema-to-json-schema/*": ["packages/schema-to-json-schema/src/*.js"], + "@traversable/schema-to-string": ["packages/schema-to-string/src/index.js"], "@traversable/schema-to-string/*": ["packages/schema-to-string/src/*.js"], - "@traversable/schema-to-validator": [ - "packages/schema-to-validator/src/index.js" - ], - "@traversable/schema-to-validator/*": [ - "packages/schema-to-validator/src/*.js" - ], + "@traversable/schema-to-validator": ["packages/schema-to-validator/src/index.js"], + "@traversable/schema-to-validator/*": ["packages/schema-to-validator/src/*.js"], "@traversable/schema/*": ["packages/schema/src/*.js"], "@traversable/typebox": ["packages/typebox/src/index.js"], "@traversable/typebox-test": ["packages/typebox-test/src/index.js"],