/** * Type spec engine. Declare attributes using chainable syntax, * and returns object with spec. */ import { IOEndpoint } from '../io-tools'; import { definitionDecorator, EventMap, EventsDefinition, tools } from '../object-plus'; import { Transactional } from '../transactions'; import { AttributeOptions, AttributeToJSON, getMetatype, Parse, SharedType } from './metatypes'; import { AttributesContainer } from './updates'; const { assign } = tools; export interface AttributeCheck { ( value : any, key : string ) : boolean error? : any } // Infer the proper TS type from a Type-R attribute spec. export type Infer = A extends ChainableAttributeSpec ? TrueReturnType : A extends Function ? TrueReturnType : A; // Extract the proper TS return type for a function or constructor. type TrueReturnType = F extends DateConstructor ? Date : F extends ( ...args : any[] ) => infer R ? R : F extends new ( ...args : any[] ) => infer R ? R : void; export class ChainableAttributeSpec{ options : AttributeOptions & { type? : F }; constructor( options : AttributeOptions ) { // Shallow copy options, fill it with defaults. this.options = { getHooks : [], transforms : [], changeHandlers : []}; if( options ) assign( this.options, options ); } check( check : AttributeCheck, error? : any ) : this { function validate( model, value, name ){ if( !check.call( model, value, name ) ){ const msg = error || check.error || name + ' is not valid'; return typeof msg === 'function' ? msg.call( model, name ) : msg; } } const prev = this.options.validate; return this.metadata({ validate : prev ? ( function( model, value, name ){ return prev( model, value, name ) || validate( model, value, name ); } ) : validate }); } get as() : PropertyDecorator { return definitionDecorator( 'attributes', this ); } get isRequired() : this { return this.required; } get required() : this { return this.metadata({ isRequired : true }); } endpoint( endpoint : IOEndpoint ) : this { return this.metadata({ endpoint }); } watcher( ref : string | ( ( value : any, key : string ) => void ) ) : this { return this.metadata({ _onChange : ref }); } // Attribute-specific parse transform parse( fun : Parse ) : this { return this.metadata({ parse : fun }); } toJSON( fun : AttributeToJSON) : this { return this.metadata({ toJSON : typeof fun === 'function' ? fun : ( fun ? ( x, k, o ) => x && x.toJSON( o ) : emptyFunction ) }); } // Attribute get hook. get( fun ) : this { return this.metadata({ getHooks : this.options.getHooks.concat( fun ) }); } // Attribute set hook. set( fun ) : this { function handleSetHook( next, prev, record : AttributesContainer, options ) { if( this.isChanged( next, prev ) ) { const changed = fun.call( record, next, this.name ); return changed === void 0 ? prev : this.convert( changed, prev, record, options ); } return prev; } return this.metadata({ transforms : this.options.transforms.concat( handleSetHook ) }); } changeEvents( events : boolean ) : this { return this.metadata({ changeEvents : events }); } // Subsribe to events from an attribute. events( map : EventsDefinition ) : this { const eventMap = new EventMap( map ); function handleEventsSubscribtion( next, prev, record : AttributesContainer ){ prev && prev.trigger && eventMap.unsubscribe( record, prev ); next && next.trigger && eventMap.subscribe( record, next ); } return this.metadata({ changeHandlers : this.options.changeHandlers.concat( handleEventsSubscribtion ) }); } // Creates a copy of the spec. get has() : this { return this; } metadata( options : object ) : this { const cloned = new ChainableAttributeSpec( this.options ); assign( cloned.options, options ); return cloned as any; } value( x ) : this { return this.metadata({ value : x, hasCustomDefault : true }); } static from( spec : any ) : ChainableAttributeSpec { // Pass metatype through untouched... if( spec && spec instanceof ChainableAttributeSpec ) { return spec; } return typeof spec === 'function' ? type( spec ) : value( spec ); } } function emptyFunction(){} export function type( this : void, Type : ChainableAttributeSpec | F, value? : any ) : ChainableAttributeSpec { if( Type instanceof ChainableAttributeSpec ) return Type; const attrDef = new ChainableAttributeSpec({ type : Type }), defaultValue = Type && value === void 0 ? getMetatype( Type ).defaultValue : value; return defaultValue === void 0 ? attrDef : attrDef.value( defaultValue ); } export function shared( this : void, Constructor : C ) : ChainableAttributeSpec { return new ChainableAttributeSpec({ value : null, type : Constructor, _metatype : SharedType }); } // Create attribute metatype inferring the type from the value. export function value( this : void, x : any ) : ChainableAttributeSpec { const Type = inferType( x ), // Transactional types inferred from values must have shared type. AttrDef = Type && Type.prototype instanceof Transactional ? shared( Type ) : type( Type ); return AttrDef.value( x ); } function inferType( value : any ) : Function { switch( typeof value ) { case 'number' : return Number; case 'string' : return String; case 'boolean' : return Boolean; case 'function' : return Function; case 'undefined' : return void 0; case 'object' : return value ? value.constructor : void 0; } }