export type EncodedDeviceType = string [ ] ; export type DecodedDeviceType = string [ ] [ ] ; export interface IDeviceTypeService { deviceTypes : DecodedDeviceType ; encode ( deviceTypes ) : EncodedDeviceType ; decode ( encodedDeviceType ) : DecodedDeviceType ; includesDeviceType ( current , requested ) : boolean ; includesSomeDeviceType ( current , requested ) : boolean ; validate ( deviceTypes ) : { areValid : boolean ; unknown : string [ ] ; } ; getDisplayText ( deviceType ) : string ; } export class DeviceTypeService implements IDeviceTypeService { constructor ( public readonly deviceTypes ) { if ( ! deviceTypes ) { throw new Error ( 'DeviceTypeService: deviceTypes argument is required' ) ; } } encode ( deviceTypes ) { const encoded = deviceTypes . map ( ( group ) => group . join ( ';' ) ) ; const { areValid , unknown } = this . validate ( encoded ) ; if ( ! areValid ) { throw new Error ( `DeviceTypeService::encode Unknown device types ${ unknown }` ) ; } return encoded ; } decode ( encodedDeviceType ) { const { areValid , unknown } = this . validate ( encodedDeviceType ) ; if ( ! areValid ) { throw new Error ( `DeviceTypeService::decode Unknown device types ${ unknown }` ) ; } return encodedDeviceType . map ( ( deviceType ) => deviceType . split ( ';' ) ) ; } filterForHighestSpecificity ( types ) { const decodedTypes = types . map ( ( type ) => [ ... this . decode ( [ type ] ) [ 0 ] ] ) ; return this . encode ( decodedTypes . filter ( ( type ) => ! decodedTypes . some ( ( nestedType ) => nestedType [ 0 ] === type [ 0 ] && type . length < nestedType . length ) ) ) ; } includesDeviceType ( checkedType , types ) { const highestSpecificityTypes = this . filterForHighestSpecificity ( types ) . map ( ( type ) => [ ... this . decode ( [ type ] ) [ 0 ] ] ) ; return highestSpecificityTypes . some ( ( requestedDeviceType ) => checkedType . startsWith ( this . encode ( [ requestedDeviceType ] ) [ 0 ] ) ) ; } includesSomeDeviceType ( checkedTypes , types ) { const highestSpecificityTypes = this . filterForHighestSpecificity ( types ) . map ( ( type ) => [ ... this . decode ( [ type ] ) [ 0 ] ] ) ; return highestSpecificityTypes . some ( ( requestedDeviceType ) => checkedTypes . some ( ( checkedType ) => checkedType . startsWith ( this . encode ( [ requestedDeviceType ] ) [ 0 ] ) ) ) ; } validate ( deviceTypes ) { const encoded = this . deviceTypes . map ( ( group ) => group . join ( ';' ) ) ; const unknown = [ ] ; const areValid = deviceTypes . map ( ( deviceType ) => { const valid = encoded . includes ( deviceType ) ; if ( ! valid ) { unknown . push ( deviceType ) ; } return valid ; } ) . every ( ( deviceType ) => deviceType ) ; return { areValid , unknown } ; } getDisplayText ( deviceType ) { return deviceType ? this . decode ( [ deviceType ] ) [ 0 ] . join ( ' - ' ) : 'Unknown' ; } }