Skip to content

Commit

Permalink
Revision 0.33.3 (#950)
Browse files Browse the repository at this point in the history
* Reimplement Object Diff

* Version
  • Loading branch information
sinclairzx81 authored Aug 9, 2024
1 parent c489464 commit a5b03c0
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 32 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
55 changes: 32 additions & 23 deletions src/value/delta/delta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
// ------------------------------------------------------------------
Expand All @@ -108,28 +104,41 @@ function CreateDelete(path: string): Edit {
return { type: 'delete', path }
}
// ------------------------------------------------------------------
// AssertDiffable
// ------------------------------------------------------------------
function AssertDiffable(value: unknown): asserts value is Record<string | number, unknown> {
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<Edit> {
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<Edit> {
Expand Down Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions test/runtime/value/delta/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
// ----------------------------------------------------
Expand Down Expand Up @@ -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)
})
// ----------------------------------------------------
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
})
})
10 changes: 10 additions & 0 deletions test/runtime/value/delta/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

0 comments on commit a5b03c0

Please sign in to comment.