diff --git a/package-lock.json b/package-lock.json index 79ede7384..e2eb7ab66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.33.2", + "version": "0.33.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.33.2", + "version": "0.33.3", "license": "MIT", "devDependencies": { "@arethetypeswrong/cli": "^0.13.2", diff --git a/package.json b/package.json index 9f4f29ebd..07be5d30f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.33.2", + "version": "0.33.3", "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/value/delta/delta.ts b/src/value/delta/delta.ts index bd437acc1..fd65c57cc 100644 --- a/src/value/delta/delta.ts +++ b/src/value/delta/delta.ts @@ -26,11 +26,12 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { IsStandardObject, IsArray, IsTypedArray, IsValueType, IsSymbol, IsUndefined } from '../guard/index' +import { HasPropertyKey, IsStandardObject, IsArray, IsTypedArray, IsValueType } from '../guard/index' import type { ObjectType, ArrayType, TypedArrayType, ValueType } from '../guard/index' import type { Static } from '../../type/static/index' import { ValuePointer } from '../pointer/index' import { Clone } from '../clone/index' +import { Equal } from '../equal/equal' import { TypeBoxError } from '../../type/error/index' import { Literal, type TLiteral } from '../../type/literal/index' @@ -85,16 +86,11 @@ export const Edit: TUnion<[typeof Insert, typeof Update, typeof Delete]> = Union // ------------------------------------------------------------------ // Errors // ------------------------------------------------------------------ -export class ValueDeltaError extends TypeBoxError { +export class ValueDiffError extends TypeBoxError { constructor(public readonly value: unknown, message: string) { super(message) } } -export class ValueDeltaSymbolError extends ValueDeltaError { - constructor(public readonly value: unknown) { - super(value, 'Cannot diff objects with symbol keys') - } -} // ------------------------------------------------------------------ // Command Factory // ------------------------------------------------------------------ @@ -108,28 +104,41 @@ function CreateDelete(path: string): Edit { return { type: 'delete', path } } // ------------------------------------------------------------------ +// AssertDiffable +// ------------------------------------------------------------------ +function AssertDiffable(value: unknown): asserts value is Record { + if (globalThis.Object.getOwnPropertySymbols(value).length > 0) throw new ValueDiffError(value, 'Cannot diff objects with symbols') +} +// ------------------------------------------------------------------ // Diffing Generators // ------------------------------------------------------------------ function* ObjectType(path: string, current: ObjectType, next: unknown): IterableIterator { + AssertDiffable(current) + AssertDiffable(next) if (!IsStandardObject(next)) return yield CreateUpdate(path, next) - const currentKeys = [...globalThis.Object.keys(current), ...globalThis.Object.getOwnPropertySymbols(current)] - const nextKeys = [...globalThis.Object.keys(next), ...globalThis.Object.getOwnPropertySymbols(next)] - for (const key of currentKeys) { - if (IsSymbol(key)) throw new ValueDeltaSymbolError(key) - if (IsUndefined(next[key]) && nextKeys.includes(key)) yield CreateUpdate(`${path}/${globalThis.String(key)}`, undefined) - } + const currentKeys = globalThis.Object.getOwnPropertyNames(current) + const nextKeys = globalThis.Object.getOwnPropertyNames(next) + // ---------------------------------------------------------------- + // inserts + // ---------------------------------------------------------------- for (const key of nextKeys) { - if (IsUndefined(current[key]) || IsUndefined(next[key])) continue - if (IsSymbol(key)) throw new ValueDeltaSymbolError(key) - yield* Visit(`${path}/${globalThis.String(key)}`, current[key], next[key]) + if (HasPropertyKey(current, key)) continue + yield CreateInsert(`${path}/${key}`, next[key]) } - for (const key of nextKeys) { - if (IsSymbol(key)) throw new ValueDeltaSymbolError(key) - if (IsUndefined(current[key])) yield CreateInsert(`${path}/${globalThis.String(key)}`, next[key]) + // ---------------------------------------------------------------- + // updates + // ---------------------------------------------------------------- + for (const key of currentKeys) { + if (!HasPropertyKey(next, key)) continue + if (Equal(current, next)) continue + yield* Visit(`${path}/${key}`, current[key], next[key]) } - for (const key of currentKeys.reverse()) { - if (IsSymbol(key)) throw new ValueDeltaSymbolError(key) - if (IsUndefined(next[key]) && !nextKeys.includes(key)) yield CreateDelete(`${path}/${globalThis.String(key)}`) + // ---------------------------------------------------------------- + // deletes + // ---------------------------------------------------------------- + for (const key of currentKeys) { + if (HasPropertyKey(next, key)) continue + yield CreateDelete(`${path}/${key}`) } } function* ArrayType(path: string, current: ArrayType, next: unknown): IterableIterator { @@ -161,7 +170,7 @@ function* Visit(path: string, current: unknown, next: unknown): IterableIterator if (IsArray(current)) return yield* ArrayType(path, current, next) if (IsTypedArray(current)) return yield* TypedArrayType(path, current, next) if (IsValueType(current)) return yield* ValueType(path, current, next) - throw new ValueDeltaError(current, 'Unable to create diff edits for unknown value') + throw new ValueDiffError(current, 'Unable to diff value') } // ------------------------------------------------------------------ // Diff diff --git a/test/runtime/value/delta/diff.ts b/test/runtime/value/delta/diff.ts index 2943e157f..6be662fcb 100644 --- a/test/runtime/value/delta/diff.ts +++ b/test/runtime/value/delta/diff.ts @@ -251,14 +251,14 @@ describe('value/delta/Diff', () => { const A = { x: 1, y: 1, z: 1 } const B = { a: 2, b: 2, c: 2 } const D = Value.Diff(A, B) - const E = [Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Delete('/z'), Delete('/y'), Delete('/x')] + const E = [Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Delete('/x'), Delete('/y'), Delete('/z')] Assert.IsEqual(D, E) }) - it('Should diff PROPERTY update, insert and delete order preserved', () => { + it('Should diff PROPERTY insert, update, and delete order preserved', () => { const A = { x: 1, y: 1, z: 1, w: 1 } const B = { a: 2, b: 2, c: 2, w: 2 } const D = Value.Diff(A, B) - const E = [Update('/w', 2), Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Delete('/z'), Delete('/y'), Delete('/x')] + const E = [Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Update('/w', 2), Delete('/x'), Delete('/y'), Delete('/z')] Assert.IsEqual(D, E) }) // ---------------------------------------------------- @@ -303,7 +303,7 @@ describe('value/delta/Diff', () => { const A = { v: { x: 1, y: 1 } } const B = { v: { x: 2, w: 2 } } const D = Value.Diff(A, B) - const E = [Update('/v/x', B.v.x), Insert('/v/w', B.v.w), Delete('/v/y')] + const E = [Insert('/v/w', B.v.w), Update('/v/x', B.v.x), Delete('/v/y')] Assert.IsEqual(D, E) }) // ---------------------------------------------------- @@ -344,11 +344,11 @@ describe('value/delta/Diff', () => { const E = [Delete('/0/v/z')] Assert.IsEqual(D, E) }) - it('Should diff NESTED ARRAY update, insert and delete order preserved', () => { + it('Should diff NESTED ARRAY insert update and delete order preserved', () => { const A = [{ v: { x: 1, y: 1 } }] const B = [{ v: { x: 2, w: 2 } }] const D = Value.Diff(A, B) - const E = [Update('/0/v/x', B[0].v.x), Insert('/0/v/w', B[0].v.w), Delete('/0/v/y')] + const E = [Insert('/0/v/w', B[0].v.w), Update('/0/v/x', B[0].v.x), Delete('/0/v/y')] Assert.IsEqual(D, E) }) it('Should throw if attempting to diff a current value with symbol key', () => { @@ -382,4 +382,14 @@ describe('value/delta/Diff', () => { const E = [Update('', new Uint8Array([0, 9, 2, 3, 4]))] Assert.IsEqual(D, E) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/937 + // ---------------------------------------------------------------- + it('Should generate no diff for undefined properties of current and next', () => { + const A = { a: undefined } + const B = { a: undefined } + const D = Value.Diff(A, B) + const E = [] as any + Assert.IsEqual(D, E) + }) }) diff --git a/test/runtime/value/delta/patch.ts b/test/runtime/value/delta/patch.ts index 7e308c42a..2addfe53c 100644 --- a/test/runtime/value/delta/patch.ts +++ b/test/runtime/value/delta/patch.ts @@ -411,4 +411,14 @@ describe('value/delta/Patch', () => { const P = Value.Patch(A, D) Assert.IsEqual(B, P) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/937 + // ---------------------------------------------------------------- + it('Should generate no diff for undefined properties of current and next', () => { + const A = { a: undefined } + const B = { a: undefined } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) })