Added a transformer to convert between data types

This commit is contained in:
Christopher Allford 2020-11-05 19:25:45 -08:00
parent 7287eb024c
commit 7fcbc69235
7 changed files with 243 additions and 12 deletions

View File

@ -2,12 +2,12 @@ import { ModelTransformation, ModelTransformer } from '../model-transformer';
import { DummyModel } from '../../__test_data__/dummy-model';
class DummyTransformation implements ModelTransformation {
public readonly order: number;
public readonly fromModelOrder: number;
private readonly fn: ( ( p: any ) => any ) | null;
public constructor( order: number, fn: ( ( p: any ) => any ) | null ) {
this.order = order;
this.fromModelOrder = order;
this.fn = fn;
}

View File

@ -9,12 +9,12 @@ import { Model } from '../models/model';
export interface ModelTransformation {
/**
* The order of execution for the transformer.
* - For "toModel" higher numbers execute later.
* - For "fromModel" the order is reversed.
* - For "fromModel" higher numbers execute later.
* - For "toModel" the order is reversed.
*
* @type {number}
*/
readonly order: number;
readonly fromModelOrder: number;
/**
* Performs a transformation from model properties to raw properties.
@ -34,12 +34,18 @@ export interface ModelTransformation {
}
/**
* An enum for more easily defining transformation order values.
* An enum for defining the "toModel" transformation order values.
*/
export enum TransformationOrder {
First = 0,
Normal = 500000,
Last = 1000000
Last = 1000000,
/**
* A special value reserved for transformations that MUST come after all orders due to
* the way that they destroy the property keys or values.
*/
VeryLast = 2000000
}
/**
@ -61,7 +67,7 @@ export class ModelTransformer< T extends Model > {
*/
public constructor( transformations: ModelTransformation[] ) {
// Ensure that the transformations are sorted by priority.
transformations.sort( ( a, b ) => ( a.order > b.order ) ? 1 : -1 );
transformations.sort( ( a, b ) => ( a.fromModelOrder > b.fromModelOrder ) ? 1 : -1 );
this.transformations = transformations;
}

View File

@ -0,0 +1,78 @@
import { PropertyType, PropertyTypeTransformation } from '../property-type-transformation';
describe( 'PropertyTypeTransformation', () => {
let transformation: PropertyTypeTransformation;
beforeEach( () => {
transformation = new PropertyTypeTransformation(
{
string: PropertyType.String,
integer: PropertyType.Integer,
float: PropertyType.Float,
boolean: PropertyType.Boolean,
date: PropertyType.Date,
callback: ( value: string ) => 'Transformed-' + value,
},
);
} );
it( 'should convert strings', () => {
let transformed = transformation.toModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
transformed = transformation.fromModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
} );
it( 'should convert integers', () => {
let transformed = transformation.toModel( { integer: '100' } );
expect( transformed.integer ).toStrictEqual( 100 );
transformed = transformation.fromModel( { integer: 100 } );
expect( transformed.integer ).toStrictEqual( '100' );
} );
it( 'should convert floats', () => {
let transformed = transformation.toModel( { float: '2.5' } );
expect( transformed.float ).toStrictEqual( 2.5 );
transformed = transformation.fromModel( { float: 2.5 } );
expect( transformed.float ).toStrictEqual( '2.5' );
} );
it( 'should convert booleans', () => {
let transformed = transformation.toModel( { boolean: 'true' } );
expect( transformed.boolean ).toStrictEqual( true );
transformed = transformation.fromModel( { boolean: false } );
expect( transformed.boolean ).toStrictEqual( 'false' );
} );
it( 'should convert dates', () => {
let transformed = transformation.toModel( { date: '2020-11-06T03:11:41.000Z' } );
expect( transformed.date ).toStrictEqual( new Date( '2020-11-06T03:11:41.000Z' ) );
transformed = transformation.fromModel( { date: new Date( '2020-11-06T03:11:41.000Z' ) } );
expect( transformed.date ).toStrictEqual( '2020-11-06T03:11:41.000Z' );
} );
it( 'should use conversion callbacks', () => {
let transformed = transformation.toModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
transformed = transformation.fromModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
} );
} );

View File

@ -11,7 +11,7 @@ type AdditionalProperties = { [ key: string ]: any };
* a default value if it is not already set.
*/
export class AddPropertyTransformation implements ModelTransformation {
public readonly order = TransformationOrder.Normal;
public readonly fromModelOrder = TransformationOrder.Normal;
/**
*The additional properties to add when executing toModel.

View File

@ -10,7 +10,7 @@ import { ModelTransformation } from '../model-transformer';
type TransformationCallback = ( properties: any ) => any;
export class CustomTransformation implements ModelTransformation {
public readonly order: number;
public readonly fromModelOrder: number;
/**
* The hook to run for toModel.
@ -40,7 +40,7 @@ export class CustomTransformation implements ModelTransformation {
toHook: TransformationCallback | null,
fromHook: TransformationCallback | null,
) {
this.order = order;
this.fromModelOrder = order;
this.toHook = toHook;
this.fromHook = fromHook;
}

View File

@ -17,7 +17,7 @@ export class KeyChangeTransformation< T extends Model > implements ModelTransfor
* Ensure that this transformation always happens at the very end since it changes the keys
* in the transformed object.
*/
public readonly order = TransformationOrder.Last + TransformationOrder.Last;
public readonly fromModelOrder = TransformationOrder.VeryLast + 1;
/**
* The key change transformations that this object should perform.

View File

@ -0,0 +1,147 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
/**
* An enum defining all of the property types that we might want to transform.
*
* @enum {number}
*/
export enum PropertyType {
String,
Integer,
Float,
Boolean,
Date,
}
/**
* A callback that can be used to transform property types.
*
* @callback PropertyTypeCallback
* @param {*} value The value to transform.
* @return {*} The transformed value.
*/
type PropertyTypeCallback = ( value: any ) => any;
/**
* The types for all of a model's properties.
*
* @typedef PropertyTypes
* @alias Object.<string,PropertyType>
*/
type PropertyTypes = { [ key: string ]: PropertyType | PropertyTypeCallback };
/**
* A model transformer for converting property types between representation formats.
*/
export class PropertyTypeTransformation implements ModelTransformation {
/**
* We want the type transformation to take place after all of the others,
* since they may be operating on internal data types.
*/
public readonly fromModelOrder = TransformationOrder.VeryLast;
/**
* The property types we will want to transform.
*
* @type {PropertyTypes}
* @private
*/
private readonly types: PropertyTypes;
/**
* Creates a new transformation.
*
* @param {PropertyTypes} types The property types we want to transform.
*/
public constructor( types: PropertyTypes ) {
this.types = types;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
for ( const key in this.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertFrom( value, type );
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
for ( const key in this.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertTo( value, type );
}
return properties;
}
/**
* Converts the given value into the requested type.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertTo( value: string, type: PropertyType ): any {
switch ( type ) {
case PropertyType.String: return String( value );
case PropertyType.Integer: return parseInt( value );
case PropertyType.Float: return parseFloat( value );
case PropertyType.Boolean: return Boolean( value );
case PropertyType.Date: return new Date( value );
}
}
/**
* Converts the given type into a string.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertFrom( value: any, type: PropertyType ): string {
switch ( type ) {
case PropertyType.String:
case PropertyType.Integer:
case PropertyType.Float:
case PropertyType.Boolean:
return String( value );
case PropertyType.Date: return ( value as Date ).toISOString();
}
}
}