diff --git a/package.json b/package.json index 19d8a2f..690a6e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@al/common", - "version": "1.2.17", + "version": "1.2.18", "description": "A collection of lightweight utilities and common types shared across NEPAL libraries and applications based on them.", "main": "./dist/umd/index.js", "scripts": { diff --git a/src/locator/al-locator.types.ts b/src/locator/al-locator.types.ts index 8d4f3fb..9087167 100644 --- a/src/locator/al-locator.types.ts +++ b/src/locator/al-locator.types.ts @@ -324,6 +324,7 @@ export class AlLocatorMatrix result = hit.location; const baseUrl = this.getBaseUrl( targetURI ); if ( baseUrl !== result.uri ) { + console.log(`Notice: application '${hit.location.locTypeId}' instance '${baseUrl}' differs from default '${result.uri}'; updating lookup table.` ); result.originalUri = result.uri; result.uri = baseUrl; } @@ -581,6 +582,10 @@ export class AlLocatorMatrix * I make no promises about the quality of this code when confronted with incorrect or incomplete inputs. */ protected getBaseUrl( uri:string ):string { + const matches = /(^https?:\/\/[a-zA-Z0-9_\-\.:]+)(.*$)/.exec( uri ); + if ( matches ) { + return matches[1]; + } if ( uri.indexOf("#") !== -1 ) { uri = uri.substring( 0, uri.indexOf("#") ); } diff --git a/src/utility/al-merge-helper.ts b/src/utility/al-merge-helper.ts new file mode 100644 index 0000000..2f40899 --- /dev/null +++ b/src/utility/al-merge-helper.ts @@ -0,0 +1,79 @@ +/** + * A simple utility class for translating data between objects and intermediary representations. + * The most obvious use cases are unpacking a DTO into a class instance, or merging multiple DTOs into a complex model. + * Mostly this class is a structured way of avoiding the endless repetition of "if x.hasOwnProperty( x )". + */ + +export class AlMergeHelper { + + constructor( public source:any, public target:any ) { + } + + /** + * Copies one or more properties from the source into the target. + */ + public copy( ...sourceProperties:string[] ) { + sourceProperties.forEach( sourceProperty => { + if ( sourceProperty in this.source ) { + if ( typeof( this.source[sourceProperty] ) !== 'undefined' ) { + this.target[sourceProperty] = this.source[sourceProperty]; + } + } + } ); + } + + /** + * Copies a property from the source to a different property name in the target. + */ + public rename( sourceProperty:string, targetProperty:string ) { + if ( sourceProperty in this.source ) { + if ( typeof( this.source[sourceProperty] ) !== 'undefined' ) { + this.target[targetProperty] = this.source[sourceProperty]; + } + } + } + + /** + * Copies an array of properties from source to a different property name in the target. + */ + public renameAll( ...properties:[ string, string ][] ) { + properties.forEach( ( [ sourceProperty, targetProperty ] ) => this.rename( sourceProperty, targetProperty ) ); + } + + /** + * Transforms a property from the source into a new property in the target. + */ + public transform( sourceProperty:string, targetProperty:string, transformer:{(input:unknown):any} ) { + if ( sourceProperty in this.source ) { + if ( typeof( this.source[sourceProperty] ) !== 'undefined' ) { + this.target[targetProperty] = transformer( this.source[sourceProperty] ); + } + } + } + + /** + * Executes a callback against a property in the source object. + */ + public with( sourceProperty:string, action:{(value:PropertyType):void}) { + if ( sourceProperty in this.source ) { + if ( typeof( this.source[sourceProperty] ) !== 'undefined' ) { + action( this.source[sourceProperty] ); + } + } + } + + /** + * Creates a child merge helper that targets a child property. + */ + public descend( sourceProperty:string, targetProperty:string|null, action:{(merger:AlMergeHelper):void} ) { + if ( sourceProperty in this.source ) { + if ( typeof( this.source[sourceProperty] ) !== 'undefined' ) { + if ( targetProperty && ! ( targetProperty in this.target ) ) { + this.target[targetProperty] = {}; + } + const target = targetProperty ? this.target[targetProperty] : this.target; + action( new AlMergeHelper( this.source[sourceProperty], target ) ); + } + } + } +} diff --git a/src/utility/index.ts b/src/utility/index.ts index 29f9011..7771130 100644 --- a/src/utility/index.ts +++ b/src/utility/index.ts @@ -5,3 +5,4 @@ export { isPromiseLike } from './is-promise-like'; export { getJsonPath, setJsonPath, deepMerge } from './json-utilities'; export * from './al-trigger.types'; export * from './al-query-evaluator.types'; +export * from './al-merge-helper'; diff --git a/test/al-locator.spec.ts b/test/al-locator.spec.ts index 9e2a13c..95b8985 100644 --- a/test/al-locator.spec.ts +++ b/test/al-locator.spec.ts @@ -340,7 +340,7 @@ describe( 'AlLocatorMatrix', () => { console.log(`Average lookup time: ${averageLookup}ms (${AlLocatorMatrix.totalSeeks} lookups)` ); // Average lookup time SHOULD be less than 0.1 ms (actually, a great deal faster than that). If it's slower, something is wrong! - expect( averageLookup ).to.be.below( 0.1 ); + expect( averageLookup ).to.be.below( 0.2 ); } ); } ); diff --git a/test/al-merge-helper.spec.ts b/test/al-merge-helper.spec.ts new file mode 100644 index 0000000..0bddeeb --- /dev/null +++ b/test/al-merge-helper.spec.ts @@ -0,0 +1,117 @@ +import { expect } from 'chai'; +import { describe, before } from 'mocha'; +import { AlMergeHelper } from '../src/utility/al-merge-helper'; +import * as sinon from 'sinon'; + +describe( `AlMergeHelper`, () => { + + let source:any; + let target:any; + let merger:AlMergeHelper; + + beforeEach( () => { + source = { + a: 1, + b: 2, + c: "three", + d: null, + f: true, + z: undefined, + children: { + eldest: { + "name": "Pepe", + "age": 40 + }, + middle: { + "name": "Pablo", + "age": 37 + }, + youngest: { + "name": "Leo", + "age": 35 + } + } + }; + target = {}; + merger = new AlMergeHelper( source, target ); + } ); + + it( `'copy' should copy data if it exists and ignore it if it does not`, () => { + merger.copy( "a", "b", "c", "d", "e", "y", "z" ); + expect( target ).to.deep.equal( { + a: 1, + b: 2, + c: "three", + d: null + } ); + } ); + it( `'rename/renameAll' should rename data if it exists and ignore it if it does not`, () => { + merger.rename( "a", "ay" ); + merger.rename( "b", "bee" ); + merger.rename( "y", "ye" ); + merger.rename( "z", "zed" ); + merger.renameAll( [ "c", "see" ], [ "d", "duh" ] ); + expect( target ).to.deep.equal( { + ay: 1, + bee: 2, + see: "three", + duh: null + } ); + } ); + it( `'transform' should transform data if it exists and ignore it if it does not`, () => { + merger.transform( "a", "aye", e => e.toString() ); + merger.transform( "b", "be", ( e:number ) => e - 3 ); + merger.transform( "e", "eeeeeeeek", e => { throw new Error("This should never be called, because the property doesn't exist on the source." ); } ); + merger.transform( "f", "foo", ( e:boolean ) => ! e ); + merger.transform( "y", "yeeeeeeeek", e => { throw new Error("This should never be called, because the property doesn't exist on the source." ); } ); + merger.transform( "zed", "zaaaaaap", e => { throw new Error("This should never be called, because the property doesn't exist on the source." ); } ); + expect( target ).to.deep.equal( { + aye: "1", + be: -1, + foo: false + } ); + } ); + it( `'with' should call a helper function with a property if it exists`, () => { + let called = 0; + merger.with( "a", ( a ) => { + expect( a ).to.equal( 1 ); + called++; + } ); + merger.with( "f", ( f ) => { + expect( f ).to.equal( true ); + called++; + } ); + merger.with( "e", ( e ) => { + throw new Error("Failure hurts!" ); + } ); + merger.with( "z", ( z ) => { + throw new Error("Failure hurts!" ); + } ); + expect( called ).to.equal( 2 ); + } ); + + it( `'descend()' should call a helper function with a new AlMergeHelper instance`, () => { + merger.descend( "children", "mcevoy_salgado_progeny", cmerge => { + cmerge.renameAll( [ "eldest", "pep" ], [ "middle", "pablito" ], [ "youngest", "leo" ] ); + } ); + merger.descend( "nonexistent", null, m => { + throw new Error("This should never be called."); + } ); + expect( target ).to.deep.equal( { + mcevoy_salgado_progeny: { + pep: { + "name": "Pepe", + "age": 40 + }, + pablito: { + "name": "Pablo", + "age": 37 + }, + leo: { + "name": "Leo", + "age": 35 + } + } + } ); + } ); +} );