Skip to content

Instantly share code, notes, and snippets.

@zett42
Last active January 4, 2020 23:27
Show Gist options
  • Save zett42/bbd427e66cfabe3045ed73761d386a91 to your computer and use it in GitHub Desktop.
Save zett42/bbd427e66cfabe3045ed73761d386a91 to your computer and use it in GitHub Desktop.
@SerializeEnumWrapper - a decorator function for enum wrapper classes that adds metadata for serialization and reflection
/*
Copyright (c) 2020 zett42.
https://github.com/zett42
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Serialize, deflate, inflate } from 'serialazy'
import { SerializeEnumWrapper, getEnumValuesMetadata, getEnumValuesDescriptorMetadata } from './SerializeEnumWrapper'
//==============================================================================================================
// Declarations
enum BatVehicleEnum {
Batmobile = 42,
Batpod,
Batplane,
}
enum BatVehicleEnumStr {
Batmobile = "TheBatmobile",
Batpod = "TheBatpod",
Batplane = "TheBatplane",
}
//--------------------------------------------------------------------------------------------------------------
// Wrap enum with default options
@SerializeEnumWrapper( BatVehicleEnum )
class BatVehicle {
constructor( public value: BatVehicleEnum = BatVehicleEnum.Batmobile ) {}
}
@SerializeEnumWrapper( BatVehicleEnumStr )
class BatVehicleStr {
constructor( public value: BatVehicleEnumStr = BatVehicleEnumStr.Batmobile ) {}
}
//--------------------------------------------------------------------------------------------------------------
// Wrap enum with non-default toJsonAsKey option
@SerializeEnumWrapper( BatVehicleEnum, { toJsonAsKey: true } )
class BatVehicle_toJsonAsKey_true {
constructor( public value: BatVehicleEnum = BatVehicleEnum.Batmobile ) {}
}
@SerializeEnumWrapper( BatVehicleEnumStr, { toJsonAsKey: true } )
class BatVehicleStr_toJsonAsKey_true {
constructor( public value: BatVehicleEnumStr = BatVehicleEnumStr.Batmobile ) {}
}
//--------------------------------------------------------------------------------------------------------------
// Wrap enum with non-default serializeAsKey option
@SerializeEnumWrapper( BatVehicleEnum, { serializeAsKey: false } )
class BatVehicle_serializeAsKey_false {
constructor( public value: BatVehicleEnum = BatVehicleEnum.Batmobile ) {}
}
@SerializeEnumWrapper( BatVehicleEnumStr, { serializeAsKey: false } )
class BatVehicleStr_serializeAsKey_false {
constructor( public value: BatVehicleEnumStr = BatVehicleEnumStr.Batmobile ) {}
}
//--------------------------------------------------------------------------------------------------------------
// Wrap enum with metadata per value
@SerializeEnumWrapper( BatVehicleEnum, {
valuesDescriptor: {
Batmobile: "it has four wheels",
Batpod: "it has two wheels",
Batplane: { amazing: "it can fly" },
}
})
class BatVehicle_valuesDescriptor {
constructor( public value: BatVehicleEnum = BatVehicleEnum.Batmobile ) {}
}
@SerializeEnumWrapper( BatVehicleEnumStr, {
valuesDescriptor: {
Batmobile: "it has four wheels",
Batpod: "it has two wheels",
Batplane: { amazing: "it can fly" },
}
})
class BatVehicleStr_valuesDescriptor {
constructor( public value: BatVehicleEnumStr = BatVehicleEnumStr.Batmobile ) {}
}
//--------------------------------------------------------------------------------------------------------------
// Include enum wrapper as member in serializable class.
class Batman {
@Serialize()
currentVehicle: BatVehicle = new BatVehicle( BatVehicleEnum.Batplane );
}
class Batman_serializeAsKey_false {
@Serialize()
currentVehicle: BatVehicle_serializeAsKey_false = new BatVehicle_serializeAsKey_false( BatVehicleEnum.Batplane );
}
class BatmanStr {
@Serialize()
currentVehicle: BatVehicleStr = new BatVehicleStr( BatVehicleEnumStr.Batplane );
}
class BatmanStr_serializeAsKey_false {
@Serialize()
currentVehicle: BatVehicleStr_serializeAsKey_false = new BatVehicleStr_serializeAsKey_false( BatVehicleEnumStr.Batplane );
}
//==============================================================================================================
// JEST TEST SUITE
describe( 'SerializeEnumWrapper', () => {
it('provides standard methods', () => {
const e = new BatVehicle( BatVehicleEnum.Batpod );
expect( e.value ).toBe( BatVehicleEnum.Batpod );
expect( e.valueOf() ).toBe( BatVehicleEnum.Batpod );
expect( e.toString() ).toBe( "43" );
});
it('provides standard methods for string enum', () => {
const e = new BatVehicleStr( BatVehicleEnumStr.Batpod );
expect( e.value ).toBe( BatVehicleEnumStr.Batpod );
expect( e.valueOf() ).toBe( BatVehicleEnumStr.Batpod );
expect( e.toString() ).toBe( "TheBatpod" );
});
//--------------------------------------------------------------------------------------------------------------
it('converts enum value to JSON', () => {
const e_default = new BatVehicle( BatVehicleEnum.Batpod );
const e_toJsonAsKey_true = new BatVehicle_toJsonAsKey_true( BatVehicleEnum.Batpod );
expect( JSON.stringify( e_default ) ).toBe( '43' );
expect( JSON.stringify( e_toJsonAsKey_true ) ).toBe( '"Batpod"' );
});
it('converts string enum value to JSON', () => {
const e_default = new BatVehicleStr( BatVehicleEnumStr.Batpod );
const e_toJsonAsKey_true = new BatVehicleStr_toJsonAsKey_true( BatVehicleEnumStr.Batpod );
expect( JSON.stringify( e_default ) ).toBe( '"TheBatpod"' );
expect( JSON.stringify( e_toJsonAsKey_true ) ).toBe( '"Batpod"' );
});
//--------------------------------------------------------------------------------------------------------------
it('returns enum metadata: values', () => {
const e = new BatVehicle( BatVehicleEnum.Batpod );
const values = getEnumValuesMetadata( e );
expect( values ).toStrictEqual({ 42: "Batmobile", 43: "Batpod", 44: "Batplane", Batmobile: 42, Batpod: 43, Batplane: 44 });
});
//--------------------------------------------------------------------------------------------------------------
it('returns enum metadata: valuesDescriptor', () => {
const e = new BatVehicle_valuesDescriptor( BatVehicleEnum.Batpod );
const values = getEnumValuesMetadata( e );
expect( values ).toStrictEqual({ 42: "Batmobile", 43: "Batpod", 44: "Batplane", Batmobile: 42, Batpod: 43, Batplane: 44 });
const descriptor = getEnumValuesDescriptorMetadata( e );
expect( descriptor ).toStrictEqual({
Batmobile: "it has four wheels",
Batpod: "it has two wheels",
Batplane: { amazing: "it can fly" },
})
});
//--------------------------------------------------------------------------------------------------------------
it('serializes enum with default options', () => {
const original = new Batman;
const serialized = deflate( original );
expect( serialized ).toStrictEqual({ currentVehicle: "Batplane" });
const cloned = inflate( Batman, serialized );
expect( cloned ).toStrictEqual( original );
});
//--------------------------------------------------------------------------------------------------------------
it('serializes string enum with default options', () => {
const original = new BatmanStr;
const serialized = deflate( original );
expect( serialized ).toStrictEqual({ currentVehicle: "Batplane" });
const cloned = inflate( BatmanStr, serialized );
expect( cloned ).toStrictEqual( original );
});
//--------------------------------------------------------------------------------------------------------------
it('serializes enum as value', () => {
const original = new Batman_serializeAsKey_false;
const serialized = deflate( original );
expect( serialized ).toStrictEqual({ currentVehicle: 44 });
const cloned = inflate( Batman_serializeAsKey_false, serialized );
expect( cloned ).toStrictEqual( original );
});
//--------------------------------------------------------------------------------------------------------------
it('serializes string enum as value', () => {
const original = new BatmanStr_serializeAsKey_false;
const serialized = deflate( original );
expect( serialized ).toStrictEqual({ currentVehicle: "TheBatplane" });
const cloned = inflate( BatmanStr_serializeAsKey_false, serialized );
expect( cloned ).toStrictEqual( original );
})
});
/*
Copyright (c) 2020 zett42.
https://github.com/zett42
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Serialize } from 'serialazy'
import Constructable from 'serialazy/lib/dist/types/constructable'
export interface IEnumWrapper< EnumT > {
value: EnumT;
}
export const EnumValuesMetadataKey = Symbol("EnumValuesMetadataKey");
export const EnumValuesDescriptorMetadataKey = Symbol("EnumValuesDescriptorMetadataKey");
//----------------------------------------------------------------------------------------------
/**
* Serialization and reflection decorator for an enum wrapper class (IEnumWrapper).
*
* @remarks
* To be able to define custom serialization and store additional metadata for enum types,
* enums must be wrapped in a class of type IEnumWrapper. This is necessary, because a native enum
* cannot have decorators and its "design:type" metadata equals "Number", meaning the original type
* information is lost at runtime.
*
* This decorator does the following:
* - Serialize enum as the key (optionally as value, see options parameter)
* - Override standard prototype methods of the wrapper class to "unwrap" the native enum value:
* valueOf(), toString(), toJSON()
* - Adds a metadata property (key EnumValuesMetadataKey) to the prototype of the wrapper
* class which stores all enum keys and values.
* It can be retrieved from an instance of IEnumWrapper like this:
* getEnumValuesMetadata( wrappedEnumValue )
* - Optionally adds a metadata property (key EnumValuesDescriptorMetadataKey) to the prototype
* of the wrapper class which stores custom metadata per enum key.
* It can be retrieved from an instance of IEnumWrapper like this:
* getEnumValuesDescriptorMetadata( wrappedEnumValue )
*
* @argument enumValues The value of the enum type (an object mapping enum keys to values)
* @argument options {
* serializeAsKey: boolean // if true (default), serialize key, otherwise serialize value of enum
* toJsonAsKey: boolean // if false (default), JSON.stringify() uses value of enum, otherwise uses key of enum
* valuesDescriptor: object // map each enum key to any value or object that defines your metadata
* }
*
* @returns Decorator factory.
**/
export function SerializeEnumWrapper< EnumT, KeysT extends string >(
enumValues : { [ key in KeysT ]: EnumT },
options: {
serializeAsKey?: boolean,
toJsonAsKey?: boolean,
valuesDescriptor?: { [ key in KeysT ]: any }
} = {} )
{
return function( targetCtor: Constructable.Default< IEnumWrapper< EnumT > > ) {
function lookupEnumKeyByValue( value: EnumT ) {
if( typeof value === "number" ) {
// Lookup enum key by numeric value.
return ( enumValues as any )[ value ];
}
// String enums don't have reverse mapping, so we use find instead. This inefficient linear search,
// which could be improved by building our own reverse lookup map.
const found = Object.entries( enumValues ).find( elem => elem[ 1 ] === value );
return found ? found[ 0 ] : undefined;
}
// Override standard prototype functions to provide facilities similar to build-in wrapper
// classes like Number.
targetCtor.prototype.valueOf = function() { return this.value };
targetCtor.prototype.toString = function() { return this.value.toString() };
// For JSON return the enum value by default. JSON represantation is often used to compare objects or log
// them to the console, where we usually want to get the actual enum values, not the keys.
targetCtor.prototype.toJSON = function() { return options.toJsonAsKey
? lookupEnumKeyByValue( this.value )
: this.value }
// Attach enum metadata to the prototype of the wrapper class.
Reflect.defineMetadata( EnumValuesMetadataKey, enumValues, targetCtor.prototype );
if( options.valuesDescriptor ) {
Reflect.defineMetadata( EnumValuesDescriptorMetadataKey, options.valuesDescriptor, targetCtor.prototype );
}
// Define serialization for the wrapper class.
// Serialize the enum key by default, which is more useful in most cases. Serialization should be reproducible
// even when new values are inserted into an enum type.
const serializeAsKey = ( typeof options.serializeAsKey === "undefined" || options.serializeAsKey );
const serializeDecorator = Serialize.Type({
down: ( val: IEnumWrapper< EnumT > ) : string => serializeAsKey
? lookupEnumKeyByValue( val.value )
: val.value,
up: ( val: string | number ) : IEnumWrapper< EnumT > => {
const result = new targetCtor();
result.value = serializeAsKey
? ( enumValues as any )[ val ]
: val as unknown as EnumT;
return result;
}
});
serializeDecorator( targetCtor );
}
}
//----------------------------------------------------------------------------------------------
/**
* Get the enum keys and values meta data from an instance of IEnumWrapper.
* @param wrappedEnumValue Instance of IEnumWrapper
*/
export function getEnumValuesMetadata< EnumT >( wrappedEnumValue : IEnumWrapper< EnumT > ) {
return Reflect.getMetadata( EnumValuesMetadataKey, Object.getPrototypeOf( wrappedEnumValue ) );
}
//----------------------------------------------------------------------------------------------
/**
* Get optional metadata per enum key from an instance of IEnumWrapper.
* @param wrappedEnumValue Instance of IEnumWrapper
*/
export function getEnumValuesDescriptorMetadata< EnumT >( wrappedEnumValue : IEnumWrapper< EnumT > ) {
return Reflect.getMetadata( EnumValuesDescriptorMetadataKey, Object.getPrototypeOf( wrappedEnumValue ) );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment