diff --git a/src/bucket/delta.ts b/src/bucket/delta.ts new file mode 100644 index 0000000..429873d --- /dev/null +++ b/src/bucket/delta.ts @@ -0,0 +1,169 @@ +/** + * A delta + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of the Liza Data Collection Framework. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** The data structure expected for a document's internal key/value store */ +export type Kv = Record; + +/** Possible delta values for Kv array indexes */ +export type DeltaDatum = T | null | undefined; + + +/** + * The constructor type for a delta generating function + * + * @param src - the source data set + * @param dest - the destination data set + * + * @return the delta which transforms src to dest + */ +export type DeltaConstructor = Kv, V extends Kv = U> = ( + src: U, + dest: V, +) => DeltaResult; + + +/** Transform type T to hold possible delta values */ +export type DeltaResult = { [K in keyof T]: DeltaDatum | null }; + + + /** + * Create delta to transform from src into dest + * + * @param src - the source data set + * @param dest - the destination data set + * + * @return the delta + */ +export function createDelta, V extends Kv>( + src: U, + dest: V, +): DeltaResult +{ + const delta: DeltaResult = {}; + + // Loop through all keys + const key_set = new Set( + Object.keys( src ).concat( Object.keys( dest ) ) ); + + key_set.forEach( key => + { + const src_data = src[ key ]; + const dest_data = dest[ key ]; + + // If source does not contain the key, use entire dest data + if ( !src_data || !src_data.length ) + { + delta[ key ] = dest_data; + + return; + } + + // If the key no longer exists in dest then nullify this key + if ( !dest_data || !dest_data.length ) + { + delta[ key ] = null; + + return; + } + + // If neither condition above is true then create the key iteratively + const delta_key = _createDeltaKey( src_data, dest_data ); + + if ( delta_key.changed ) + { + delta[ key ] = delta_key.data; + } + } ); + + return >delta; +} + + +/** + * Build the delta key iteratively + * + * @param src - the source data array + * @param dest - the destination data array + * + * @return an object with an identical flag and a data array + */ +function _createDeltaKey( + src: T[], + dest: T[], +): { changed: boolean, data: DeltaDatum[] } +{ + const data = []; + const max_size = Math.max( dest.length, src.length ); + + let changed: boolean = false; + + for ( let i = 0; i < max_size; i++ ) + { + const dest_datum = dest[ i ]; + const src_datum = src[ i ]; + + // terminate the key if we don't have a dest value + if ( dest_datum === undefined ) + { + changed = true; + data[ i ] = null; + + break; + } + else if ( _deepEqual( dest_datum, src_datum ) ) + { + data[ i ] = undefined; + } + else + { + changed = true; + data[ i ] = dest_datum; + } + } + + return { + changed: changed, + data: data, + }; +} + + +/** + * Compare two arrays by index + * + * @param a - the first array to compare + * @param b - the second array to compare + */ +function _deepEqual( a: any, b: any ): boolean +{ + if ( Array.isArray( a ) ) + { + if ( !Array.isArray( b ) || ( a.length !== b.length ) ) + { + return false; + } + + return a.map( ( item, i ) => _deepEqual( item, b[ i ] ) ) + .every( res => res === true ); + } + + return ''+a === ''+b; +} diff --git a/test/bucket/delta.ts b/test/bucket/delta.ts new file mode 100644 index 0000000..ba1d192 --- /dev/null +++ b/test/bucket/delta.ts @@ -0,0 +1,99 @@ +/** + * Test the delta generated from two key/value stores + * + * Copyright (C) 2010-2019 R-T Specialty, LLC. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +import { createDelta as sut, Kv , DeltaResult} from "../../src/bucket/delta"; + +import { expect, use as chai_use } from 'chai'; +chai_use( require( 'chai-as-promised' ) ); + +interface SutTestCase +{ + label: string; + src_data: T; + dest_data: T; + expected: DeltaResult; +} + +describe( 'Delta', () => +{ + ( >[]>[ + { + label: "No changes are made, key is dropped", + src_data: { foo: [ 'bar', 'baz' ] }, + dest_data: { foo: [ 'bar', 'baz' ] }, + expected: {}, + }, + { + label: "Only the unchanged key is dropped", + src_data: { foo: [ 'bar', 'baz' ], bar: [ 'qwe' ] }, + dest_data: { foo: [ 'bar', 'baz' ], bar: [ 'asd' ] }, + expected: { bar: [ 'asd' ] }, + }, + { + label: "Changed values are updated by index with old value", + src_data: { foo: [ "bar", "baz", "quux" ] }, + dest_data: { foo: [ "bar", "quuux" ], moo: [ "cow" ] }, + expected: { foo: [ undefined, "quuux", null ], moo: [ "cow" ] }, + }, + { + label: "The keys are null when they don't exist in first set", + src_data: {}, + dest_data: { foo: [ "bar", "quuux" ], moo: [ "cow" ] }, + expected: { foo: [ "bar", "quuux" ], moo: [ "cow" ] }, + }, + { + label: "Removed keys in new set show up", + src_data: { foo: [ "bar" ] }, + dest_data: {}, + expected: { foo: null }, + }, + { + label: "Indexes after a null terminator aren't included", + src_data: { foo: [ "one", "two", "three", "four" ] }, + dest_data: { foo: [ "one", "done" ] }, + expected: { foo: [ undefined, "done", null ] }, + }, + { + label: "Consider nested arrays to be scalar values", + src_data: { foo: [ [ "one" ], [ "two", "three" ] ] }, + dest_data: { foo: [ [ "one" ], [ "two" ] ] }, + expected: { foo: [ undefined, [ "two" ] ] }, + }, + { + label: "Don't evaluate zeros as falsy", + src_data: { foo: [ 0 ] }, + dest_data: { foo: [ 0 ] }, + expected: {}, + }, + { + label: "Don't evaluate empty strings as falsy", + src_data: { foo: [ '' ] }, + dest_data: { foo: [ '' ] }, + expected: {}, + }, + ] ).forEach( ( { label, src_data, dest_data, expected } ) => + { + it( label, () => + { + expect( sut( src_data, dest_data ) ).to.deep.equal( expected ); + } ); + } ); +} );