import { entries } from "alcalzone-shared/objects" ; import { log } from "./logger" ; import { OperationProvider } from "./operation-provider" ; const METADATA_ipsoKey = Symbol ( "ipsoKey" ) ; const METADATA_required = Symbol ( "required" ) ; const METADATA_serializeWith = Symbol ( "serializeWith" ) ; const METADATA_deserializeWith = Symbol ( "deserializeWith" ) ; const METADATA_doNotSerialize = Symbol ( "doNotSerialize" ) ; export type PropertyTransformKernel = ( value : any , parent ? : IPSOObject ) => any ; export interface PropertyTransform extends PropertyTransformKernel { /** If this transform is not supposed to be skipped ever */ neverSkip : boolean ; /** If this transform requires arrays to be split */ splitArrays : boolean ; } param kernel param options function buildPropertyTransform ( kernel , options = { } ) { if ( options . splitArrays == null ) options . splitArrays = true ; if ( options . neverSkip == null ) options . neverSkip = false ; const ret = kernel as PropertyTransform ; ret . splitArrays = options . splitArrays ; ret . neverSkip = options . neverSkip ; return ret ; } export type RequiredPredicate = ( me : IPSOObject , reference ? : IPSOObject ) => boolean ; /**
 * Defines the ipso key neccessary to serialize a property to a CoAP object
 */ export const ipsoKey = ( key ) : PropertyDecorator => { return ( target , property ) => { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_ipsoKey , constr ) || { } ; metadata [ property ] = key ; metadata [ key ] = property ; Reflect . defineMetadata ( METADATA_ipsoKey , metadata , constr ) ; } ; } ; param keyOrProperty function lookupKeyOrProperty < T extends IPSOObject > ( target , keyOrProperty ) { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_ipsoKey , constr ) ; return metadata && metadata [ keyOrProperty ] ; } /**
 * Declares that a property is required to be present in a serialized CoAP object
 */ export const required = ( predicate = true ) : PropertyDecorator => { return < T extends IPSOObject > ( target , property ) => { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_required , constr ) || { } ; metadata [ property ] = predicate ; Reflect . defineMetadata ( METADATA_required , metadata , constr ) ; } ; } ; param property function isRequired < T extends IPSOObject > ( target , reference , property ) { const constr = target . constructor ; log ( `${ constr . name }: checking if ${ property } is required...` , "silly" ) ; const metadata = Reflect . getMetadata ( METADATA_required , constr ) || { } ; if ( metadata . hasOwnProperty ( property ) ) { const ret = metadata [ property as keyof typeof metadata ] ; if ( typeof ret === "boolean" ) return ret ; return ret ( target , reference ) ; } return false ; } param property function isAlwaysRequired < T extends IPSOObject > ( target , property ) { const constr = target . constructor ; log ( `${ constr . name }: checking if ${ property } is always required...` , "silly" ) ; const metadata = Reflect . getMetadata ( METADATA_required , constr ) || { } ; if ( metadata . hasOwnProperty ( property ) ) { const ret = metadata [ property as keyof typeof metadata ] ; if ( typeof ret === "boolean" ) return ret ; } return false ; } param kernel param options export function serializeWith ( kernel , options ? ) { const transform = buildPropertyTransform ( kernel , options ) ; return < T extends IPSOObject > ( target , property ) => { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_serializeWith , constr ) || { } ; metadata [ property ] = transform ; Reflect . defineMetadata ( METADATA_serializeWith , metadata , constr ) ; } ; } const defaultSerializers = { Boolean : buildPropertyTransform ( ( bool ) => bool ? 1 : 0 , { neverSkip : true } ) , } ; /**
 * Retrieves the serializer for a given property
 */ function getSerializer < T extends IPSOObject > ( target , property ) { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_serializeWith , constr ) || { } ; if ( metadata . hasOwnProperty ( property ) ) return metadata [ property ] ; const type = getPropertyType ( target , property ) ; if ( type && type . name in defaultSerializers ) { return defaultSerializers [ type . name ] ; } } param kernel param options export function deserializeWith ( kernel , options ? ) { const transform = buildPropertyTransform ( kernel , options ) ; return < T extends IPSOObject > ( target , property ) => { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_deserializeWith , constr ) || { } ; metadata [ property ] = transform ; Reflect . defineMetadata ( METADATA_deserializeWith , metadata , constr ) ; } ; } /**
 * Defines that a property will not be serialized
 */ export const doNotSerialize = < T extends IPSOObject > ( target , property ) => { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_doNotSerialize , constr ) || { } ; metadata [ property ] = true ; Reflect . defineMetadata ( METADATA_doNotSerialize , metadata , constr ) ; } ; /**
 * Checks if a given property will be serialized or not
 */ function isSerializable < T extends IPSOObject > ( target , property ) { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_doNotSerialize , constr ) || { } ; return ! metadata . hasOwnProperty ( property ) ; } const defaultDeserializers = { Boolean : buildPropertyTransform ( ( raw ) => raw === 1 || raw === "true" || raw === "on" || raw === true , { neverSkip : true } ) , } ; /**
 * Retrieves the deserializer for a given property
 */ function getDeserializer < T extends IPSOObject > ( target , property ) { const constr = target . constructor ; const metadata = Reflect . getMetadata ( METADATA_deserializeWith , constr ) || { } ; if ( metadata . hasOwnProperty ( property ) ) { return metadata [ property ] ; } const type = getPropertyType ( target , property ) ; if ( type && type . name in defaultDeserializers ) { return defaultDeserializers [ type . name ] ; } } /**
 * Finds the design type for a given property
 */ function getPropertyType < T extends IPSOObject > ( target , property ) { return Reflect . getMetadata ( "design:type" , target , property ) ; } /**
 * Allows checking if an IPSOObject was proxied and provides access
 * to the unproxied object. Usage:
 * (obj as IPSOObjectProxy<T>).isProxy;
 * (obj as IPSOObjectProxy<T>).underlyingObject;
 */ interface ProxiedIPSOObject < T extends IPSOObject > extends IPSOObject { readonly underlyingObject : T ; } /**
 * Provides a set of options regarding IPSO objects and serialization
 */ export interface IPSOOptions { /**
     * Determines if basic serializers (i.e. for simple values) should be skipped
     * This is used to support raw CoAP values instead of the simplified scales
     */ skipValueSerializers ? : boolean ; } export class IPSOObject { internal @ doNotSerialize public options : IPSOOptions ; constructor ( options = { } ) { this . options = options ; } /**
     * Reads this instance's properties from the given object
     */ public parse ( obj ) { for ( const [ key , value ] of entries ( obj ) ) { let deserializer = getDeserializer ( this , key ) ; let propName ; if ( deserializer == null ) { propName = lookupKeyOrProperty ( this , key ) ; if ( ! propName ) { log ( `found unknown property with key ${ key }` , "warn" ) ; log ( `object was: ${ JSON . stringify ( obj ) }` , "warn" ) ; continue ; } deserializer = getDeserializer ( this , propName ) ; } else { propName = key ; } const requiresArraySplitting = deserializer ? deserializer . splitArrays : true ; const parsedValue = this . parseValue ( key , value , deserializer , requiresArraySplitting ) ; this [ propName as keyof this ] = parsedValue ; } return this ; } private parseValue ( propKey , value , transform ? , requiresArraySplitting = true ) { if ( value instanceof Array && requiresArraySplitting ) { return value . map ( v => this . parseValue ( propKey , v , transform , requiresArraySplitting ) ) ; } else if ( typeof value === "object" ) { if ( transform ) { return transform ( value , this ) ; } else { log ( `could not find deserializer for key ${ propKey }` , "warn" ) ; } } else if ( transform && ( transform . neverSkip || ! this . options . skipValueSerializers ) ) { return transform ( value , this ) ; } else { return value ; } } /**
     * Overrides this object's properties with those from another partial one
     */ public merge ( obj , allProperties = false ) { for ( const [ key , value ] of entries ( obj as Record < string , any > ) ) { if ( allProperties || this . hasOwnProperty ( key ) ) { ( this as any ) [ key ] = value ; } } return this ; } /** serializes this object in order to transfer it via COAP */ public serialize ( reference ? ) { if ( this . isProxy ) return this . unproxy ( ) . serialize ( reference ) ; if ( reference != null && reference instanceof IPSOObject && reference . isProxy ) reference = reference . unproxy ( ) ; const ret = { } ; const serializeValue = ( propName , value , refValue , transform ? ) => { const _required = isRequired ( this , reference , propName ) ; let _ret = value ; if ( value instanceof IPSOObject ) { _ret = value . serialize ( refValue ) ; if ( value . isSerializedObjectEmpty ( _ret ) ) return null ; } else { if ( refValue != null ) { if ( ! _required && refValue === value ) return null ; } else { } } if ( transform && ( transform . neverSkip || ! this . options . skipValueSerializers ) ) { _ret = transform ( _ret , this ) ; } else if ( typeof _ret === "number" && this . options . skipValueSerializers ) { _ret = Math . round ( _ret ) ; } return _ret ; } ; for ( const propName of Object . keys ( this ) as ( Extract < keyof this , string > ) [ ] ) { if ( ! propName . startsWith ( "_" ) && this . hasOwnProperty ( propName ) && isSerializable ( this , propName ) ) { const key = lookupKeyOrProperty ( this , propName ) ; let value = this [ propName ] ; let refValue = null ; if ( reference != null && reference . hasOwnProperty ( propName ) ) { refValue = reference [ propName ] ; } const serializer = getSerializer ( this , propName ) ; const requiresArraySplitting = serializer ? serializer . splitArrays : true ; if ( value instanceof Array && requiresArraySplitting ) { if ( refValue != null ) { if ( ! ( refValue instanceof Array && refValue . length === value . length ) ) { throw new Error ( "cannot serialize arrays when the reference values don't match" ) ; } value = value . map ( ( v , i ) => serializeValue ( propName , v , refValue [ i ] , serializer ) ) ; } else { value = value . map ( v => serializeValue ( propName , v , null , serializer ) ) ; } value = ( value as any [ ] ) . filter ( v => v != null ) ; if ( value . length === 0 ) value = null ; } else { value = serializeValue ( propName , value , refValue , serializer ) ; } if ( ! ! key && value != null ) ret [ key ] = value ; } } return ret ; } /**
     * Deeply clones an IPSO Object
     */ public clone ( ... constructorArgs ) { type Constructable < T > = new ( options ? , ... constructorArgs ) => T ; const constructor = this . constructor as Constructable < this > ; const ret = new constructor ( this . options , ... constructorArgs ) ; const serialized = this . serialize ( ) ; return ret . parse ( serialized ) ; } private isSerializedObjectEmpty ( obj ) { for ( const key of Object . keys ( obj ) ) { const propName = lookupKeyOrProperty ( this , key ) as keyof this ; if ( ! isAlwaysRequired ( this , propName ) ) { return false ; } } return true ; } /** If this object was proxied or not */ @ doNotSerialize public readonly isProxy : boolean = false ; /** Returns the raw object without a wrapping proxy */ public unproxy ( ) { if ( this . isProxy ) { return ( this as any as ProxiedIPSOObject < this > ) . underlyingObject ; } return this ; } param get param set public createProxy ( get ? , set ? ) { return new Proxy ( this , { get : ( me , key ) => { if ( key === "isProxy" ) return true ; if ( key === "underlyingObject" ) return me ; if ( key === "unproxy" ) return me . unproxy ; if ( get != null ) return get ( me , key ) ; if ( typeof me [ key ] === "function" ) { if ( key === "clone" ) { return ( ) => me . clone ( ) . createProxy ( ) ; } else { return me [ key ] ; } } if ( me [ key ] instanceof Array && me [ key ] . length > 0 && me [ key ] [ 0 ] instanceof IPSOObject ) { return me [ key ] . map ( ( d ) => d . createProxy ( ) ) ; } if ( me [ key ] instanceof IPSOObject ) return me [ key ] . createProxy ( ) ; return me [ key ] ; } , set : ( me , key , value , receiver ) => { if ( set != null ) return set ( me , key , value , receiver ) ; me [ key ] = value ; return true ; } , } ) ; } @ doNotSerialize protected client : OperationProvider | undefined ; internal param client public link ( client ) { this . client = client ; return this ; } /**
     * Fixes property values that are known to be bugged
     */ public fixBuggedProperties ( ) { return this ; } }