deneric2
Version:
JSON parser
225 lines (201 loc) • 7.31 kB
text/typescript
import get from 'lodash/get'
import set from 'lodash/set'
import cloneDeep from 'lodash/cloneDeep'
import isObject from 'lodash/isObject'
type CommonType = StringConstructor | NumberConstructor | BooleanConstructor | ArrayConstructor | ObjectConstructor
type SingleType = CommonType | Deneric | typeof Deneric
type TDataType = SingleType | ComplexDataType
type Newable = { new(value?: any): Deneric }
export type DenericSchema = { [key: string]: [dataPath: string, dataType: TDataType, jsonIgnore?: boolean, defaultValue?: any] }
const COMMON_DATA_TYPES: CommonType[] = [String, Number, Boolean, Array, Object]
const DENERIC_SCHEMA_PATH: string = '__deneric_schema'
abstract class ComplexDataType {
abstract itemType: SingleType | ComplexDataType
isArray: boolean = false
isMap: boolean = false
}
class ArrayDataType extends ComplexDataType {
isArray = true
constructor(public itemType: SingleType | ComplexDataType) {
super()
}
}
class MapDataType extends ComplexDataType {
isMap = true
constructor(public itemType: SingleType | ComplexDataType) {
super()
}
}
const Utils = Object.freeze({
getValue(data: any, dataPath: string) {
if (!!dataPath) {
return get(data, dataPath)
}
return data
},
getValueFromJson(data: any, dataType: TDataType, defaultValue: any, strict: boolean): any {
// Handle dataType is Deneric Instance
if ((dataType as typeof Deneric).prototype instanceof Deneric) {
return new (dataType as unknown as Newable)(data)
}
// Handle dataType is ComplexDataType
if (dataType instanceof ComplexDataType) {
const complexDataType = dataType as ComplexDataType
if (complexDataType.isArray) {
return Array.isArray(data) ? data.map(item => Utils.getValueFromJson(item, complexDataType.itemType, Utils.getDefaultValue(complexDataType.itemType), strict)) : defaultValue
}
if (complexDataType.isMap && isObject(data)) {
return Object.keys(data).reduce((prev, key) => {
set(prev, key, Utils.getValueFromJson(Utils.getValue(data, key), complexDataType.itemType, Utils.getDefaultValue(complexDataType.itemType), strict))
return prev
}, {})
}
return defaultValue
}
const dataIsNil = data === null || data === undefined
// Handle dataType is not common types
if (!COMMON_DATA_TYPES.includes(dataType as CommonType)) {
return dataIsNil ? defaultValue : data
}
// Handle dataType is common types with strict rule
if (strict) {
switch (dataType) {
case String:
return typeof data === 'string' ? data : defaultValue
case Number:
return typeof data === 'number' ? data : defaultValue
case Boolean:
return typeof data === 'boolean' ? data : defaultValue
case Array:
return Array.isArray(data) ? data : defaultValue
case Object:
return isObject(data) ? data : defaultValue
}
}
// Handle dataType is common types with no-strict rule and data is not Nil
if (!strict && !dataIsNil) {
switch (dataType) {
case String:
return String(data)
case Number:
return Number(data)
case Boolean:
return Boolean(data)
case Array:
return [].concat(data)
case Object:
return Object(data)
}
}
return defaultValue
},
getValueFromDeneric(data: any, dataType: TDataType, defaultValue: any): any {
switch (dataType) {
case String:
return typeof data === 'string' ? data : defaultValue
case Number:
return typeof data === 'number' ? data : defaultValue
case Boolean:
return typeof data === 'boolean' ? data : defaultValue
case Array:
return Array.isArray(data) ? data : defaultValue
case Object:
return isObject(data) ? cloneDeep(data) : defaultValue
}
if (dataType instanceof ComplexDataType) {
const complexDataType = dataType as ComplexDataType
if (complexDataType.isArray) {
return Array.isArray(data) ? data.map(item => Utils.getValueFromDeneric(item, complexDataType.itemType, Utils.getDefaultValue(complexDataType.itemType))) : defaultValue
}
if (complexDataType.isMap) {
data = isObject(data) ? data : {}
return Object.keys(data).reduce((prev, key) => {
set(prev, key, Utils.getValueFromDeneric(data[key], complexDataType.itemType, Utils.getDefaultValue(complexDataType.itemType)))
return prev
}, {})
}
}
if ((dataType as typeof Deneric).prototype instanceof Deneric) {
return (data as Deneric).toJson()
}
return data
},
getDefaultValue(dataType: TDataType) {
if (dataType instanceof ComplexDataType) {
const complexDataType = dataType as ComplexDataType
if (complexDataType.isArray) {
return []
}
if (complexDataType.isMap) {
return {}
}
}
switch (dataType) {
case String:
return ''
case Number:
return 0
case Boolean:
return false
case Array:
return []
case Object:
return {}
}
return undefined
},
setSchema(instance: Deneric, schema: DenericSchema): void {
// if (!Object.getOwnPropertyNames(instance).includes(DENERIC_SCHEMA_PATH)) {
// Object.defineProperty(instance, DENERIC_SCHEMA_PATH,{
// enumerable: false,
// writable: true,
// })
// }
set(instance, DENERIC_SCHEMA_PATH, schema)
},
getSchema(instance: Deneric): DenericSchema {
return get(instance, DENERIC_SCHEMA_PATH) || {} as DenericSchema
}
})
abstract class Deneric {
static Array = (dataType: TDataType) => new ArrayDataType(dataType)
static Map = (valueDataType: TDataType) => new MapDataType(valueDataType)
static Utils = Utils
// private __proto__!: { schema: DenericSchema }
constructor(schema: DenericSchema) {
if (!schema) {
throw new TypeError('Invalid schema: ' + this.constructor.name)
}
Utils.setSchema(this, schema)
}
clone<T extends Deneric>(): T {
return cloneDeep(this) as unknown as T
}
fromJson<T extends Deneric>(data: any, strict: boolean = true): T {
const schema: DenericSchema = Utils.getSchema(this)
Object.keys(schema).forEach(key => {
const [dataPath, dataType] = schema[key]
let defaultValue = get(this, key) ?? Utils.getDefaultValue(dataType)
if (schema[key][3] === undefined) {
schema[key][3] = defaultValue
} else {
defaultValue = schema[key][3] ?? Utils.getDefaultValue(dataType)
}
const value = Utils.getValueFromJson(cloneDeep(Utils.getValue(data, dataPath)), dataType, defaultValue, strict)
set(this, key, value)
})
return this as unknown as T
}
toJson(): object {
const schema: DenericSchema = Utils.getSchema(this)
const json = {}
Object.keys(schema).forEach(key => {
const [dataPath, dataType, jsonIgnore, defaultValueFromSchema] = schema[key]
if (!jsonIgnore) {
set(json, dataPath, Utils.getValueFromDeneric(get(this, key), dataType, defaultValueFromSchema || Utils.getDefaultValue(dataType)))
}
})
return json
}
}
export default Deneric