From 1ab5fe10775afac38990cf775935d639e6f5e2a5 Mon Sep 17 00:00:00 2001 From: Andrew Jarrett Date: Sat, 9 Aug 2025 22:33:16 -0500 Subject: [PATCH 1/4] feat(json-schema): implements `JsonSchema.diff` for primitive, array, record & object schemas --- packages/arktype/test/deep-equal.test.ts | 8 +- packages/json-schema-types/src/exports.ts | 1 + packages/json-schema-types/src/types.ts | 1 - packages/json-schema/src/deep-equal.ts | 78 +-- packages/json-schema/src/diff.ts | 334 ++++++++++ packages/json-schema/src/exports.ts | 1 + .../test/deep-clone.object.bench.ts | 24 - .../deep-clone.real-world-example.bench.ts | 2 + packages/json-schema/test/deep-clone.test.ts | 4 +- .../test/deep-clone.tuple.bench.ts | 32 +- packages/json-schema/test/deep-equal.test.ts | 180 ++++-- packages/json-schema/test/diff.test.ts | 605 ++++++++++++++++++ .../test/to-type.integration.test.ts | 2 +- 13 files changed, 1102 insertions(+), 170 deletions(-) create mode 100644 packages/json-schema/src/diff.ts create mode 100644 packages/json-schema/test/diff.test.ts 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-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/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..4ad475c0 --- /dev/null +++ b/packages/json-schema/src/diff.ts @@ -0,0 +1,334 @@ +import { Json } from '@traversable/json' +import { + Equal, + escape, + ident, + joinPath, + Object_keys, + Object_entries, + stringifyLiteral, +} from '@traversable/registry' +import type { Discriminated } 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 Update { type: 'update', path: (string | number)[], value: unknown } +interface Insert { type: 'insert', path: (string | number)[], value: unknown } +interface Delete { type: 'delete', path: (string | number)[] } + +type Edit = Update | Insert | Delete +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 defaultIndex = () => ({ + ...JsonSchema.defaultIndex, + bindings: new Map(), + path: [], +}) satisfies Scope + +function jsonPointerFromPath(path: (string | number)[]): unknown { + if (path.length === 0) return "/" + else if (path.length === 1 && path[0] === "") return "/" + else { + const [head, ...tail] = path + return [ + ...head === "" ? [head] : ["", head], + ...tail + ].map((_) => { + _ = _ + "" + if (_.indexOf("~") === -1 && _.indexOf("/") === -1) return _ + let chars = [..._], + out = "", + char: string | undefined + while ((char = chars.shift()) !== undefined) + out += + char === "/" ? "~1" : + char === "~" ? "~0" : + char + return escape(out) + }).join("/") + } +} + +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(l: (string | number)[], r: (string | number)[], IX: Scope) { + const X = joinPath(l, IX.isOptional) + const Y = joinPath(r, IX.isOptional) + return [ + `if (${X} !== ${Y}) {`, + ` diff.push({ type: 'update', path: \`${jsonPointerFromPath(IX.dataPath)}\`, value: ${Y} })`, + `}`, + ].join('\n') +} + +function SameNumberOrDiff(l: (string | number)[], r: (string | number)[], ix: Scope) { + const X = joinPath(l, ix.isOptional) + const Y = joinPath(r, ix.isOptional) + return [ + `if (${X} !== ${Y} && (${X} === ${X} || ${Y} === ${Y})) {`, + ` diff.push({ type: 'update', path: \`${jsonPointerFromPath(ix.dataPath)}\`, value: ${Y} })`, + `}`, + ].join('\n') +} + +function SameValueOrDiff(l: (string | number)[], r: (string | number)[], ix: Scope) { + const X = joinPath(l, ix.isOptional) + const Y = joinPath(r, ix.isOptional) + return [ + `if (!Object.is(${X}, ${Y})) {`, + ` diff.push({ type: 'update', path: \`${jsonPointerFromPath(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) } + +function createEnumDiff(x: JsonSchema.Enum): Builder { + return function diffEnum(LEFT, RIGHT, IX) { + return ( + x.enum.every((v) => typeof v === 'number') ? SameNumberOrDiff(LEFT, RIGHT, IX) + : x.enum.some((v) => typeof v === 'number') ? SameValueOrDiff(LEFT, RIGHT, IX) + : StrictlyEqualOrDiff(LEFT, RIGHT, IX) + ) + } +} + +function createArrayDiff(x: JsonSchema.Array): Builder { + return function diffArray(LEFT, RIGHT, IX) { + const LEFT_PATH = joinPath(LEFT, IX.isOptional) + const RIGHT_PATH = joinPath(RIGHT, IX.isOptional) + const LEFT_ITEM_IDENT = `${ident(LEFT_PATH, IX.bindings)}_item` + const RIGHT_ITEM_IDENT = `${ident(RIGHT_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(${LEFT_PATH}${DOT}length, ${RIGHT_PATH}${DOT}length);`, + `let ${IX_IDENT} = 0;`, + `for (; ${IX_IDENT} < ${LENGTH_IDENT}; ${IX_IDENT}++) {`, + ` const ${LEFT_ITEM_IDENT} = ${LEFT_PATH}[${IX_IDENT}];`, + ` const ${RIGHT_ITEM_IDENT} = ${RIGHT_PATH}[${IX_IDENT}];`, + x.items([LEFT_ITEM_IDENT], [RIGHT_ITEM_IDENT], { ...IX, dataPath: PATH }), + `}`, + `if (${LENGTH_IDENT} < ${LEFT_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${LEFT_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(PATH)}\` })`, + ` }`, + `}`, + `if (${LENGTH_IDENT} < ${RIGHT_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${RIGHT_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(PATH)}\`, value: ${RIGHT_PATH}[${IX_IDENT}] })`, + ` }`, + `}`, + ].join('\n') + } +} + +function createDiffOptional(continuation: Builder): Builder { + return function diffOptional(LEFT_PATH, RIGHT_PATH, IX) { + const LEFT = joinPath(LEFT_PATH, IX.isOptional) + const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) + return [ + `if (${RIGHT} === undefined && ${LEFT} !== undefined) {`, + ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(IX.dataPath)}\` })`, + `}`, + `else if (${LEFT} === undefined && ${RIGHT} !== undefined) {`, + ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(IX.dataPath)}\`, value: ${RIGHT} })`, + `}`, + `else {`, + continuation(LEFT_PATH, RIGHT_PATH, IX), + `}`, + ].join('\n') + } +} + +function createObjectDiff(x: JsonSchema.Object): Builder { + return function diffObject(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 + return [ + ...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)( + [LEFT, key], + [RIGHT, key], + index, + ) + : continuation( + [LEFT, key], + [RIGHT, key], + index + ) + }), + ].join('\n') + } +} + +function createRecordDiff(x: JsonSchema.Record): Builder { + return function diffRecord(LEFT, RIGHT, IX) { + const KEY_IDENT = ident('key', IX.bindings) + const SEEN_IDENT = ident('seen', IX.bindings) + const LEFT_PATH = joinPath(LEFT, IX.isOptional) + const RIGHT_PATH = joinPath(RIGHT, 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([`${LEFT_PATH}[${KEY_IDENT}]`], [`${RIGHT_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + '}', + ].join('\n') + }).join('\n') + const ADDITIONAL_PROPERTIES = !x.additionalProperties ? null + : [ + x.patternProperties ? 'else {' : null, + x.additionalProperties([`${LEFT_PATH}[${KEY_IDENT}]`], [`${RIGHT_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + x.patternProperties ? '}' : null, + ].filter((_) => _ !== null).join('\n') + return [ + `const ${SEEN_IDENT} = new Set()`, + `for (let ${KEY_IDENT} in ${LEFT_PATH}) {`, + ` ${SEEN_IDENT}.add(${KEY_IDENT})`, + ` if (!(${KEY_IDENT} in ${RIGHT_PATH})) {`, + ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(PATH)}\` })`, + ` continue`, + ` }`, + PATTERN_PROPERTIES, + ADDITIONAL_PROPERTIES, + `}`, + `for (let ${KEY_IDENT} in ${RIGHT_PATH}) {`, + ` if (${SEEN_IDENT}.has(${KEY_IDENT})) {`, + ` continue`, + ` }`, + ` if (!(${KEY_IDENT} in ${LEFT_PATH})) {`, + ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(PATH)}\`, value: ${RIGHT_PATH}[${KEY_IDENT}] })`, + ` continue`, + ` }`, + PATTERN_PROPERTIES, + ADDITIONAL_PROPERTIES, + `}`, + ].filter((_) => _ !== null).join('\n') + } +} + +const fold = JsonSchema.fold((x) => { + 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.isIntersection(x): return diffIntersection(x) + // case JsonSchema.isTuple(x): return diffTuple(x, input as JsonSchema.Tuple) + // case JsonSchema.isUnion(x): return diffUnion(x, input as JsonSchema.Union) + case JsonSchema.isObject(x): return createObjectDiff(x) + case JsonSchema.isUnknown(x): return diffUnknown + } +}) + +export declare namespace diff { + 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 + } +} + +diff.writeable = diff_writeable + +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}) {`, + `const diff = []`, + BODY, + `return diff;`, + `}`, + ] + : [ + options?.typeName === undefined ? null : inputType, + `function ${FUNCTION_NAME} (x${TYPE}, y${TYPE}) {`, + `const 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.test.ts b/packages/json-schema/test/diff.test.ts new file mode 100644 index 00000000..bb57e1e6 --- /dev/null +++ b/packages/json-schema/test/diff.test.ts @@ -0,0 +1,605 @@ +import * as vi from 'vitest' +import { JsonSchema } from '@traversable/json-schema' +import prettier from '@prettier/sync' + +const format = (src: string) => prettier.format(src, { parser: 'typescript', semi: false }) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Never', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { not: {} } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: never, y: never) { + const diff = [] + if (!Object.is(x, y)) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y) { + diff.push({ type: "update", path: \`/\`, value: y }) + } + 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) { + const 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({ + type: "update", + path: \`/\${ix}/street1\`, + value: y_item.street1, + }) + } + if (y_item?.street2 === undefined && x_item?.street2 !== undefined) { + diff.push({ type: "delete", path: \`/\${ix}/street2\` }) + } else if (x_item?.street2 === undefined && y_item?.street2 !== undefined) { + diff.push({ + type: "insert", + path: \`/\${ix}/street2\`, + value: y_item?.street2, + }) + } else { + if (x_item?.street2 !== y_item?.street2) { + diff.push({ + type: "update", + path: \`/\${ix}/street2\`, + value: y_item?.street2, + }) + } + } + if (x_item.city !== y_item.city) { + diff.push({ type: "update", path: \`/\${ix}/city\`, value: y_item.city }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ type: "delete", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ type: "insert", 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) { + const 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({ + type: "update", + path: \`/\${ix}/\${ix1}/\${ix2}\`, + value: y_item_item_item, + }) + } + } + if (length2 < x_item_item.length) { + for (; ix2 < x_item_item.length; ix2++) { + diff.push({ type: "delete", path: \`/\${ix}/\${ix1}/\${ix2}\` }) + } + } + if (length2 < y_item_item.length) { + for (; ix2 < y_item_item.length; ix2++) { + diff.push({ + type: "insert", + path: \`/\${ix}/\${ix1}/\${ix2}\`, + value: y_item_item[ix2], + }) + } + } + } + if (length1 < x_item.length) { + for (; ix1 < x_item.length; ix1++) { + diff.push({ type: "delete", path: \`/\${ix}/\${ix1}\` }) + } + } + if (length1 < y_item.length) { + for (; ix1 < y_item.length; ix1++) { + diff.push({ type: "insert", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) + } + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ type: "delete", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ type: "insert", 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) { + const diff = [] + if (x === y) return diff + const seen = new Set() + for (let key in x) { + seen.add(key) + if (!(key in y)) { + diff.push({ type: "delete", path: \`/\${key}\` }) + continue + } + if (/abc/.test(key)) { + if (x[key] !== y[key]) { + diff.push({ type: "update", 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({ type: "update", 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({ + type: "update", + path: \`/\${key}/\${ix}\`, + value: y_key__item, + }) + } + } + if (length < x[key].length) { + for (; ix < x[key].length; ix++) { + diff.push({ type: "delete", path: \`/\${key}/\${ix}\` }) + } + } + if (length < y[key].length) { + for (; ix < y[key].length; ix++) { + diff.push({ + type: "insert", + path: \`/\${key}/\${ix}\`, + value: y[key][ix], + }) + } + } + } + } + for (let key in y) { + if (seen.has(key)) { + continue + } + if (!(key in x)) { + diff.push({ type: "insert", path: \`/\${key}\`, value: y[key] }) + continue + } + if (/abc/.test(key)) { + if (x[key] !== y[key]) { + diff.push({ type: "update", 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({ type: "update", 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({ + type: "update", + path: \`/\${key}/\${ix}\`, + value: y_key__item, + }) + } + } + if (length < x[key].length) { + for (; ix < x[key].length; ix++) { + diff.push({ type: "delete", path: \`/\${key}/\${ix}\` }) + } + } + if (length < y[key].length) { + for (; ix < y[key].length; ix++) { + diff.push({ + type: "insert", + path: \`/\${key}/\${ix}\`, + value: y[key][ix], + }) + } + } + } + } + return diff + } + " + `) + + type Type = Record & { abc: string; def: number } + function diff(x: Type, y: Type) { + let diff = [] + let seen = new Set(['abc', 'def']) + + for (let k in x) { + seen.add(k) + if (!(k in y)) { + diff.push({ type: 'delete', path: `/${k}` }) + continue + } + if (/abc/.test(k)) { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + else if (/def/.test(k)) { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + else { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + } + + for (let k in y) { + if (seen.has(k)) { + continue + } + + if (!(k in x)) { + diff.push({ type: 'insert', path: `/${k}`, value: y[k] }) + continue + } + + if (/abc/.test(k)) { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + else if (/def/.test(k)) { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + else { + if (x[k] !== y[k]) { + diff.push({ type: 'update', path: `/${k}`, value: y[k] }) + } + } + } + } + + // function diff3( + // x: Record> & { abc: string; def: number }, + // y: Record> & { abc: string; def: number }, + // ) { + // const diff = [] + // if (x === y) return true + // const x_keys = Object.keys(x) + // const y_keys = Object.keys(y) + // const length = x_keys.length + // if (length !== y_keys.length) return false + // for (let ix = 0; ix < length; ix++) { + // const key = x_keys[ix] + // const x_value = x[key] + // const y_value = y[key] + // if (/abc/.test(key)) { + // if (x_value !== y_value) { + // diff.push({ type: "update", path: `/`, value: y_value }) + // } + // } + // if (/def/.test(key)) { + // if (x_value !== y_value && (x_value === x_value || y_value === y_value)) { + // diff.push({ type: "update", path: `/`, value: y_value }) + // } + // } + // const length1 = Math.min(x_value.length, y_value.length) + // let ix1 = 0 + // for (; ix1 < length1; ix1++) { + // const x_value1_item = x_value[ix1] + // const y_value1_item = y_value[ix1] + // if (x_value1_item !== y_value1_item) { + // diff.push({ type: "update", path: `/${ix1}`, value: y_value1_item }) + // } + // } + // if (length1 < x_value.length) { + // for (; ix1 < x_value.length; ix1++) { + // diff.push({ type: "delete", path: `/${ix1}` }) + // } + // } + // if (length1 < y_value.length) { + // for (; ix1 < y_value.length; ix1++) { + // diff.push({ type: "insert", path: `/${ix1}`, value: y_value[ix1] }) + // } + // } + // } + // 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) { + const diff = [] + if (x === y) return diff + if (x.firstName !== y.firstName) { + diff.push({ type: "update", path: \`/firstName\`, value: y.firstName }) + } + if (y?.lastName === undefined && x?.lastName !== undefined) { + diff.push({ type: "delete", path: \`/lastName\` }) + } else if (x?.lastName === undefined && y?.lastName !== undefined) { + diff.push({ type: "insert", path: \`/lastName\`, value: y?.lastName }) + } else { + if (x?.lastName !== y?.lastName) { + diff.push({ type: "update", 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({ + type: "update", + path: \`/addresses/\${ix}/street1\`, + value: y_addresses_item.street1, + }) + } + if ( + y_addresses_item?.street2 === undefined && + x_addresses_item?.street2 !== undefined + ) { + diff.push({ type: "delete", path: \`/addresses/\${ix}/street2\` }) + } else if ( + x_addresses_item?.street2 === undefined && + y_addresses_item?.street2 !== undefined + ) { + diff.push({ + type: "insert", + path: \`/addresses/\${ix}/street2\`, + value: y_addresses_item?.street2, + }) + } else { + if (x_addresses_item?.street2 !== y_addresses_item?.street2) { + diff.push({ + type: "update", + path: \`/addresses/\${ix}/street2\`, + value: y_addresses_item?.street2, + }) + } + } + if (x_addresses_item.city !== y_addresses_item.city) { + diff.push({ + type: "update", + path: \`/addresses/\${ix}/city\`, + value: y_addresses_item.city, + }) + } + } + if (length < x.addresses.length) { + for (; ix < x.addresses.length; ix++) { + diff.push({ type: "delete", path: \`/addresses/\${ix}\` }) + } + } + if (length < y.addresses.length) { + for (; ix < y.addresses.length; ix++) { + diff.push({ + type: "insert", + path: \`/addresses/\${ix}\`, + value: y.addresses[ix], + }) + } + } + return diff + } + " + `) + }) + +}) 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)) From b5ff7f2f680eefb3996228d1565d965277f45f24 Mon Sep 17 00:00:00 2001 From: Andrew Jarrett Date: Sun, 10 Aug 2025 16:17:39 -0500 Subject: [PATCH 2/4] feat(json-schema): finishes `JsonSchema.diff` core functionality --- packages/json-schema/package.json | 3 +- .../src/__generated__/__manifest__.ts | 3 +- packages/json-schema/src/diff.ts | 414 +++- packages/json-schema/test/diff.fuzz.test.ts | 68 + packages/json-schema/test/diff.test.ts | 2008 ++++++++++++++++- pnpm-lock.yaml | 11 +- pnpm-workspace.yaml | 14 +- 7 files changed, 2291 insertions(+), 230 deletions(-) create mode 100644 packages/json-schema/test/diff.fuzz.test.ts 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/diff.ts b/packages/json-schema/src/diff.ts index 4ad475c0..ead857fc 100644 --- a/packages/json-schema/src/diff.ts +++ b/packages/json-schema/src/diff.ts @@ -8,7 +8,7 @@ import { Object_entries, stringifyLiteral, } from '@traversable/registry' -import type { Discriminated } from '@traversable/json-schema-types' +import type { Discriminated, TypeName } from '@traversable/json-schema-types' import { check, JsonSchema, @@ -21,9 +21,9 @@ import { Invariant, } from '@traversable/json-schema-types' -interface Update { type: 'update', path: (string | number)[], value: unknown } -interface Insert { type: 'insert', path: (string | number)[], value: unknown } -interface Delete { type: 'delete', path: (string | number)[] } +interface Update { type: 'update', path: string, value: unknown } +interface Insert { type: 'insert', path: string, value: unknown } +interface Delete { type: 'delete', path: string } type Edit = Update | Insert | Delete type Diff = (x: T, y: T) => Edit[] @@ -37,33 +37,40 @@ export interface Scope extends JsonSchema.Index { export type Builder = (left: Path, right: Path, index: Scope) => string +const diff_unfuzzable = [ + 'union', + 'unknown', +] satisfies TypeName[] + const defaultIndex = () => ({ ...JsonSchema.defaultIndex, bindings: new Map(), path: [], }) satisfies Scope -function jsonPointerFromPath(path: (string | number)[]): unknown { - if (path.length === 0) return "/" - else if (path.length === 1 && path[0] === "") return "/" +function jsonPointer(path: (string | number)[]): string { + if (path.length === 0) return `""` + else if (path.length === 1 && path[0] === "") return `"/"` else { const [head, ...tail] = path - return [ + let out = [ ...head === "" ? [head] : ["", head], ...tail - ].map((_) => { - _ = _ + "" - if (_.indexOf("~") === -1 && _.indexOf("/") === -1) return _ - let chars = [..._], + ].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) + while ((char = chars.shift()) !== undefined) { out += char === "/" ? "~1" : char === "~" ? "~0" : char + } return escape(out) }).join("/") + return out.includes('${') ? `\`${out}\`` : `"${out}"` } } @@ -76,32 +83,32 @@ function requiresObjectIs(x: unknown): boolean { || JsonSchema.isUnknown(x) } -function StrictlyEqualOrDiff(l: (string | number)[], r: (string | number)[], IX: Scope) { - const X = joinPath(l, IX.isOptional) - const Y = joinPath(r, IX.isOptional) +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({ type: 'update', path: \`${jsonPointerFromPath(IX.dataPath)}\`, value: ${Y} })`, + ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } -function SameNumberOrDiff(l: (string | number)[], r: (string | number)[], ix: Scope) { - const X = joinPath(l, ix.isOptional) - const Y = joinPath(r, ix.isOptional) +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({ type: 'update', path: \`${jsonPointerFromPath(ix.dataPath)}\`, value: ${Y} })`, + ` diff.push({ type: "update", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } -function SameValueOrDiff(l: (string | number)[], r: (string | number)[], ix: Scope) { - const X = joinPath(l, ix.isOptional) - const Y = joinPath(r, ix.isOptional) +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({ type: 'update', path: \`${jsonPointerFromPath(ix.dataPath)}\`, value: ${Y} })`, + ` diff.push({ type: "update", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -114,129 +121,133 @@ function diffInteger(...args: Parameters) { return SameNumberOrDiff(... 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({ type: "update", 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({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + ].join('\n') + : BODY.join('\n') + } + } +}) + function createEnumDiff(x: JsonSchema.Enum): Builder { - return function diffEnum(LEFT, RIGHT, IX) { + return function diffEnum(X, Y, IX) { return ( - x.enum.every((v) => typeof v === 'number') ? SameNumberOrDiff(LEFT, RIGHT, IX) - : x.enum.some((v) => typeof v === 'number') ? SameValueOrDiff(LEFT, RIGHT, IX) - : StrictlyEqualOrDiff(LEFT, RIGHT, IX) + 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(LEFT, RIGHT, IX) { - const LEFT_PATH = joinPath(LEFT, IX.isOptional) - const RIGHT_PATH = joinPath(RIGHT, IX.isOptional) - const LEFT_ITEM_IDENT = `${ident(LEFT_PATH, IX.bindings)}_item` - const RIGHT_ITEM_IDENT = `${ident(RIGHT_PATH, IX.bindings)}_item` + 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(${LEFT_PATH}${DOT}length, ${RIGHT_PATH}${DOT}length);`, + `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 ${LEFT_ITEM_IDENT} = ${LEFT_PATH}[${IX_IDENT}];`, - ` const ${RIGHT_ITEM_IDENT} = ${RIGHT_PATH}[${IX_IDENT}];`, - x.items([LEFT_ITEM_IDENT], [RIGHT_ITEM_IDENT], { ...IX, dataPath: PATH }), + ` 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} < ${LEFT_PATH}${DOT}length) {`, - ` for(; ${IX_IDENT} < ${LEFT_PATH}${DOT}length; ${IX_IDENT}++) {`, - ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(PATH)}\` })`, + `if (${LENGTH_IDENT} < ${X_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${X_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ type: "delete", path: ${jsonPointer(PATH)} })`, ` }`, `}`, - `if (${LENGTH_IDENT} < ${RIGHT_PATH}${DOT}length) {`, - ` for(; ${IX_IDENT} < ${RIGHT_PATH}${DOT}length; ${IX_IDENT}++) {`, - ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(PATH)}\`, value: ${RIGHT_PATH}[${IX_IDENT}] })`, + `if (${LENGTH_IDENT} < ${Y_PATH}${DOT}length) {`, + ` for(; ${IX_IDENT} < ${Y_PATH}${DOT}length; ${IX_IDENT}++) {`, + ` diff.push({ type: "insert", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, ` }`, `}`, ].join('\n') } } -function createDiffOptional(continuation: Builder): Builder { - return function diffOptional(LEFT_PATH, RIGHT_PATH, IX) { - const LEFT = joinPath(LEFT_PATH, IX.isOptional) - const RIGHT = joinPath(RIGHT_PATH, IX.isOptional) - return [ - `if (${RIGHT} === undefined && ${LEFT} !== undefined) {`, - ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(IX.dataPath)}\` })`, - `}`, - `else if (${LEFT} === undefined && ${RIGHT} !== undefined) {`, - ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(IX.dataPath)}\`, value: ${RIGHT} })`, - `}`, - `else {`, - continuation(LEFT_PATH, RIGHT_PATH, IX), - `}`, - ].join('\n') - } -} - -function createObjectDiff(x: JsonSchema.Object): Builder { - return function diffObject(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 - return [ - ...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)( - [LEFT, key], - [RIGHT, key], - index, - ) - : continuation( - [LEFT, key], - [RIGHT, key], - index - ) - }), - ].join('\n') - } -} - function createRecordDiff(x: JsonSchema.Record): Builder { - return function diffRecord(LEFT, RIGHT, IX) { + return function diffRecord(X, Y, IX) { const KEY_IDENT = ident('key', IX.bindings) const SEEN_IDENT = ident('seen', IX.bindings) - const LEFT_PATH = joinPath(LEFT, IX.isOptional) - const RIGHT_PATH = joinPath(RIGHT, IX.isOptional) + 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([`${LEFT_PATH}[${KEY_IDENT}]`], [`${RIGHT_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + 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([`${LEFT_PATH}[${KEY_IDENT}]`], [`${RIGHT_PATH}[${KEY_IDENT}]`], { ...IX, dataPath: PATH }), + 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 ${LEFT_PATH}) {`, + `for (let ${KEY_IDENT} in ${X_PATH}) {`, ` ${SEEN_IDENT}.add(${KEY_IDENT})`, - ` if (!(${KEY_IDENT} in ${RIGHT_PATH})) {`, - ` diff.push({ type: 'delete', path: \`${jsonPointerFromPath(PATH)}\` })`, + ` if (!(${KEY_IDENT} in ${Y_PATH})) {`, + ` diff.push({ type: "delete", path: ${jsonPointer(PATH)} })`, ` continue`, ` }`, PATTERN_PROPERTIES, ADDITIONAL_PROPERTIES, `}`, - `for (let ${KEY_IDENT} in ${RIGHT_PATH}) {`, + `for (let ${KEY_IDENT} in ${Y_PATH}) {`, ` if (${SEEN_IDENT}.has(${KEY_IDENT})) {`, ` continue`, ` }`, - ` if (!(${KEY_IDENT} in ${LEFT_PATH})) {`, - ` diff.push({ type: 'insert', path: \`${jsonPointerFromPath(PATH)}\`, value: ${RIGHT_PATH}[${KEY_IDENT}] })`, + ` if (!(${KEY_IDENT} in ${X_PATH})) {`, + ` diff.push({ type: "insert", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, ` continue`, ` }`, PATTERN_PROPERTIES, @@ -246,10 +257,199 @@ function createRecordDiff(x: JsonSchema.Record): Builder { } } -const fold = JsonSchema.fold((x) => { +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({ type: "delete", path: ${jsonPointer(IX.dataPath)} })`, + `}`, + `else if (${X_PATH} === undefined && ${Y_PATH} !== undefined) {`, + ` diff.push({ type: "insert", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + `}`, + `else {`, + continuation(X, Y, IX), + `}`, + ].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({ type: "update", 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({ type: "update", 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({ type: "update", 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({ type: "update", 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.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 @@ -259,16 +459,16 @@ const fold = JsonSchema.fold((x) => { case JsonSchema.isEnum(x): return createEnumDiff(x) case JsonSchema.isArray(x): return createArrayDiff(x) case JsonSchema.isRecord(x): return createRecordDiff(x) - // case JsonSchema.isIntersection(x): return diffIntersection(x) - // case JsonSchema.isTuple(x): return diffTuple(x, input as JsonSchema.Tuple) - // case JsonSchema.isUnion(x): return diffUnion(x, input as JsonSchema.Union) + 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 { - type Options = toType.Options & { + export type Options = toType.Options & { /** * Configure the name of the generated diff function * @default "diff" @@ -280,9 +480,17 @@ export declare namespace diff { */ stripTypes?: boolean } + export { + Diff, + Edit, + Delete, + Insert, + Update, + } } diff.writeable = diff_writeable +diff.unfuzzable = diff_unfuzzable export function diff>(schema: S): Diff export function diff(schema: JsonSchema) { 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..88e1e339 --- /dev/null +++ b/packages/json-schema/test/diff.fuzz.test.ts @@ -0,0 +1,68 @@ +import * as vi from 'vitest' +import * as fc from 'fast-check' +import prettier from '@prettier/sync' + +import { JsonSchema } from '@traversable/json-schema' +import { deriveUnequalValue } from '@traversable/registry' +import { JsonSchemaTest } from '@traversable/json-schema-test' +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(JsonSchema.diff(schema)(left, right), null, 2)) + console.debug('oracle:', JSON.stringify(oracle(left, right), 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 }, + }) +} + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { + vi.test.skip('〖⛳️〗› ❲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(diff(left, right), oracle(left, right)) + } 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 index bb57e1e6..16cf2c59 100644 --- a/packages/json-schema/test/diff.test.ts +++ b/packages/json-schema/test/diff.test.ts @@ -1,10 +1,11 @@ import * as vi from 'vitest' import { JsonSchema } from '@traversable/json-schema' +import { Diff as oracle } from '@sinclair/typebox/value' import prettier from '@prettier/sync' const format = (src: string) => prettier.format(src, { parser: 'typescript', semi: false }) -vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.diff.writeable', () => { vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Never', () => { vi.expect.soft(format( JsonSchema.diff.writeable( @@ -15,7 +16,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: never, y: never) { const diff = [] if (!Object.is(x, y)) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -33,7 +34,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: null, y: null) { const diff = [] if (x !== y) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -51,7 +52,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: boolean, y: boolean) { const diff = [] if (x !== y) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -69,7 +70,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: number, y: number) { const diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -87,7 +88,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: number, y: number) { const diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -105,7 +106,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { "function diff(x: string, y: string) { const diff = [] if (x !== y) { - diff.push({ type: "update", path: \`/\`, value: y }) + diff.push({ type: "update", path: "", value: y }) } return diff } @@ -113,6 +114,304 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) }) + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Enum', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: never, y: never) { + const diff = [] + if (!Object.is(x, y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0, y: 0) { + const diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0, 1] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0 | 1, y: 0 | 1) { + const diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: [0, 'two'] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0 | "two", y: 0 | "two") { + const diff = [] + if (!Object.is(x, y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { enum: ['one', 'two'] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: "one" | "two", y: "one" | "two") { + const diff = [] + if (x !== y) { + diff.push({ type: "update", 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) { + const diff = [] + if (x !== y) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: false } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: false, y: false) { + const diff = [] + if (x !== y) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: 0 } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: 0, y: 0) { + const diff = [] + if (x !== y && (x === x || y === y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: 'hey' } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: "hey", y: "hey") { + const diff = [] + if (x !== y) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [], y: []) { + const diff = [] + if (x === y) return diff + if (x.length !== y.length) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [0, false] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [0, false], y: [0, false]) { + const diff = [] + if (x === y) return diff + if (x[0] !== y[0] && (x[0] === x[0] || y[0] === y[0])) { + diff.push({ type: "update", path: "/0", value: y[0] }) + } + if (x[1] !== y[1]) { + diff.push({ type: "update", path: "/1", value: y[1] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: [[], [[false]]] } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [[], [[false]]], y: [[], [[false]]]) { + const diff = [] + if (x === y) return diff + if (x[0].length !== y[0].length) { + diff.push({ type: "update", path: "/0", value: y[0] }) + } + if (x[1][0][0] !== y[1][0][0]) { + diff.push({ type: "update", path: "/1/0/0", value: y[1][0][0] }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { const: {} } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: {}, y: {}) { + const diff = [] + if (x === y) return diff + if (Object.keys(x).length !== Object.keys(y).length) { + diff.push({ type: "update", 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 }) { + const diff = [] + if (x === y) return diff + if (x.abc !== y.abc && (x.abc === x.abc || y.abc === y.abc)) { + diff.push({ type: "update", path: "/abc", value: y.abc }) + } + if (x.def !== y.def) { + diff.push({ type: "update", 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 }>, + ) { + const 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({ type: "update", path: \`/\${ix}/abc\`, value: y_item.abc }) + } + if (x_item.def !== y_item.def) { + diff.push({ type: "update", path: \`/\${ix}/def\`, value: y_item.def }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ type: "delete", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + } + } + return diff + } + " + `) + + }) + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Array', () => { vi.expect.soft(format( JsonSchema.diff.writeable( @@ -387,109 +686,6 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { " `) - type Type = Record & { abc: string; def: number } - function diff(x: Type, y: Type) { - let diff = [] - let seen = new Set(['abc', 'def']) - - for (let k in x) { - seen.add(k) - if (!(k in y)) { - diff.push({ type: 'delete', path: `/${k}` }) - continue - } - if (/abc/.test(k)) { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - else if (/def/.test(k)) { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - else { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - } - - for (let k in y) { - if (seen.has(k)) { - continue - } - - if (!(k in x)) { - diff.push({ type: 'insert', path: `/${k}`, value: y[k] }) - continue - } - - if (/abc/.test(k)) { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - else if (/def/.test(k)) { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - else { - if (x[k] !== y[k]) { - diff.push({ type: 'update', path: `/${k}`, value: y[k] }) - } - } - } - } - - // function diff3( - // x: Record> & { abc: string; def: number }, - // y: Record> & { abc: string; def: number }, - // ) { - // const diff = [] - // if (x === y) return true - // const x_keys = Object.keys(x) - // const y_keys = Object.keys(y) - // const length = x_keys.length - // if (length !== y_keys.length) return false - // for (let ix = 0; ix < length; ix++) { - // const key = x_keys[ix] - // const x_value = x[key] - // const y_value = y[key] - // if (/abc/.test(key)) { - // if (x_value !== y_value) { - // diff.push({ type: "update", path: `/`, value: y_value }) - // } - // } - // if (/def/.test(key)) { - // if (x_value !== y_value && (x_value === x_value || y_value === y_value)) { - // diff.push({ type: "update", path: `/`, value: y_value }) - // } - // } - // const length1 = Math.min(x_value.length, y_value.length) - // let ix1 = 0 - // for (; ix1 < length1; ix1++) { - // const x_value1_item = x_value[ix1] - // const y_value1_item = y_value[ix1] - // if (x_value1_item !== y_value1_item) { - // diff.push({ type: "update", path: `/${ix1}`, value: y_value1_item }) - // } - // } - // if (length1 < x_value.length) { - // for (; ix1 < x_value.length; ix1++) { - // diff.push({ type: "delete", path: `/${ix1}` }) - // } - // } - // if (length1 < y_value.length) { - // for (; ix1 < y_value.length; ix1++) { - // diff.push({ type: "insert", path: `/${ix1}`, value: y_value[ix1] }) - // } - // } - // } - // return diff - // } - }) vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Object', () => { @@ -528,15 +724,15 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { const diff = [] if (x === y) return diff if (x.firstName !== y.firstName) { - diff.push({ type: "update", path: \`/firstName\`, value: y.firstName }) + diff.push({ type: "update", path: "/firstName", value: y.firstName }) } if (y?.lastName === undefined && x?.lastName !== undefined) { - diff.push({ type: "delete", path: \`/lastName\` }) + diff.push({ type: "delete", path: "/lastName" }) } else if (x?.lastName === undefined && y?.lastName !== undefined) { - diff.push({ type: "insert", path: \`/lastName\`, value: y?.lastName }) + diff.push({ type: "insert", path: "/lastName", value: y?.lastName }) } else { if (x?.lastName !== y?.lastName) { - diff.push({ type: "update", path: \`/lastName\`, value: y?.lastName }) + diff.push({ type: "update", path: "/lastName", value: y?.lastName }) } } const length = Math.min(x.addresses.length, y.addresses.length) @@ -602,4 +798,1588 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { `) }) + vi.test('〖️⛳️〗› ❲JsonSchema.diff.writeable❳: JsonSchema.Tuple', () => { + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [], + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [], y: []) { + const diff = [] + if (x === y) return diff + if (x.length !== y.length) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + type: 'array', + prefixItems: [ + { type: 'string' } + ], + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: [string], y: [string]) { + const diff = [] + if (x === y) return diff + if (x[0] !== y[0]) { + diff.push({ type: "update", 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]) { + const diff = [] + if (x === y) return diff + if (x[0] !== y[0]) { + diff.push({ type: "update", path: "/0", value: y[0] }) + } + if (x[1] !== y[1] && (x[1] === x[1] || y[1] === y[1])) { + diff.push({ type: "update", 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]]], + ) { + const diff = [] + if (x === y) return diff + if (x[0][0] !== y[0][0]) { + diff.push({ type: "update", path: "/0/0", value: y[0][0] }) + } + if (x[1][0][0] !== y[1][0][0]) { + diff.push({ type: "update", 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({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (!Object.is(x, y)) { + diff.push({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ type: "update", 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) { + const 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({ type: "update", path: "", value: y }) + } + } else if (typeof x === "string" && typeof y === "string") { + if (x !== y) { + diff.push({ type: "update", 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({ type: "update", path: \`/\${ix}\`, value: y_item }) + } + } + if (length < x.length) { + for (; ix < x.length; ix++) { + diff.push({ type: "delete", path: \`/\${ix}\` }) + } + } + if (length < y.length) { + for (; ix < y.length; ix++) { + diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + } + } + } else if (check1(x) && check1(y)) { + if (x.street1 !== y.street1) { + diff.push({ type: "update", path: "/street1", value: y.street1 }) + } + if (y?.street2 === undefined && x?.street2 !== undefined) { + diff.push({ type: "delete", path: "/street2" }) + } else if (x?.street2 === undefined && y?.street2 !== undefined) { + diff.push({ type: "insert", path: "/street2", value: y?.street2 }) + } else { + if (x?.street2 !== y?.street2) { + diff.push({ type: "update", path: "/street2", value: y?.street2 }) + } + } + if (x.city !== y.city) { + diff.push({ type: "update", path: "/city", value: y.city }) + } + } else { + diff.push({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (x.tag === "A" && y.tag === "A") { + if (x.tag !== y.tag) { + diff.push({ type: "update", path: "/tag", value: y.tag }) + } + if (x.onA !== y.onA && (x.onA === x.onA || y.onA === y.onA)) { + diff.push({ type: "update", path: "/onA", value: y.onA }) + } + } else if (x.tag === "B" && y.tag === "B") { + if (x.tag !== y.tag) { + diff.push({ type: "update", path: "/tag", value: y.tag }) + } + if (x.onB !== y.onB) { + diff.push({ type: "update", path: "/onB", value: y.onB }) + } + } else { + diff.push({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (!Object.is(x, y)) { + diff.push({ type: "update", path: "", value: y }) + } + return diff + } + " + `) + + vi.expect.soft(format( + JsonSchema.diff.writeable( + { + allOf: [ + { type: 'string' } + ] + } + ) + )).toMatchInlineSnapshot + (` + "function diff(x: string, y: string) { + const diff = [] + if (x === y) return diff + if (x !== y) { + diff.push({ type: "update", 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) { + const diff = [] + if (x === y) return diff + if (x.abc !== y.abc) { + diff.push({ type: "update", path: "/abc", value: y.abc }) + } + if (y?.def === undefined && x?.def !== undefined) { + diff.push({ type: "delete", path: "/def" }) + } else if (x?.def === undefined && y?.def !== undefined) { + diff.push({ type: "insert", path: "/def", value: y?.def }) + } else { + if (x?.def !== y?.def && (x?.def === x?.def || y?.def === y?.def)) { + diff.push({ type: "update", path: "/def", value: y?.def }) + } + } + if (x.ghi !== y.ghi && (x.ghi === x.ghi || y.ghi === y.ghi)) { + diff.push({ type: "update", path: "/ghi", value: y.ghi }) + } + if (x.jkl !== y.jkl) { + diff.push({ type: "update", path: "/jkl", value: y.jkl }) + } + if (y?.def === undefined && x?.def !== undefined) { + diff.push({ type: "delete", path: "/def" }) + } else if (x?.def === undefined && y?.def !== undefined) { + diff.push({ type: "insert", path: "/def", value: y?.def }) + } else { + if (x?.def !== y?.def) { + diff.push({ type: "update", 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({ type: "update", path: \`/pqr/\${ix}\`, value: y_pqr_item }) + } + } + if (length < x.pqr.length) { + for (; ix < x.pqr.length; ix++) { + diff.push({ type: "delete", path: \`/pqr/\${ix}\` }) + } + } + if (length < y.pqr.length) { + for (; ix < y.pqr.length; ix++) { + diff.push({ type: "insert", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) + } + } + return diff + } + " + `) + }) + +}) + +vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.diff', () => { + const sort = (x: JsonSchema.diff.Edit, y: JsonSchema.diff.Edit) => + x.path < y.path ? -1 : y.path < x.path ? 1 : 0 + + vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Never', () => { + const diff_1 = JsonSchema.diff( + { not: {} } + ) + + 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 = JsonSchema.diff( + { type: 'null' } + ) + + 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 = JsonSchema.diff( + { type: 'boolean' } + ) + + 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 = JsonSchema.diff( + { type: 'integer' } + ) + + 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 = JsonSchema.diff( + { type: 'number' } + ) + + 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 = JsonSchema.diff( + { type: 'string' } + ) + + 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 = JsonSchema.diff( + { enum: ['A', 'B'] } + ) + + 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 = JsonSchema.diff( + { const: null } + ) + + 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 = JsonSchema.diff( + { const: 0 } + ) + + 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 = JsonSchema.diff( + { const: false } + ) + + 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 = JsonSchema.diff( + { const: [] } + ) + + 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 = JsonSchema.diff( + { const: [1] } + ) + + 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 = JsonSchema.diff( + { const: { a: 1 } } + ) + + 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 = JsonSchema.diff( + { const: { a: 1, b: 2 } } + ) + + 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 = JsonSchema.diff( + { + type: 'array', + items: { type: 'string' } + } + ) + + 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 = JsonSchema.diff( + { + type: 'object', + additionalProperties: { + type: 'array', + items: { type: 'string' } + }, + patternProperties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ) + + 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 = JsonSchema.diff( + { + type: 'object', + required: ['abc', 'def'], + properties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ) + + 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 = JsonSchema.diff( + { + type: 'array', + prefixItems: [], + } + ) + + 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 = JsonSchema.diff( + { + type: 'array', + prefixItems: [ + { type: 'string' } + ], + } + ) + + 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 = JsonSchema.diff( + { + anyOf: [], + } + ) + + 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 = JsonSchema.diff( + { + anyOf: [ + { type: 'string' } + ], + } + ) + + 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 = JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + ], + } + ) + + 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 = JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + { + type: 'array', + items: { type: 'string' } + }, + ], + } + ) + + 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 = JsonSchema.diff( + { + anyOf: [ + { + type: 'array', + items: { type: 'string' } + }, + { + type: 'object', + required: ['abc'], + properties: { + abc: { type: 'string' }, + def: { type: 'number' } + } + } + ], + } + ) + + 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 = 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' } + } + } + ], + } + ) + + 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 = JsonSchema.diff( + { + allOf: [], + } + ) + + 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 = 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' }, + } + } + ], + } + ) + + 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/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 From 05fcd99bb152479ff7520b562f251be0e52c3c79 Mon Sep 17 00:00:00 2001 From: Andrew Jarrett Date: Mon, 11 Aug 2025 12:20:53 -0500 Subject: [PATCH 3/4] chore: cleanup --- .../json-schema-test/src/generator-options.ts | 49 -- packages/json-schema/src/diff.ts | 62 +- packages/json-schema/test/diff.fuzz.test.ts | 114 ++- packages/json-schema/test/diff.test.ts | 720 ++++++++++-------- packages/zod/src/to-string.ts | 2 +- packages/zod/test/to-type.test.ts | 1 - tsconfig.base.json | 36 +- 7 files changed, 558 insertions(+), 426 deletions(-) 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/src/diff.ts b/packages/json-schema/src/diff.ts index ead857fc..a8857cfb 100644 --- a/packages/json-schema/src/diff.ts +++ b/packages/json-schema/src/diff.ts @@ -21,11 +21,11 @@ import { Invariant, } from '@traversable/json-schema-types' -interface Update { type: 'update', path: string, value: unknown } -interface Insert { type: 'insert', path: string, value: unknown } -interface Delete { type: 'delete', path: string } +interface Add { type: 'add', path: string, value: unknown } +interface Replace { type: 'replace', path: string, value: unknown } +interface Remove { type: 'remove', path: string } -type Edit = Update | Insert | Delete +type Edit = Add | Replace | Remove type Diff = (x: T, y: T) => Edit[] export type Path = (string | number)[] @@ -38,6 +38,7 @@ export interface Scope extends JsonSchema.Index { export type Builder = (left: Path, right: Path, index: Scope) => string const diff_unfuzzable = [ + 'never', 'union', 'unknown', ] satisfies TypeName[] @@ -83,12 +84,12 @@ function requiresObjectIs(x: unknown): boolean { || 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) +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({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -98,7 +99,7 @@ function SameNumberOrDiff(x: (string | number)[], y: (string | number)[], ix: Sc const Y = joinPath(y, ix.isOptional) return [ `if (${X} !== ${Y} && (${X} === ${X} || ${Y} === ${Y})) {`, - ` diff.push({ type: "update", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -108,7 +109,7 @@ function SameValueOrDiff(x: (string | number)[], y: (string | number)[], ix: Sco const Y = joinPath(y, ix.isOptional) return [ `if (!Object.is(${X}, ${Y})) {`, - ` diff.push({ type: "update", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -135,7 +136,7 @@ const foldJson = Json.fold((x) => { return x.length === 0 ? [ `if (${X_PATH}.length !== ${Y_PATH}.length) {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : x.map( @@ -159,7 +160,7 @@ const foldJson = Json.fold((x) => { return BODY.length === 0 ? [ `if (Object.keys(${X_PATH}).length !== Object.keys(${Y_PATH}).length) {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : BODY.join('\n') @@ -197,12 +198,12 @@ function createArrayDiff(x: JsonSchema.Array): Builder { `}`, `if (${LENGTH_IDENT} < ${X_PATH}${DOT}length) {`, ` for(; ${IX_IDENT} < ${X_PATH}${DOT}length; ${IX_IDENT}++) {`, - ` diff.push({ type: "delete", path: ${jsonPointer(PATH)} })`, + ` diff.push({ type: "remove", path: ${jsonPointer(PATH)} })`, ` }`, `}`, `if (${LENGTH_IDENT} < ${Y_PATH}${DOT}length) {`, ` for(; ${IX_IDENT} < ${Y_PATH}${DOT}length; ${IX_IDENT}++) {`, - ` diff.push({ type: "insert", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, + ` diff.push({ type: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, ` }`, `}`, ].join('\n') @@ -236,7 +237,7 @@ function createRecordDiff(x: JsonSchema.Record): Builder { `for (let ${KEY_IDENT} in ${X_PATH}) {`, ` ${SEEN_IDENT}.add(${KEY_IDENT})`, ` if (!(${KEY_IDENT} in ${Y_PATH})) {`, - ` diff.push({ type: "delete", path: ${jsonPointer(PATH)} })`, + ` diff.push({ type: "remove", path: ${jsonPointer(PATH)} })`, ` continue`, ` }`, PATTERN_PROPERTIES, @@ -247,7 +248,7 @@ function createRecordDiff(x: JsonSchema.Record): Builder { ` continue`, ` }`, ` if (!(${KEY_IDENT} in ${X_PATH})) {`, - ` diff.push({ type: "insert", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, + ` diff.push({ type: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, ` continue`, ` }`, PATTERN_PROPERTIES, @@ -263,13 +264,13 @@ function createDiffOptional(continuation: Builder): Builder { const Y_PATH = joinPath(Y, IX.isOptional) return [ `if (${Y_PATH} === undefined && ${X_PATH} !== undefined) {`, - ` diff.push({ type: "delete", path: ${jsonPointer(IX.dataPath)} })`, + ` diff.push({ type: "remove", path: ${jsonPointer(IX.dataPath)} })`, `}`, `else if (${X_PATH} === undefined && ${Y_PATH} !== undefined) {`, - ` diff.push({ type: "insert", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "add", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, - `else {`, - continuation(X, Y, IX), + `else if (${X_PATH} !== undefined && ${Y_PATH} !== undefined) {`, + continuation([X_PATH], [Y_PATH], { ...IX, isOptional: false }), `}`, ].join('\n') } @@ -298,7 +299,7 @@ function createObjectDiff(x: JsonSchema.Object): Builder { return BODY.length === 0 ? [ `if (${X_PATH} !== ${Y_PATH}) {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : BODY.join('\n') @@ -312,7 +313,7 @@ function createTupleDiff(x: JsonSchema.Tuple): Builder { if (x.prefixItems.length === 0) { return [ `if (${X_PATH}.length !== ${Y_PATH}.length) {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') } @@ -413,7 +414,7 @@ function createInclusiveUnionDiff( ...PREDICATES.map((_) => _ === null ? null : _.PREDICATE), CHECKS.join('\n'), `else {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].filter((_) => _ !== null).join('\n') } @@ -440,7 +441,7 @@ function createExclusiveUnionDiff( ].join('\n') }), `else {`, - ` diff.push({ type: "update", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') } @@ -483,9 +484,12 @@ export declare namespace diff { export { Diff, Edit, - Delete, - Insert, - Update, + // Delete, + // Insert, + // Update, + Add, + Replace, + Remove, } } @@ -524,7 +528,7 @@ function diff_writeable(schema: JsonSchema, options?: diff.Options): string { ? [ options?.typeName === undefined ? null : inputType, `function ${FUNCTION_NAME} (x${TYPE}, y${TYPE}) {`, - `const diff = []`, + `let diff = []`, BODY, `return diff;`, `}`, @@ -532,7 +536,7 @@ function diff_writeable(schema: JsonSchema, options?: diff.Options): string { : [ options?.typeName === undefined ? null : inputType, `function ${FUNCTION_NAME} (x${TYPE}, y${TYPE}) {`, - `const diff = []`, + `let diff = []`, ROOT_DIFF, BODY, `return diff;`, diff --git a/packages/json-schema/test/diff.fuzz.test.ts b/packages/json-schema/test/diff.fuzz.test.ts index 88e1e339..e5049f66 100644 --- a/packages/json-schema/test/diff.fuzz.test.ts +++ b/packages/json-schema/test/diff.fuzz.test.ts @@ -5,6 +5,7 @@ import prettier from '@prettier/sync' import { JsonSchema } from '@traversable/json-schema' import { deriveUnequalValue } from '@traversable/registry' 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 }) @@ -21,8 +22,8 @@ function logger({ schema, left, right, error }: LoggerDeps) { 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(JsonSchema.diff(schema)(left, right), null, 2)) - console.debug('oracle:', JSON.stringify(oracle(left, right), null, 2)) + 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() @@ -41,8 +42,28 @@ const Builder = { }) } +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.type](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.skip('〖⛳️〗› ❲JsonSchema.diff❳: equal data (additionalProperties only)', () => { + vi.test('〖⛳️〗› ❲JsonSchema.diff❳: equal data (additionalProperties only)', () => { fc.assert( fc.property( Builder.additionalProperties['*'], @@ -53,7 +74,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { const duplicate = fc.clone(arbitrary, 2) const [left, right] = fc.sample(duplicate, 1)[0] try { - vi.assert.deepEqual(diff(left, right), oracle(left, right)) + 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)') @@ -62,7 +86,87 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳', () => { ), { endOnFailure: true, examples: [], - numRuns: 10_000, + // 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 index 16cf2c59..39ff82ec 100644 --- a/packages/json-schema/test/diff.test.ts +++ b/packages/json-schema/test/diff.test.ts @@ -1,10 +1,32 @@ import * as vi from 'vitest' -import { JsonSchema } from '@traversable/json-schema' +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.type](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( @@ -14,9 +36,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: never, y: never) { - const diff = [] + let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -32,9 +54,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: null, y: null) { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -50,9 +72,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: boolean, y: boolean) { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -68,9 +90,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: number, y: number) { - const diff = [] + let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -86,9 +108,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: number, y: number) { - const diff = [] + let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -104,9 +126,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: string, y: string) { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -122,9 +144,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: never, y: never) { - const diff = [] + let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -138,9 +160,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: 0, y: 0) { - const diff = [] + let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -154,9 +176,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: 0 | 1, y: 0 | 1) { - const diff = [] + let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -170,9 +192,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: 0 | "two", y: 0 | "two") { - const diff = [] + let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -186,9 +208,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: "one" | "two", y: "one" | "two") { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -204,9 +226,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: null, y: null) { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -220,9 +242,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: false, y: false) { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -236,9 +258,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: 0, y: 0) { - const diff = [] + let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -252,9 +274,9 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: "hey", y: "hey") { - const diff = [] + let diff = [] if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -268,10 +290,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [], y: []) { - const diff = [] + let diff = [] if (x === y) return diff if (x.length !== y.length) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -285,13 +307,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [0, false], y: [0, false]) { - const diff = [] + let diff = [] if (x === y) return diff if (x[0] !== y[0] && (x[0] === x[0] || y[0] === y[0])) { - diff.push({ type: "update", path: "/0", value: y[0] }) + diff.push({ type: "replace", path: "/0", value: y[0] }) } if (x[1] !== y[1]) { - diff.push({ type: "update", path: "/1", value: y[1] }) + diff.push({ type: "replace", path: "/1", value: y[1] }) } return diff } @@ -305,13 +327,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [[], [[false]]], y: [[], [[false]]]) { - const diff = [] + let diff = [] if (x === y) return diff if (x[0].length !== y[0].length) { - diff.push({ type: "update", path: "/0", value: y[0] }) + diff.push({ type: "replace", path: "/0", value: y[0] }) } if (x[1][0][0] !== y[1][0][0]) { - diff.push({ type: "update", path: "/1/0/0", value: y[1][0][0] }) + diff.push({ type: "replace", path: "/1/0/0", value: y[1][0][0] }) } return diff } @@ -325,10 +347,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: {}, y: {}) { - const diff = [] + let diff = [] if (x === y) return diff if (Object.keys(x).length !== Object.keys(y).length) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -347,13 +369,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: { abc: 123; def: false }, y: { abc: 123; def: false }) { - const diff = [] + let diff = [] if (x === y) return diff if (x.abc !== y.abc && (x.abc === x.abc || y.abc === y.abc)) { - diff.push({ type: "update", path: "/abc", value: y.abc }) + diff.push({ type: "replace", path: "/abc", value: y.abc }) } if (x.def !== y.def) { - diff.push({ type: "update", path: "/def", value: y.def }) + diff.push({ type: "replace", path: "/def", value: y.def }) } return diff } @@ -378,7 +400,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema x: Array<{ abc: 123; def: false }>, y: Array<{ abc: 123; def: false }>, ) { - const diff = [] + let diff = [] if (x === y) return diff const length = Math.min(x.length, y.length) let ix = 0 @@ -389,20 +411,20 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema x_item.abc !== y_item.abc && (x_item.abc === x_item.abc || y_item.abc === y_item.abc) ) { - diff.push({ type: "update", path: \`/\${ix}/abc\`, value: y_item.abc }) + diff.push({ type: "replace", path: \`/\${ix}/abc\`, value: y_item.abc }) } if (x_item.def !== y_item.def) { - diff.push({ type: "update", path: \`/\${ix}/def\`, value: y_item.def }) + diff.push({ type: "replace", path: \`/\${ix}/def\`, value: y_item.def }) } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "delete", path: \`/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -433,7 +455,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = Array<{ street1: string; street2?: string; city: string }> function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff const length = Math.min(x.length, y.length) let ix = 0 @@ -442,40 +464,36 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_item = y[ix] if (x_item.street1 !== y_item.street1) { diff.push({ - type: "update", + type: "replace", path: \`/\${ix}/street1\`, value: y_item.street1, }) } if (y_item?.street2 === undefined && x_item?.street2 !== undefined) { - diff.push({ type: "delete", path: \`/\${ix}/street2\` }) + diff.push({ type: "remove", path: \`/\${ix}/street2\` }) } else if (x_item?.street2 === undefined && y_item?.street2 !== undefined) { - diff.push({ - type: "insert", - path: \`/\${ix}/street2\`, - value: y_item?.street2, - }) - } else { + diff.push({ type: "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({ - type: "update", + type: "replace", path: \`/\${ix}/street2\`, value: y_item?.street2, }) } } if (x_item.city !== y_item.city) { - diff.push({ type: "update", path: \`/\${ix}/city\`, value: y_item.city }) + diff.push({ type: "replace", path: \`/\${ix}/city\`, value: y_item.city }) } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "delete", path: \`/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -501,7 +519,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = Array>> function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff const length = Math.min(x.length, y.length) let ix = 0 @@ -520,7 +538,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_item_item_item = y_item_item[ix2] if (x_item_item_item !== y_item_item_item) { diff.push({ - type: "update", + type: "replace", path: \`/\${ix}/\${ix1}/\${ix2}\`, value: y_item_item_item, }) @@ -528,13 +546,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length2 < x_item_item.length) { for (; ix2 < x_item_item.length; ix2++) { - diff.push({ type: "delete", path: \`/\${ix}/\${ix1}/\${ix2}\` }) + diff.push({ type: "remove", path: \`/\${ix}/\${ix1}/\${ix2}\` }) } } if (length2 < y_item_item.length) { for (; ix2 < y_item_item.length; ix2++) { diff.push({ - type: "insert", + type: "add", path: \`/\${ix}/\${ix1}/\${ix2}\`, value: y_item_item[ix2], }) @@ -543,23 +561,23 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length1 < x_item.length) { for (; ix1 < x_item.length; ix1++) { - diff.push({ type: "delete", path: \`/\${ix}/\${ix1}\` }) + diff.push({ type: "remove", path: \`/\${ix}/\${ix1}\` }) } } if (length1 < y_item.length) { for (; ix1 < y_item.length; ix1++) { - diff.push({ type: "insert", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) + diff.push({ type: "add", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) } } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "delete", path: \`/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -588,22 +606,22 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = Record> & { abc: string; def: number } function diff(x: Type, y: Type) { - const diff = [] + 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({ type: "delete", path: \`/\${key}\` }) + diff.push({ type: "remove", path: \`/\${key}\` }) continue } if (/abc/.test(key)) { if (x[key] !== y[key]) { - diff.push({ type: "update", path: \`/\${key}\`, value: y[key] }) + diff.push({ type: "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({ type: "update", path: \`/\${key}\`, value: y[key] }) + diff.push({ type: "replace", path: \`/\${key}\`, value: y[key] }) } } else { const length = Math.min(x[key].length, y[key].length) @@ -613,7 +631,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_key__item = y[key][ix] if (x_key__item !== y_key__item) { diff.push({ - type: "update", + type: "replace", path: \`/\${key}/\${ix}\`, value: y_key__item, }) @@ -621,16 +639,12 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x[key].length) { for (; ix < x[key].length; ix++) { - diff.push({ type: "delete", path: \`/\${key}/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${key}/\${ix}\` }) } } if (length < y[key].length) { for (; ix < y[key].length; ix++) { - diff.push({ - type: "insert", - path: \`/\${key}/\${ix}\`, - value: y[key][ix], - }) + diff.push({ type: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) } } } @@ -640,16 +654,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema continue } if (!(key in x)) { - diff.push({ type: "insert", path: \`/\${key}\`, value: y[key] }) + diff.push({ type: "add", path: \`/\${key}\`, value: y[key] }) continue } if (/abc/.test(key)) { if (x[key] !== y[key]) { - diff.push({ type: "update", path: \`/\${key}\`, value: y[key] }) + diff.push({ type: "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({ type: "update", path: \`/\${key}\`, value: y[key] }) + diff.push({ type: "replace", path: \`/\${key}\`, value: y[key] }) } } else { const length = Math.min(x[key].length, y[key].length) @@ -659,7 +673,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_key__item = y[key][ix] if (x_key__item !== y_key__item) { diff.push({ - type: "update", + type: "replace", path: \`/\${key}/\${ix}\`, value: y_key__item, }) @@ -667,16 +681,12 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x[key].length) { for (; ix < x[key].length; ix++) { - diff.push({ type: "delete", path: \`/\${key}/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${key}/\${ix}\` }) } } if (length < y[key].length) { for (; ix < y[key].length; ix++) { - diff.push({ - type: "insert", - path: \`/\${key}/\${ix}\`, - value: y[key][ix], - }) + diff.push({ type: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) } } } @@ -721,18 +731,18 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema addresses: Array<{ street1: string; street2?: string; city: string }> } function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (x.firstName !== y.firstName) { - diff.push({ type: "update", path: "/firstName", value: y.firstName }) + diff.push({ type: "replace", path: "/firstName", value: y.firstName }) } if (y?.lastName === undefined && x?.lastName !== undefined) { - diff.push({ type: "delete", path: "/lastName" }) + diff.push({ type: "remove", path: "/lastName" }) } else if (x?.lastName === undefined && y?.lastName !== undefined) { - diff.push({ type: "insert", path: "/lastName", value: y?.lastName }) - } else { + diff.push({ type: "add", path: "/lastName", value: y?.lastName }) + } else if (x?.lastName !== undefined && y?.lastName !== undefined) { if (x?.lastName !== y?.lastName) { - diff.push({ type: "update", path: "/lastName", value: y?.lastName }) + diff.push({ type: "replace", path: "/lastName", value: y?.lastName }) } } const length = Math.min(x.addresses.length, y.addresses.length) @@ -742,7 +752,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_addresses_item = y.addresses[ix] if (x_addresses_item.street1 !== y_addresses_item.street1) { diff.push({ - type: "update", + type: "replace", path: \`/addresses/\${ix}/street1\`, value: y_addresses_item.street1, }) @@ -751,20 +761,23 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema y_addresses_item?.street2 === undefined && x_addresses_item?.street2 !== undefined ) { - diff.push({ type: "delete", path: \`/addresses/\${ix}/street2\` }) + diff.push({ type: "remove", path: \`/addresses/\${ix}/street2\` }) } else if ( x_addresses_item?.street2 === undefined && y_addresses_item?.street2 !== undefined ) { diff.push({ - type: "insert", + type: "add", path: \`/addresses/\${ix}/street2\`, value: y_addresses_item?.street2, }) - } else { + } else if ( + x_addresses_item?.street2 !== undefined && + y_addresses_item?.street2 !== undefined + ) { if (x_addresses_item?.street2 !== y_addresses_item?.street2) { diff.push({ - type: "update", + type: "replace", path: \`/addresses/\${ix}/street2\`, value: y_addresses_item?.street2, }) @@ -772,7 +785,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (x_addresses_item.city !== y_addresses_item.city) { diff.push({ - type: "update", + type: "replace", path: \`/addresses/\${ix}/city\`, value: y_addresses_item.city, }) @@ -780,13 +793,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x.addresses.length) { for (; ix < x.addresses.length; ix++) { - diff.push({ type: "delete", path: \`/addresses/\${ix}\` }) + diff.push({ type: "remove", path: \`/addresses/\${ix}\` }) } } if (length < y.addresses.length) { for (; ix < y.addresses.length; ix++) { diff.push({ - type: "insert", + type: "add", path: \`/addresses/\${ix}\`, value: y.addresses[ix], }) @@ -809,10 +822,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [], y: []) { - const diff = [] + let diff = [] if (x === y) return diff if (x.length !== y.length) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -831,10 +844,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [string], y: [string]) { - const diff = [] + let diff = [] if (x === y) return diff if (x[0] !== y[0]) { - diff.push({ type: "update", path: "/0", value: y[0] }) + diff.push({ type: "replace", path: "/0", value: y[0] }) } return diff } @@ -854,13 +867,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: [string, number], y: [string, number]) { - const diff = [] + let diff = [] if (x === y) return diff if (x[0] !== y[0]) { - diff.push({ type: "update", path: "/0", value: y[0] }) + diff.push({ type: "replace", path: "/0", value: y[0] }) } if (x[1] !== y[1] && (x[1] === x[1] || y[1] === y[1])) { - diff.push({ type: "update", path: "/1", value: y[1] }) + diff.push({ type: "replace", path: "/1", value: y[1] }) } return diff } @@ -899,19 +912,19 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema x: [[string], [[string, number]]], y: [[string], [[string, number]]], ) { - const diff = [] + let diff = [] if (x === y) return diff if (x[0][0] !== y[0][0]) { - diff.push({ type: "update", path: "/0/0", value: y[0][0] }) + diff.push({ type: "replace", path: "/0/0", value: y[0][0] }) } if (x[1][0][0] !== y[1][0][0]) { - diff.push({ type: "update", path: "/1/0/0", value: y[1][0][0] }) + diff.push({ type: "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({ type: "update", path: "/1/0/1", value: y[1][0][1] }) + diff.push({ type: "replace", path: "/1/0/1", value: y[1][0][1] }) } return diff } @@ -932,10 +945,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = never function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (!Object.is(x, y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -955,10 +968,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = string function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -998,7 +1011,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema | Array | { street1: string; street2?: string; city: string } function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (Object.is(x, y)) return diff function check(value) { return ( @@ -1016,11 +1029,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (typeof x === "number" && typeof y === "number") { if (x !== y && (x === x || y === y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } } else if (typeof x === "string" && typeof y === "string") { if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } } else if (check(x) && check(y)) { const length = Math.min(x.length, y.length) @@ -1029,37 +1042,37 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema 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({ type: "update", path: \`/\${ix}\`, value: y_item }) + diff.push({ type: "replace", path: \`/\${ix}\`, value: y_item }) } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "delete", path: \`/\${ix}\` }) + diff.push({ type: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "insert", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) } } } else if (check1(x) && check1(y)) { if (x.street1 !== y.street1) { - diff.push({ type: "update", path: "/street1", value: y.street1 }) + diff.push({ type: "replace", path: "/street1", value: y.street1 }) } if (y?.street2 === undefined && x?.street2 !== undefined) { - diff.push({ type: "delete", path: "/street2" }) + diff.push({ type: "remove", path: "/street2" }) } else if (x?.street2 === undefined && y?.street2 !== undefined) { - diff.push({ type: "insert", path: "/street2", value: y?.street2 }) - } else { + diff.push({ type: "add", path: "/street2", value: y?.street2 }) + } else if (x?.street2 !== undefined && y?.street2 !== undefined) { if (x?.street2 !== y?.street2) { - diff.push({ type: "update", path: "/street2", value: y?.street2 }) + diff.push({ type: "replace", path: "/street2", value: y?.street2 }) } } if (x.city !== y.city) { - diff.push({ type: "update", path: "/city", value: y.city }) + diff.push({ type: "replace", path: "/city", value: y.city }) } } else { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -1079,10 +1092,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = string function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -1117,24 +1130,24 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema (` "type Type = { tag: "A"; onA: number } | { tag: "B"; onB: string } function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (x.tag === "A" && y.tag === "A") { if (x.tag !== y.tag) { - diff.push({ type: "update", path: "/tag", value: y.tag }) + diff.push({ type: "replace", path: "/tag", value: y.tag }) } if (x.onA !== y.onA && (x.onA === x.onA || y.onA === y.onA)) { - diff.push({ type: "update", path: "/onA", value: y.onA }) + diff.push({ type: "replace", path: "/onA", value: y.onA }) } } else if (x.tag === "B" && y.tag === "B") { if (x.tag !== y.tag) { - diff.push({ type: "update", path: "/tag", value: y.tag }) + diff.push({ type: "replace", path: "/tag", value: y.tag }) } if (x.onB !== y.onB) { - diff.push({ type: "update", path: "/onB", value: y.onB }) + diff.push({ type: "replace", path: "/onB", value: y.onB }) } } else { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -1153,10 +1166,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: unknown, y: unknown) { - const diff = [] + let diff = [] if (x === y) return diff if (!Object.is(x, y)) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -1174,10 +1187,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema )).toMatchInlineSnapshot (` "function diff(x: string, y: string) { - const diff = [] + let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "update", path: "", value: y }) + diff.push({ type: "replace", path: "", value: y }) } return diff } @@ -1221,33 +1234,33 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema pqr: Array } function diff(x: Type, y: Type) { - const diff = [] + let diff = [] if (x === y) return diff if (x.abc !== y.abc) { - diff.push({ type: "update", path: "/abc", value: y.abc }) + diff.push({ type: "replace", path: "/abc", value: y.abc }) } if (y?.def === undefined && x?.def !== undefined) { - diff.push({ type: "delete", path: "/def" }) + diff.push({ type: "remove", path: "/def" }) } else if (x?.def === undefined && y?.def !== undefined) { - diff.push({ type: "insert", path: "/def", value: y?.def }) - } else { + diff.push({ type: "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({ type: "update", path: "/def", value: y?.def }) + diff.push({ type: "replace", path: "/def", value: y?.def }) } } if (x.ghi !== y.ghi && (x.ghi === x.ghi || y.ghi === y.ghi)) { - diff.push({ type: "update", path: "/ghi", value: y.ghi }) + diff.push({ type: "replace", path: "/ghi", value: y.ghi }) } if (x.jkl !== y.jkl) { - diff.push({ type: "update", path: "/jkl", value: y.jkl }) + diff.push({ type: "replace", path: "/jkl", value: y.jkl }) } if (y?.def === undefined && x?.def !== undefined) { - diff.push({ type: "delete", path: "/def" }) + diff.push({ type: "remove", path: "/def" }) } else if (x?.def === undefined && y?.def !== undefined) { - diff.push({ type: "insert", path: "/def", value: y?.def }) - } else { + diff.push({ type: "add", path: "/def", value: y?.def }) + } else if (x?.def !== undefined && y?.def !== undefined) { if (x?.def !== y?.def) { - diff.push({ type: "update", path: "/def", value: y?.def }) + diff.push({ type: "replace", path: "/def", value: y?.def }) } } const length = Math.min(x.pqr.length, y.pqr.length) @@ -1256,17 +1269,17 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const x_pqr_item = x.pqr[ix] const y_pqr_item = y.pqr[ix] if (x_pqr_item !== y_pqr_item) { - diff.push({ type: "update", path: \`/pqr/\${ix}\`, value: y_pqr_item }) + diff.push({ type: "replace", path: \`/pqr/\${ix}\`, value: y_pqr_item }) } } if (length < x.pqr.length) { for (; ix < x.pqr.length; ix++) { - diff.push({ type: "delete", path: \`/pqr/\${ix}\` }) + diff.push({ type: "remove", path: \`/pqr/\${ix}\` }) } } if (length < y.pqr.length) { for (; ix < y.pqr.length; ix++) { - diff.push({ type: "insert", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) + diff.push({ type: "add", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) } } return diff @@ -1278,12 +1291,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema.diff', () => { - const sort = (x: JsonSchema.diff.Edit, y: JsonSchema.diff.Edit) => - x.path < y.path ? -1 : y.path < x.path ? 1 : 0 vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Never', () => { - const diff_1 = JsonSchema.diff( - { not: {} } + const diff_1 = fn.flow( + JsonSchema.diff( + { not: {} } + ), + adapt ) const x_1 = 0 as never @@ -1311,8 +1325,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Null', () => { - const diff_1 = JsonSchema.diff( - { type: 'null' } + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'null' } + ), + adapt, ) const x_1 = null as never @@ -1340,8 +1357,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Boolean', () => { - const diff_1 = JsonSchema.diff( - { type: 'boolean' } + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'boolean' } + ), + adapt ) const x_1 = true @@ -1369,8 +1389,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Integer', () => { - const diff_1 = JsonSchema.diff( - { type: 'integer' } + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'integer' } + ), + adapt, ) const x_1 = 0 @@ -1399,8 +1422,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Number', () => { - const diff_1 = JsonSchema.diff( - { type: 'number' } + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'number' } + ), + adapt, ) const x_1 = 0.25 @@ -1428,8 +1454,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.String', () => { - const diff_1 = JsonSchema.diff( - { type: 'string' } + const diff_1 = fn.flow( + JsonSchema.diff( + { type: 'string' } + ), + adapt, ) const x_1 = '' @@ -1457,8 +1486,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Enum', () => { - const diff_1 = JsonSchema.diff( - { enum: ['A', 'B'] } + const diff_1 = fn.flow( + JsonSchema.diff( + { enum: ['A', 'B'] } + ), + adapt ) const x_1 = 'A' @@ -1486,8 +1518,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Const', () => { - const diff_1 = JsonSchema.diff( - { const: null } + const diff_1 = fn.flow( + JsonSchema.diff( + { const: null } + ), + adapt, ) const x_1 = null @@ -1513,8 +1548,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_1, x_1).sort(sort) ) - const diff_2 = JsonSchema.diff( - { const: 0 } + const diff_2 = fn.flow( + JsonSchema.diff( + { const: 0 } + ), + adapt, ) const x_2 = 0 @@ -1540,8 +1578,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_2, x_2).sort(sort) ) - const diff_3 = JsonSchema.diff( - { const: false } + const diff_3 = fn.flow( + JsonSchema.diff( + { const: false } + ), + adapt, ) const x_3 = false @@ -1567,8 +1608,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_3, x_3).sort(sort) ) - const diff_4 = JsonSchema.diff( - { const: [] } + const diff_4 = fn.flow( + JsonSchema.diff( + { const: [] } + ), + adapt, ) const x_4 = [] as [] @@ -1594,8 +1638,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_4, x_4).sort(sort) ) - const diff_5 = JsonSchema.diff( - { const: [1] } + const diff_5 = fn.flow( + JsonSchema.diff( + { const: [1] } + ), + adapt, ) const x_5 = [1] as [1] @@ -1621,8 +1668,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_5, x_5).sort(sort) ) - const diff_6 = JsonSchema.diff( - { const: { a: 1 } } + const diff_6 = fn.flow( + JsonSchema.diff( + { const: { a: 1 } } + ), + adapt, ) const x_6 = { a: 1 } as const @@ -1648,8 +1698,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_6, x_6).sort(sort) ) - const diff_7 = JsonSchema.diff( - { const: { a: 1, b: 2 } } + const diff_7 = fn.flow( + JsonSchema.diff( + { const: { a: 1, b: 2 } } + ), + adapt, ) const x_7 = { a: 1, b: 2 } as const @@ -1677,11 +1730,14 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Array', () => { - const diff_1 = JsonSchema.diff( - { - type: 'array', - items: { type: 'string' } - } + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'array', + items: { type: 'string' } + } + ), + adapt, ) const x_1 = Array.of() @@ -1710,18 +1766,21 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Record', () => { - const diff_1 = JsonSchema.diff( - { - type: 'object', - additionalProperties: { - type: 'array', - items: { type: 'string' } - }, - patternProperties: { - abc: { type: 'string' }, - def: { type: 'number' } + 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 @@ -1749,15 +1808,18 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Object', () => { - const diff_1 = JsonSchema.diff( - { - type: 'object', - required: ['abc', 'def'], - properties: { - abc: { type: 'string' }, - def: { type: 'number' } + 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 } @@ -1785,11 +1847,14 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Tuple', () => { - const diff_1 = JsonSchema.diff( - { - type: 'array', - prefixItems: [], - } + const diff_1 = fn.flow( + JsonSchema.diff( + { + type: 'array', + prefixItems: [], + } + ), + adapt, ) const x_1: [] = [] @@ -1815,13 +1880,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_1, x_1).sort(sort) ) - const diff_2 = JsonSchema.diff( - { - type: 'array', - prefixItems: [ - { type: 'string' } - ], - } + const diff_2 = fn.flow( + JsonSchema.diff( + { + type: 'array', + prefixItems: [ + { type: 'string' } + ], + } + ), + adapt, ) const x_2 = ['hey'] as [string] @@ -1849,10 +1917,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Union', () => { - const diff_1 = JsonSchema.diff( - { - anyOf: [], - } + const diff_1 = fn.flow( + JsonSchema.diff( + { + anyOf: [], + } + ), + adapt, ) const x_1 = 0 as never @@ -1878,12 +1949,15 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_1, x_1).sort(sort) ) - const diff_2 = JsonSchema.diff( - { - anyOf: [ - { type: 'string' } - ], - } + const diff_2 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' } + ], + } + ), + adapt, ) const x_2 = 'hey' @@ -1909,13 +1983,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_2, x_2).sort(sort) ) - const diff_3 = JsonSchema.diff( - { - anyOf: [ - { type: 'string' }, - { type: 'number' }, - ], - } + const diff_3 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + ], + } + ), + adapt, ) const x_3 = 'hey' @@ -1941,17 +2018,20 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_3, x_3).sort(sort) ) - const diff_4 = JsonSchema.diff( - { - anyOf: [ - { type: 'string' }, - { type: 'number' }, - { - type: 'array', - items: { type: 'string' } - }, - ], - } + const diff_4 = fn.flow( + JsonSchema.diff( + { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + { + type: 'array', + items: { type: 'string' } + }, + ], + } + ), + adapt, ) const x_4 = 'hey' @@ -2003,23 +2083,26 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_4, z_4).sort(sort) ) - const diff_5 = JsonSchema.diff( - { - anyOf: [ - { - type: 'array', - items: { type: 'string' } - }, - { - type: 'object', - required: ['abc'], - properties: { - abc: { type: 'string' }, - def: { type: 'number' } + 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'] @@ -2107,27 +2190,30 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_5, z_5).sort(sort) ) - const diff_6 = 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' } + 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 @@ -2306,10 +2392,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema }) vi.test('〖️⛳️〗› ❲JsonSchema.diff❳: JsonSchema.Intersection', () => { - const diff_1 = JsonSchema.diff( - { - allOf: [], - } + const diff_1 = fn.flow( + JsonSchema.diff( + { + allOf: [], + } + ), + adapt, ) const x_1 = 0 as never @@ -2335,27 +2424,30 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema oracle(y_1, x_1).sort(sort) ) - const diff_2 = 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' }, + 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 } 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/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"], From d6702f2e8a52104f4f22d8ec719e7a2ee6ce0478 Mon Sep 17 00:00:00 2001 From: Andrew Jarrett Date: Tue, 12 Aug 2025 18:06:15 -0500 Subject: [PATCH 4/4] fix(json-schema): make diffing compliant with RFC-6902 --- packages/json-schema/src/diff.ts | 36 ++-- packages/json-schema/test/diff.fuzz.test.ts | 3 +- packages/json-schema/test/diff.test.ts | 214 ++++++++++---------- 3 files changed, 124 insertions(+), 129 deletions(-) diff --git a/packages/json-schema/src/diff.ts b/packages/json-schema/src/diff.ts index a8857cfb..18c874f5 100644 --- a/packages/json-schema/src/diff.ts +++ b/packages/json-schema/src/diff.ts @@ -21,9 +21,9 @@ import { Invariant, } from '@traversable/json-schema-types' -interface Add { type: 'add', path: string, value: unknown } -interface Replace { type: 'replace', path: string, value: unknown } -interface Remove { type: 'remove', path: string } +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[] @@ -89,7 +89,7 @@ function StrictlyEqualOrDiff(x: (string | number)[], y: (string | number)[], ix: const Y = joinPath(y, ix.isOptional) return [ `if (${X} !== ${Y}) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -99,7 +99,7 @@ function SameNumberOrDiff(x: (string | number)[], y: (string | number)[], ix: Sc const Y = joinPath(y, ix.isOptional) return [ `if (${X} !== ${Y} && (${X} === ${X} || ${Y} === ${Y})) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -109,7 +109,7 @@ function SameValueOrDiff(x: (string | number)[], y: (string | number)[], ix: Sco const Y = joinPath(y, ix.isOptional) return [ `if (!Object.is(${X}, ${Y})) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(ix.dataPath)}, value: ${Y} })`, `}`, ].join('\n') } @@ -136,7 +136,7 @@ const foldJson = Json.fold((x) => { return x.length === 0 ? [ `if (${X_PATH}.length !== ${Y_PATH}.length) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : x.map( @@ -160,7 +160,7 @@ const foldJson = Json.fold((x) => { return BODY.length === 0 ? [ `if (Object.keys(${X_PATH}).length !== Object.keys(${Y_PATH}).length) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : BODY.join('\n') @@ -198,12 +198,12 @@ function createArrayDiff(x: JsonSchema.Array): Builder { `}`, `if (${LENGTH_IDENT} < ${X_PATH}${DOT}length) {`, ` for(; ${IX_IDENT} < ${X_PATH}${DOT}length; ${IX_IDENT}++) {`, - ` diff.push({ type: "remove", path: ${jsonPointer(PATH)} })`, + ` 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({ type: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, + ` diff.push({ op: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${IX_IDENT}] })`, ` }`, `}`, ].join('\n') @@ -237,7 +237,7 @@ function createRecordDiff(x: JsonSchema.Record): Builder { `for (let ${KEY_IDENT} in ${X_PATH}) {`, ` ${SEEN_IDENT}.add(${KEY_IDENT})`, ` if (!(${KEY_IDENT} in ${Y_PATH})) {`, - ` diff.push({ type: "remove", path: ${jsonPointer(PATH)} })`, + ` diff.push({ op: "remove", path: ${jsonPointer(PATH)} })`, ` continue`, ` }`, PATTERN_PROPERTIES, @@ -248,7 +248,7 @@ function createRecordDiff(x: JsonSchema.Record): Builder { ` continue`, ` }`, ` if (!(${KEY_IDENT} in ${X_PATH})) {`, - ` diff.push({ type: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, + ` diff.push({ op: "add", path: ${jsonPointer(PATH)}, value: ${Y_PATH}[${KEY_IDENT}] })`, ` continue`, ` }`, PATTERN_PROPERTIES, @@ -264,10 +264,10 @@ function createDiffOptional(continuation: Builder): Builder { const Y_PATH = joinPath(Y, IX.isOptional) return [ `if (${Y_PATH} === undefined && ${X_PATH} !== undefined) {`, - ` diff.push({ type: "remove", path: ${jsonPointer(IX.dataPath)} })`, + ` diff.push({ op: "remove", path: ${jsonPointer(IX.dataPath)} })`, `}`, `else if (${X_PATH} === undefined && ${Y_PATH} !== undefined) {`, - ` diff.push({ type: "add", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` 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 }), @@ -299,7 +299,7 @@ function createObjectDiff(x: JsonSchema.Object): Builder { return BODY.length === 0 ? [ `if (${X_PATH} !== ${Y_PATH}) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') : BODY.join('\n') @@ -313,7 +313,7 @@ function createTupleDiff(x: JsonSchema.Tuple): Builder { if (x.prefixItems.length === 0) { return [ `if (${X_PATH}.length !== ${Y_PATH}.length) {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') } @@ -414,7 +414,7 @@ function createInclusiveUnionDiff( ...PREDICATES.map((_) => _ === null ? null : _.PREDICATE), CHECKS.join('\n'), `else {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].filter((_) => _ !== null).join('\n') } @@ -441,7 +441,7 @@ function createExclusiveUnionDiff( ].join('\n') }), `else {`, - ` diff.push({ type: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, + ` diff.push({ op: "replace", path: ${jsonPointer(IX.dataPath)}, value: ${Y_PATH} })`, `}`, ].join('\n') } diff --git a/packages/json-schema/test/diff.fuzz.test.ts b/packages/json-schema/test/diff.fuzz.test.ts index e5049f66..8977ca83 100644 --- a/packages/json-schema/test/diff.fuzz.test.ts +++ b/packages/json-schema/test/diff.fuzz.test.ts @@ -3,7 +3,6 @@ import * as fc from 'fast-check' import prettier from '@prettier/sync' import { JsonSchema } from '@traversable/json-schema' -import { deriveUnequalValue } from '@traversable/registry' import { JsonSchemaTest } from '@traversable/json-schema-test' import type { Insert, Update, Delete } from '@sinclair/typebox/value' import { Diff as oracle } from '@sinclair/typebox/value' @@ -55,7 +54,7 @@ const adapter = { } function adapt(xs: JsonSchema.diff.Edit[]) { - return xs.map((x) => adapter[x.type](x as never)) + return xs.map((x) => adapter[x.op](x as never)) } function sort(x: T, y: T) { diff --git a/packages/json-schema/test/diff.test.ts b/packages/json-schema/test/diff.test.ts index 39ff82ec..b8d8c31f 100644 --- a/packages/json-schema/test/diff.test.ts +++ b/packages/json-schema/test/diff.test.ts @@ -20,7 +20,7 @@ const adapter = { } function adapt(xs: JsonSchema.diff.Edit[]) { - return xs.map((x) => adapter[x.type](x as never)) + return xs.map((x) => adapter[x.op](x as never)) } function sort(x: T, y: T) { @@ -38,7 +38,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: never, y: never) { let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -56,7 +56,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: null, y: null) { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -74,7 +74,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: boolean, y: boolean) { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -92,7 +92,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: number, y: number) { let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -110,7 +110,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: number, y: number) { let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -128,7 +128,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: string, y: string) { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -146,7 +146,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: never, y: never) { let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -162,7 +162,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: 0, y: 0) { let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -178,7 +178,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: 0 | 1, y: 0 | 1) { let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -194,7 +194,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: 0 | "two", y: 0 | "two") { let diff = [] if (!Object.is(x, y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -210,7 +210,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: "one" | "two", y: "one" | "two") { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -228,7 +228,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: null, y: null) { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -244,7 +244,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: false, y: false) { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -260,7 +260,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: 0, y: 0) { let diff = [] if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -276,7 +276,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema "function diff(x: "hey", y: "hey") { let diff = [] if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -293,7 +293,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x.length !== y.length) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -310,10 +310,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x[0] !== y[0] && (x[0] === x[0] || y[0] === y[0])) { - diff.push({ type: "replace", path: "/0", value: y[0] }) + diff.push({ op: "replace", path: "/0", value: y[0] }) } if (x[1] !== y[1]) { - diff.push({ type: "replace", path: "/1", value: y[1] }) + diff.push({ op: "replace", path: "/1", value: y[1] }) } return diff } @@ -330,10 +330,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x[0].length !== y[0].length) { - diff.push({ type: "replace", path: "/0", value: y[0] }) + diff.push({ op: "replace", path: "/0", value: y[0] }) } if (x[1][0][0] !== y[1][0][0]) { - diff.push({ type: "replace", path: "/1/0/0", value: y[1][0][0] }) + diff.push({ op: "replace", path: "/1/0/0", value: y[1][0][0] }) } return diff } @@ -350,7 +350,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (Object.keys(x).length !== Object.keys(y).length) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -372,10 +372,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x.abc !== y.abc && (x.abc === x.abc || y.abc === y.abc)) { - diff.push({ type: "replace", path: "/abc", value: y.abc }) + diff.push({ op: "replace", path: "/abc", value: y.abc }) } if (x.def !== y.def) { - diff.push({ type: "replace", path: "/def", value: y.def }) + diff.push({ op: "replace", path: "/def", value: y.def }) } return diff } @@ -411,20 +411,20 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema x_item.abc !== y_item.abc && (x_item.abc === x_item.abc || y_item.abc === y_item.abc) ) { - diff.push({ type: "replace", path: \`/\${ix}/abc\`, value: y_item.abc }) + diff.push({ op: "replace", path: \`/\${ix}/abc\`, value: y_item.abc }) } if (x_item.def !== y_item.def) { - diff.push({ type: "replace", path: \`/\${ix}/def\`, value: 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({ type: "remove", path: \`/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -464,36 +464,36 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_item = y[ix] if (x_item.street1 !== y_item.street1) { diff.push({ - type: "replace", + op: "replace", path: \`/\${ix}/street1\`, value: y_item.street1, }) } if (y_item?.street2 === undefined && x_item?.street2 !== undefined) { - diff.push({ type: "remove", path: \`/\${ix}/street2\` }) + diff.push({ op: "remove", path: \`/\${ix}/street2\` }) } else if (x_item?.street2 === undefined && y_item?.street2 !== undefined) { - diff.push({ type: "add", path: \`/\${ix}/street2\`, value: y_item?.street2 }) + 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({ - type: "replace", + op: "replace", path: \`/\${ix}/street2\`, value: y_item?.street2, }) } } if (x_item.city !== y_item.city) { - diff.push({ type: "replace", path: \`/\${ix}/city\`, value: 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({ type: "remove", path: \`/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -538,7 +538,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_item_item_item = y_item_item[ix2] if (x_item_item_item !== y_item_item_item) { diff.push({ - type: "replace", + op: "replace", path: \`/\${ix}/\${ix1}/\${ix2}\`, value: y_item_item_item, }) @@ -546,13 +546,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length2 < x_item_item.length) { for (; ix2 < x_item_item.length; ix2++) { - diff.push({ type: "remove", path: \`/\${ix}/\${ix1}/\${ix2}\` }) + diff.push({ op: "remove", path: \`/\${ix}/\${ix1}/\${ix2}\` }) } } if (length2 < y_item_item.length) { for (; ix2 < y_item_item.length; ix2++) { diff.push({ - type: "add", + op: "add", path: \`/\${ix}/\${ix1}/\${ix2}\`, value: y_item_item[ix2], }) @@ -561,23 +561,23 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length1 < x_item.length) { for (; ix1 < x_item.length; ix1++) { - diff.push({ type: "remove", path: \`/\${ix}/\${ix1}\` }) + diff.push({ op: "remove", path: \`/\${ix}/\${ix1}\` }) } } if (length1 < y_item.length) { for (; ix1 < y_item.length; ix1++) { - diff.push({ type: "add", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) + diff.push({ op: "add", path: \`/\${ix}/\${ix1}\`, value: y_item[ix1] }) } } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "remove", path: \`/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) } } return diff @@ -612,16 +612,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema for (let key in x) { seen.add(key) if (!(key in y)) { - diff.push({ type: "remove", path: \`/\${key}\` }) + diff.push({ op: "remove", path: \`/\${key}\` }) continue } if (/abc/.test(key)) { if (x[key] !== y[key]) { - diff.push({ type: "replace", path: \`/\${key}\`, value: 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({ type: "replace", path: \`/\${key}\`, value: y[key] }) + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) } } else { const length = Math.min(x[key].length, y[key].length) @@ -631,7 +631,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_key__item = y[key][ix] if (x_key__item !== y_key__item) { diff.push({ - type: "replace", + op: "replace", path: \`/\${key}/\${ix}\`, value: y_key__item, }) @@ -639,12 +639,12 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x[key].length) { for (; ix < x[key].length; ix++) { - diff.push({ type: "remove", path: \`/\${key}/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${key}/\${ix}\` }) } } if (length < y[key].length) { for (; ix < y[key].length; ix++) { - diff.push({ type: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) + diff.push({ op: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) } } } @@ -654,16 +654,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema continue } if (!(key in x)) { - diff.push({ type: "add", path: \`/\${key}\`, value: y[key] }) + diff.push({ op: "add", path: \`/\${key}\`, value: y[key] }) continue } if (/abc/.test(key)) { if (x[key] !== y[key]) { - diff.push({ type: "replace", path: \`/\${key}\`, value: 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({ type: "replace", path: \`/\${key}\`, value: y[key] }) + diff.push({ op: "replace", path: \`/\${key}\`, value: y[key] }) } } else { const length = Math.min(x[key].length, y[key].length) @@ -673,7 +673,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_key__item = y[key][ix] if (x_key__item !== y_key__item) { diff.push({ - type: "replace", + op: "replace", path: \`/\${key}/\${ix}\`, value: y_key__item, }) @@ -681,12 +681,12 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x[key].length) { for (; ix < x[key].length; ix++) { - diff.push({ type: "remove", path: \`/\${key}/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${key}/\${ix}\` }) } } if (length < y[key].length) { for (; ix < y[key].length; ix++) { - diff.push({ type: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) + diff.push({ op: "add", path: \`/\${key}/\${ix}\`, value: y[key][ix] }) } } } @@ -734,15 +734,15 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x.firstName !== y.firstName) { - diff.push({ type: "replace", path: "/firstName", value: y.firstName }) + diff.push({ op: "replace", path: "/firstName", value: y.firstName }) } if (y?.lastName === undefined && x?.lastName !== undefined) { - diff.push({ type: "remove", path: "/lastName" }) + diff.push({ op: "remove", path: "/lastName" }) } else if (x?.lastName === undefined && y?.lastName !== undefined) { - diff.push({ type: "add", path: "/lastName", value: y?.lastName }) + diff.push({ op: "add", path: "/lastName", value: y?.lastName }) } else if (x?.lastName !== undefined && y?.lastName !== undefined) { if (x?.lastName !== y?.lastName) { - diff.push({ type: "replace", path: "/lastName", value: y?.lastName }) + diff.push({ op: "replace", path: "/lastName", value: y?.lastName }) } } const length = Math.min(x.addresses.length, y.addresses.length) @@ -752,7 +752,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const y_addresses_item = y.addresses[ix] if (x_addresses_item.street1 !== y_addresses_item.street1) { diff.push({ - type: "replace", + op: "replace", path: \`/addresses/\${ix}/street1\`, value: y_addresses_item.street1, }) @@ -761,13 +761,13 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema y_addresses_item?.street2 === undefined && x_addresses_item?.street2 !== undefined ) { - diff.push({ type: "remove", path: \`/addresses/\${ix}/street2\` }) + diff.push({ op: "remove", path: \`/addresses/\${ix}/street2\` }) } else if ( x_addresses_item?.street2 === undefined && y_addresses_item?.street2 !== undefined ) { diff.push({ - type: "add", + op: "add", path: \`/addresses/\${ix}/street2\`, value: y_addresses_item?.street2, }) @@ -777,7 +777,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema ) { if (x_addresses_item?.street2 !== y_addresses_item?.street2) { diff.push({ - type: "replace", + op: "replace", path: \`/addresses/\${ix}/street2\`, value: y_addresses_item?.street2, }) @@ -785,7 +785,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (x_addresses_item.city !== y_addresses_item.city) { diff.push({ - type: "replace", + op: "replace", path: \`/addresses/\${ix}/city\`, value: y_addresses_item.city, }) @@ -793,16 +793,12 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (length < x.addresses.length) { for (; ix < x.addresses.length; ix++) { - diff.push({ type: "remove", path: \`/addresses/\${ix}\` }) + diff.push({ op: "remove", path: \`/addresses/\${ix}\` }) } } if (length < y.addresses.length) { for (; ix < y.addresses.length; ix++) { - diff.push({ - type: "add", - path: \`/addresses/\${ix}\`, - value: y.addresses[ix], - }) + diff.push({ op: "add", path: \`/addresses/\${ix}\`, value: y.addresses[ix] }) } } return diff @@ -825,7 +821,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x.length !== y.length) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -847,7 +843,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x[0] !== y[0]) { - diff.push({ type: "replace", path: "/0", value: y[0] }) + diff.push({ op: "replace", path: "/0", value: y[0] }) } return diff } @@ -870,10 +866,10 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x[0] !== y[0]) { - diff.push({ type: "replace", path: "/0", value: 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({ type: "replace", path: "/1", value: y[1] }) + diff.push({ op: "replace", path: "/1", value: y[1] }) } return diff } @@ -915,16 +911,16 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x[0][0] !== y[0][0]) { - diff.push({ type: "replace", path: "/0/0", value: 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({ type: "replace", path: "/1/0/0", value: 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({ type: "replace", path: "/1/0/1", value: y[1][0][1] }) + diff.push({ op: "replace", path: "/1/0/1", value: y[1][0][1] }) } return diff } @@ -948,7 +944,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (!Object.is(x, y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -971,7 +967,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1029,11 +1025,11 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema } if (typeof x === "number" && typeof y === "number") { if (x !== y && (x === x || y === y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } } else if (typeof x === "string" && typeof y === "string") { if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } } else if (check(x) && check(y)) { const length = Math.min(x.length, y.length) @@ -1042,37 +1038,37 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema 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({ type: "replace", path: \`/\${ix}\`, value: y_item }) + diff.push({ op: "replace", path: \`/\${ix}\`, value: y_item }) } } if (length < x.length) { for (; ix < x.length; ix++) { - diff.push({ type: "remove", path: \`/\${ix}\` }) + diff.push({ op: "remove", path: \`/\${ix}\` }) } } if (length < y.length) { for (; ix < y.length; ix++) { - diff.push({ type: "add", path: \`/\${ix}\`, value: y[ix] }) + diff.push({ op: "add", path: \`/\${ix}\`, value: y[ix] }) } } } else if (check1(x) && check1(y)) { if (x.street1 !== y.street1) { - diff.push({ type: "replace", path: "/street1", value: y.street1 }) + diff.push({ op: "replace", path: "/street1", value: y.street1 }) } if (y?.street2 === undefined && x?.street2 !== undefined) { - diff.push({ type: "remove", path: "/street2" }) + diff.push({ op: "remove", path: "/street2" }) } else if (x?.street2 === undefined && y?.street2 !== undefined) { - diff.push({ type: "add", path: "/street2", value: y?.street2 }) + diff.push({ op: "add", path: "/street2", value: y?.street2 }) } else if (x?.street2 !== undefined && y?.street2 !== undefined) { if (x?.street2 !== y?.street2) { - diff.push({ type: "replace", path: "/street2", value: y?.street2 }) + diff.push({ op: "replace", path: "/street2", value: y?.street2 }) } } if (x.city !== y.city) { - diff.push({ type: "replace", path: "/city", value: y.city }) + diff.push({ op: "replace", path: "/city", value: y.city }) } } else { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1095,7 +1091,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1134,20 +1130,20 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema if (x === y) return diff if (x.tag === "A" && y.tag === "A") { if (x.tag !== y.tag) { - diff.push({ type: "replace", path: "/tag", value: 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({ type: "replace", path: "/onA", value: 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({ type: "replace", path: "/tag", value: y.tag }) + diff.push({ op: "replace", path: "/tag", value: y.tag }) } if (x.onB !== y.onB) { - diff.push({ type: "replace", path: "/onB", value: y.onB }) + diff.push({ op: "replace", path: "/onB", value: y.onB }) } } else { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1169,7 +1165,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (!Object.is(x, y)) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1190,7 +1186,7 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x !== y) { - diff.push({ type: "replace", path: "", value: y }) + diff.push({ op: "replace", path: "", value: y }) } return diff } @@ -1237,30 +1233,30 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema let diff = [] if (x === y) return diff if (x.abc !== y.abc) { - diff.push({ type: "replace", path: "/abc", value: y.abc }) + diff.push({ op: "replace", path: "/abc", value: y.abc }) } if (y?.def === undefined && x?.def !== undefined) { - diff.push({ type: "remove", path: "/def" }) + diff.push({ op: "remove", path: "/def" }) } else if (x?.def === undefined && y?.def !== undefined) { - diff.push({ type: "add", path: "/def", value: y?.def }) + 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({ type: "replace", path: "/def", value: 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({ type: "replace", path: "/ghi", value: y.ghi }) + diff.push({ op: "replace", path: "/ghi", value: y.ghi }) } if (x.jkl !== y.jkl) { - diff.push({ type: "replace", path: "/jkl", value: y.jkl }) + diff.push({ op: "replace", path: "/jkl", value: y.jkl }) } if (y?.def === undefined && x?.def !== undefined) { - diff.push({ type: "remove", path: "/def" }) + diff.push({ op: "remove", path: "/def" }) } else if (x?.def === undefined && y?.def !== undefined) { - diff.push({ type: "add", path: "/def", value: y?.def }) + diff.push({ op: "add", path: "/def", value: y?.def }) } else if (x?.def !== undefined && y?.def !== undefined) { if (x?.def !== y?.def) { - diff.push({ type: "replace", path: "/def", value: y?.def }) + diff.push({ op: "replace", path: "/def", value: y?.def }) } } const length = Math.min(x.pqr.length, y.pqr.length) @@ -1269,17 +1265,17 @@ vi.describe('〖️⛳️〗‹‹‹ ❲@traversable/json-schema❳: JsonSchema const x_pqr_item = x.pqr[ix] const y_pqr_item = y.pqr[ix] if (x_pqr_item !== y_pqr_item) { - diff.push({ type: "replace", path: \`/pqr/\${ix}\`, value: 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({ type: "remove", path: \`/pqr/\${ix}\` }) + diff.push({ op: "remove", path: \`/pqr/\${ix}\` }) } } if (length < y.pqr.length) { for (; ix < y.pqr.length; ix++) { - diff.push({ type: "add", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) + diff.push({ op: "add", path: \`/pqr/\${ix}\`, value: y.pqr[ix] }) } } return diff