iopa
Version:
API-first, Internet of Things (IoT) stack for Typescript, official implementation of the Internet Open Protocols Alliance (IOPA) reference pattern
231 lines (208 loc) • 5.71 kB
text/typescript
/* eslint-disable no-restricted-syntax */
import type { IMap, IRef } from '@iopa/types'
interface IIopaMap<T> {
get<K extends keyof T>(key: K): T[K]
has<K extends keyof T>(key: K): boolean
set<K extends keyof T>(key: K, value: T[K])
set(value: IMapInit<any>)
default<K extends keyof T>(key: K, valueFn: T[K] | (() => T[K])): T[K]
delete<K extends keyof T>(key: K): boolean
entries(): [any, any][]
toJSON(): any
}
export type IMapInit<T> = Partial<T> | [keyof T, T[keyof T]][] | IopaMap<T>
export default class IopaMap<T> implements IMap<T> {
private static readonly _BLACK_LIST_STRINGIFY: string[] = [
'app',
'body',
'bodyUsed',
'capability',
'create',
'createChild',
'delete',
'dispose',
'get',
'headers',
'iopa.Body',
'iopa.Events',
'iopa.Headers',
'iopa.IsFinalized',
'iopa.LogStream',
'iopa.RawRequest',
'iopa.RawRequestClone',
'iopa.RawResponse',
'iopa.RawResponseClone',
'iopa.Url',
'log',
'method',
'ok',
'redirected',
'response',
'server.AbortController',
'server.AbortSignal',
'server.Capabilities',
'server.CurrentMiddleware',
'server.Environment',
'server.Events',
'server.PassThroughOnException',
'server.Trace',
'server.WaitUntil',
'set',
'setCapability',
'signal',
'toJSON',
'url'
]
public constructor(data?: IMapInit<T>, prevData?: IIopaMap<T>) {
if (prevData) {
this._loadEntries(prevData.entries())
}
if (data) {
if (Array.isArray(data)) {
this._loadEntries(data)
} else if ('entries' in data) {
this._loadEntries((data.entries as Function)())
} else {
this._loadEntries(
Object.entries(data as unknown as Record<keyof T, T[keyof T]>) as [
keyof T,
T[keyof T]
][]
)
}
}
}
public get<K extends keyof T>(key: K): T[K] {
return this[key as unknown as string]
}
public set(value: IMapInit<T>): void
public set<K extends keyof T>(key: K, value: T[K]): void
public set<K extends keyof T>(data: IMapInit<T> | K, value?: T[K]): void {
if (value || typeof data !== 'object') {
this[data as unknown as string] = value
return
}
if (Array.isArray(data)) {
this._loadEntries(data)
} else if ('entries' in data) {
this._loadEntries((data.entries as Function)())
} else {
this._loadEntries(
Object.entries(data as unknown as Record<keyof T, T[keyof T]>) as [
keyof T,
T[keyof T]
][]
)
}
}
private _loadEntries(entries: [keyof T, T[keyof T]][]): void {
for (const entry of entries) {
this.set(entry[0], entry[1])
}
}
public getRef(iRef: IRef<T>): T | undefined {
return this[iRef.id]
}
public has<K extends keyof T>(key: K): boolean {
return key in this
}
public addRef<I extends T>(iRef: IRef<T>, value: I): I {
this[iRef.id] = value
return value
}
public delete<K extends keyof T>(key: K): boolean {
if (key in this) {
delete this[key as unknown as string]
return true
}
return false
}
public default<K extends keyof T>(
key: K,
valueFn: T[K] | (() => T[K])
): T[K] {
if (key in this) {
/** noop */
} else if (typeof valueFn === 'function') {
this.set(key, (valueFn as Function)())
} else {
this.set(key, valueFn)
}
return this.get(key)
}
public entries(): [any, any][] {
return Object.entries(this) as any
}
public toString(): string {
return jsonSerialize(this.toJSON())
}
public toJSON(): T {
const jsonObj: any = {}
for (const key of Object.getOwnPropertyNames(this).filter(
(key) =>
!key.startsWith('_') &&
!IopaMap._BLACK_LIST_STRINGIFY.includes(key) &&
// eslint-disable-next-line eqeqeq
this[key] != null
)) {
if (
typeof this[key] === 'object' &&
this[key].constructor.name.toString() === 'URL'
) {
jsonObj[key] = (this[key] as URL).href
break
}
jsonObj[key] = this[key]
}
const proto1 = Object.getPrototypeOf(this)
const proto2 = Object.getPrototypeOf(proto1)
;[proto1, proto2].forEach((proto) => {
for (const key of Object.getOwnPropertyNames(proto).filter(
(key) =>
!(key in jsonObj) &&
!key.startsWith('_') &&
!IopaMap._BLACK_LIST_STRINGIFY.includes(key) &&
// eslint-disable-next-line eqeqeq
this[key] != null
)) {
const desc = Object.getOwnPropertyDescriptor(proto, key)
const hasGetter = desc && typeof desc.get === 'function'
if (hasGetter) {
const value = desc.get.call(this)
if (
typeof value === 'object' &&
value.constructor.name.toString() === 'Headers'
) {
jsonObj[key] = Object.fromEntries((value as any).entries())
break
}
jsonObj[key] = value
}
}
})
if (this['iopa.Headers']) {
jsonObj['iopa.Headers'] = Object.fromEntries(
this['iopa.Headers'].entries()
)
}
return jsonObj
}
}
function getCircularReplacer(): (key: any, value: any) => any {
const seen = new WeakSet()
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return undefined
}
seen.add(value)
if ('toJSON' in value) {
return value.toJSON()
}
}
return value
}
}
function jsonSerialize(data: any): string {
return JSON.stringify(data, getCircularReplacer(), 2)
}