From 4b40752cb96dc4b449da0bd4d982457b7cfdbe95 Mon Sep 17 00:00:00 2001 From: mgechev Date: Sat, 21 Mar 2020 23:28:10 -0700 Subject: [PATCH] refactor(devtools): type safe serialization --- .../serialized-descriptor-factory.ts | 184 +++++++----------- .../lib/state-serializer/state-serializer.ts | 27 +-- 2 files changed, 83 insertions(+), 128 deletions(-) diff --git a/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts b/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts index 0f50449b902..df7eb20e01a 100644 --- a/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts +++ b/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts @@ -1,31 +1,58 @@ import { Descriptor, NestedProp, PropType } from 'protocol'; import { METADATA_PROPERTY_NAME } from '../lview-transform'; -interface PropData { - type: PropType; +export interface CompositeType { + type: Extract; prop: any; } +export interface TerminalType { + type: Exclude; + prop: any; +} + +export type PropertyData = TerminalType | CompositeType; + +export type Formatter = { + [key in PropType]: (data: any) => Result; +}; + interface LevelOptions { currentLevel: number; level?: number; } -interface CommonTypeCases { - arrayCase?: () => any; - bigIntCase?: () => any; - booleanCase?: () => any; - stringCase?: () => any; - dateCase?: () => any; - functionCase?: () => any; - htmlElementCase?: () => any; - nullCase?: () => any; - numberCase?: () => any; - objectCase?: () => any; - symbolCase?: () => any; - undefinedCase?: () => any; - unknownCase?: () => any; -} +const serializable: { [key in PropType]: boolean } = { + [PropType.Boolean]: true, + [PropType.String]: true, + [PropType.Null]: true, + [PropType.Number]: true, + [PropType.Object]: true, + [PropType.Undefined]: true, + [PropType.Unknown]: true, + [PropType.Array]: false, + [PropType.BigInt]: false, + [PropType.Function]: false, + [PropType.HTMLNode]: false, + [PropType.Symbol]: false, + [PropType.Date]: false, +}; + +const typeToDescriptorPreview: Formatter = { + [PropType.Array]: (prop: any) => `Array(${prop.length})`, + [PropType.BigInt]: (prop: any) => truncate(prop.toString()), + [PropType.Boolean]: (prop: any) => truncate(prop.toString()), + [PropType.String]: (prop: any) => `"${prop}"`, + [PropType.Function]: (prop: any) => `${prop.name}(...)`, + [PropType.HTMLNode]: (prop: any) => prop.constructor.name, + [PropType.Null]: (_: any) => 'null', + [PropType.Number]: (prop: any) => parseInt(prop, 10).toString(), + [PropType.Object]: (prop: any) => (Object.keys(prop).length > 0 ? '{...}' : '{}'), + [PropType.Symbol]: (_: any) => 'Symbol()', + [PropType.Undefined]: (_: any) => 'undefined', + [PropType.Date]: (_: any) => 'Date()', + [PropType.Unknown]: (_: any) => 'unknown', +}; const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']); @@ -76,26 +103,25 @@ const shallowPropTypeToTreeMetaData = { }, }; -export const createShallowSerializedDescriptor = (propData: PropData): Descriptor => { +export const createShallowSerializedDescriptor = (propData: TerminalType): Descriptor => { const { type } = propData; const shallowSerializedDescriptor: Descriptor = { type, expandable: shallowPropTypeToTreeMetaData[type].expandable, editable: shallowPropTypeToTreeMetaData[type].editable, - preview: getDescriptorPreview(propData), + preview: typeToDescriptorPreview[propData.type](propData.prop), }; - const value = getShallowDescriptorValue(propData); - if (value !== undefined) { - shallowSerializedDescriptor.value = value; + if (propData.prop !== undefined && serializable[type]) { + shallowSerializedDescriptor.value = propData.prop; } return shallowSerializedDescriptor; }; export const createLevelSerializedDescriptor = ( - propData: PropData, + propData: CompositeType, levelOptions: LevelOptions, continuation: any ): Descriptor => { @@ -105,7 +131,7 @@ export const createLevelSerializedDescriptor = ( type, editable: false, expandable: Object.keys(prop).length > 0, - preview: getDescriptorPreview(propData), + preview: typeToDescriptorPreview[propData.type](propData.prop), }; if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) { @@ -119,7 +145,7 @@ export const createLevelSerializedDescriptor = ( }; export const createNestedSerializedDescriptor = ( - propData: PropData, + propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[], nestedSerializer: any @@ -130,7 +156,7 @@ export const createNestedSerializedDescriptor = ( type, editable: false, expandable: Object.keys(prop).length > 0, - preview: getDescriptorPreview(propData), + preview: typeToDescriptorPreview[propData.type](propData.prop), }; if (nodes && nodes.length) { @@ -142,83 +168,8 @@ export const createNestedSerializedDescriptor = ( return nestedSerializedDescriptor; }; -// TODO: set proper types. -const getDataByType = (type: PropType, valueByType: any, defaultValue?: any) => { - try { - switch (type) { - case PropType.Array: - return valueByType.arrayCase(); - case PropType.BigInt: - return valueByType.bigIntCase(); - case PropType.Boolean: - return valueByType.booleanCase(); - case PropType.String: - return valueByType.stringCase(); - case PropType.Function: - return valueByType.functionCase(); - case PropType.Date: - return valueByType.dateCase(); - case PropType.HTMLNode: - return valueByType.htmlElementCase(); - case PropType.Null: - return valueByType.nullCase(); - case PropType.Number: - return valueByType.numberCase(); - case PropType.Object: - return valueByType.objectCase(); - case PropType.Symbol: - return valueByType.symbolCase(); - case PropType.Undefined: - return valueByType.undefinedCase(); - case PropType.Unknown: - return valueByType.unknownCase(); - } - } catch { - return defaultValue; - } -}; - -const getDescriptorPreview = (propData: PropData) => { - const { type, prop } = propData; - - return getDataByType( - type, - { - arrayCase: () => `Array(${prop.length})`, - bigIntCase: () => truncate(prop.toString()), - booleanCase: () => truncate(prop.toString()), - stringCase: () => `"${prop}"`, - functionCase: () => `${prop.name}(...)`, - htmlElementCase: () => prop.constructor.name, - nullCase: () => 'null', - numberCase: () => parseInt(prop, 10).toString(), - objectCase: () => (Object.keys(prop).length > 0 ? '{...}' : '{}'), - symbolCase: () => 'Symbol()', - undefinedCase: () => 'undefined', - unknownCase: () => 'unknown', - }, - '' - ); -}; - -const getShallowDescriptorValue = (propData: PropData) => { - const { type, prop } = propData; - - return getDataByType( - type, - { - bigIntCase: () => prop, - booleanCase: () => prop, - stringCase: () => prop, - dateCase: () => prop, - numberCase: () => prop, - }, - undefined - ); -}; - const getNestedDescriptorValue = ( - propData: PropData, + propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[], nestedSerializer: any @@ -226,11 +177,11 @@ const getNestedDescriptorValue = ( const { type, prop } = propData; const { currentLevel } = levelOptions; - return getDataByType(type, { - arrayCase: () => - nodes.map(nestedProp => nestedSerializer(prop[nestedProp.name], nestedProp.children, currentLevel + 1)), - objectCase: () => - nodes.reduce((accumulator, nestedProp) => { + switch (type) { + case PropType.Array: + return nodes.map(nestedProp => nestedSerializer(prop[nestedProp.name], nestedProp.children, currentLevel + 1)); + case PropType.Object: + return nodes.reduce((accumulator, nestedProp) => { if ( prop.hasOwnProperty(nestedProp.name) && typeof nestedProp.name === 'string' && @@ -239,24 +190,25 @@ const getNestedDescriptorValue = ( accumulator[nestedProp.name] = nestedSerializer(prop[nestedProp.name], nestedProp.children, currentLevel + 1); } return accumulator; - }, {}), - }); + }, {}); + } }; -const getLevelDescriptorValue = (propData: PropData, levelOptions: LevelOptions, continuation: any) => { +const getLevelDescriptorValue = (propData: CompositeType, levelOptions: LevelOptions, continuation: any) => { const { type, prop } = propData; const { currentLevel, level } = levelOptions; - return getDataByType(type, { - arrayCase: () => prop.map((nested: any, idx: number) => continuation(nested, idx, currentLevel + 1, level)), - objectCase: () => - Object.keys(prop).reduce((accumulator, propName) => { + switch (type) { + case PropType.Array: + return prop.map((nested: any, idx: number) => continuation(nested, idx, currentLevel + 1, level)); + case PropType.Object: + return Object.keys(prop).reduce((accumulator, propName) => { if (typeof propName === 'string' && !ignoreList.has(propName)) { accumulator[propName] = continuation(prop[propName], propName, currentLevel + 1, level); } return accumulator; - }, {}), - }); + }, {}); + } }; const truncate = (str: string, max = 20): string => { diff --git a/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts b/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts index cb3a41728d9..772d30fbc87 100644 --- a/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts +++ b/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts @@ -3,6 +3,7 @@ import { createLevelSerializedDescriptor, createNestedSerializedDescriptor, createShallowSerializedDescriptor, + PropertyData, } from './serialized-descriptor-factory'; import { METADATA_PROPERTY_NAME } from '../lview-transform'; @@ -53,8 +54,7 @@ export const nestedSerializer = ( currentLevel = 0, level = MAX_LEVEL ): Descriptor => { - const type: PropType = getPropType(serializableInstance); - const propData = { prop: serializableInstance, type }; + const propData: PropertyData = { prop: serializableInstance, type: getPropType(serializableInstance) }; const levelOptions = { level, currentLevel }; if (currentLevel < level) { @@ -67,11 +67,13 @@ export const nestedSerializer = ( ); } - if (type === PropType.Array || type === PropType.Object) { - return createNestedSerializedDescriptor(propData, levelOptions, nodes, nestedSerializer); + switch (propData.type) { + case PropType.Array: + case PropType.Object: + return createNestedSerializedDescriptor(propData, levelOptions, nodes, nestedSerializer); + default: + return createShallowSerializedDescriptor(propData); } - - return createShallowSerializedDescriptor(propData); }; const nestedSerializerContinuation = (nodes: NestedProp[], level: number) => ( @@ -94,15 +96,16 @@ export const levelSerializer = ( level = MAX_LEVEL, continuation = levelSerializer ): Descriptor => { - const type = getPropType(serializableInstance); - const propData = { prop: serializableInstance, type }; + const propData: PropertyData = { prop: serializableInstance, type: getPropType(serializableInstance) }; const levelOptions = { level, currentLevel }; - if (type === PropType.Array || type === PropType.Object) { - return createLevelSerializedDescriptor(propData, levelOptions, continuation); + switch (propData.type) { + case PropType.Array: + case PropType.Object: + return createLevelSerializedDescriptor(propData, levelOptions, continuation); + default: + return createShallowSerializedDescriptor(propData); } - - return createShallowSerializedDescriptor(propData); }; export const serializeDirectiveState = (instance: object, levels = MAX_LEVEL): { [key: string]: Descriptor } => {