Skip to content

Commit

Permalink
feat: use array to store paths (#303)
Browse files Browse the repository at this point in the history
* feat: use array to store paths

* refactor: change reverse loops to normal loops

* test: add override members test
  • Loading branch information
AliYusuf95 authored Jun 15, 2021
1 parent f7e89d6 commit d201093
Show file tree
Hide file tree
Showing 49 changed files with 715 additions and 328 deletions.
2 changes: 1 addition & 1 deletion packages/classes/mapped-types/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
"types": ["jest", "node", "reflect-metadata"]
},
"include": [
"**/*.spec.ts",
Expand Down
91 changes: 71 additions & 20 deletions packages/classes/src/lib/storages/class-instance.storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import type { Constructible } from '../types';

/*
# Implementation strategy
Create a tree of `Map`s, such that indexing the tree recursively (with items
of a key array, sequentially), traverses the tree, so that when the key array
is exhausted, the tree node we arrive at contains the value for that key
array under the guaranteed-unique `Symbol` key `dataSymbol`.
*/

type DataMap = Map<symbol, number>;
type PathMap = Map<string, PathMap | DataMap>;
type ArrayKeyedMap = PathMap | DataMap;
const DATA_SYMBOL = Symbol('map-data');

/**
* Internal ClassInstanceStorage
*
Expand All @@ -9,36 +22,36 @@ import type { Constructible } from '../types';
* @private
*/
export class ClassInstanceStorage {
private depthStorage = new WeakMap<Constructible, Map<string, number>>();
private depthStorage = new WeakMap<Constructible, ArrayKeyedMap>();
private recursiveCountStorage = new WeakMap<
Constructible,
Map<string, number>
ArrayKeyedMap
>();

getDepthAndCount(
parent: Constructible,
member: string
member: string[]
): [depth?: number, count?: number] {
return [this.getDepth(parent, member), this.getCount(parent, member)];
}

getDepth(parent: Constructible, member: string): number | undefined {
getDepth(parent: Constructible, member: string[]): number | undefined {
return ClassInstanceStorage.getInternal(this.depthStorage, parent, member);
}

getCount(parent: Constructible, member: string): number | undefined {
getCount(parent: Constructible, member: string[]): number | undefined {
return ClassInstanceStorage.getInternal(
this.recursiveCountStorage,
parent,
member
);
}

setDepth(parent: Constructible, member: string, depth: number): void {
setDepth(parent: Constructible, member: string[], depth: number): void {
ClassInstanceStorage.setInternal(this.depthStorage, parent, member, depth);
}

setCount(parent: Constructible, member: string, count: number): void {
setCount(parent: Constructible, member: string[], count: number): void {
ClassInstanceStorage.setInternal(
this.recursiveCountStorage,
parent,
Expand All @@ -47,7 +60,7 @@ export class ClassInstanceStorage {
);
}

resetCount(parent: Constructible, member: string): void {
resetCount(parent: Constructible, member: string[]): void {
this.setCount(parent, member, 0);
}

Expand All @@ -61,42 +74,80 @@ export class ClassInstanceStorage {
dispose(): void {
this.recursiveCountStorage = new WeakMap<
Constructible,
Map<string, number>
ArrayKeyedMap
>();
this.depthStorage = new WeakMap<Constructible, Map<string, number>>();
this.depthStorage = new WeakMap<Constructible, ArrayKeyedMap>();
}

private static getInternal(
storage: WeakMap<Constructible, Map<string, number>>,
storage: WeakMap<Constructible, ArrayKeyedMap>,
parent: Constructible,
member: string
member: string[]
): number | undefined {
const parentVal = storage.get(parent);
return parentVal ? parentVal.get(member) : undefined;
return parentVal ? arrayMapGet(parentVal, member) : undefined;
}

private static setInternal(
storage: WeakMap<Constructible, Map<string, number>>,
storage: WeakMap<Constructible, ArrayKeyedMap>,
parent: Constructible,
member: string,
member: string[],
value: number
): void {
if (!storage.has(parent)) {
storage.set(parent, new Map<string, number>().set(member, value));
storage.set(parent, arrayMapSet(new Map(), member, value))
return;
}

if (!this.hasInternal(storage, parent, member)) {
storage.get(parent)!.set(member, value);
arrayMapSet(storage.get(parent), member, value)
}
}

private static hasInternal(
storage: WeakMap<Constructible, Map<string, number>>,
storage: WeakMap<Constructible, ArrayKeyedMap>,
parent: Constructible,
member: string
member: string[]
): boolean {
const parentVal = storage.get(parent);
return parentVal ? parentVal.has(member) : false;
return parentVal ? arrayMapHas(parentVal, member) : false;
}
}

function arrayMapSet(root: ArrayKeyedMap, path: string[], value: number): ArrayKeyedMap {
let map = root;
for (const item of path) {
let nextMap = (map as PathMap).get(item) as PathMap;
if (!nextMap) {
// Create next map if none exists
nextMap = new Map();
(map as PathMap).set(item, nextMap);
}
map = nextMap;
}
// Reached end of path. Set the data symbol to the given value
(map as DataMap).set(DATA_SYMBOL, value);
return root;
}

function arrayMapHas(root: ArrayKeyedMap, path: string[]): boolean {
let map = root;
for (const item of path) {
const nextMap = (map as PathMap).get(item);
if (nextMap) {
map = nextMap;
} else {
return false;
}
}
return (map as DataMap).has(DATA_SYMBOL);
}

function arrayMapGet(root: ArrayKeyedMap, path: string[]): number | undefined {
let map = root;
for (const item of path) {
map = (map as PathMap).get(item);
if (!map) return undefined;
}
return (map as DataMap).get(DATA_SYMBOL);
}
14 changes: 7 additions & 7 deletions packages/classes/src/lib/storages/class-metadata.storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata, MetadataStorage } from '@automapper/types';
import type { Constructible } from '../types';
import { isSamePath } from '@automapper/core';

/**
* Internal ClassMetadataStorage
Expand All @@ -17,19 +18,18 @@ export class ClassMetadataStorage implements MetadataStorage<Constructible> {

getMetadata(model: Constructible): Array<Metadata<Constructible>> {
const metadataList = this.storage.get(model) ?? [];
let i = metadataList.length;

// empty metadata
if (!i) {
if (!metadataList.length) {
// try to get the metadata on the prototype of the class
return model.name ? this.getMetadata(Object.getPrototypeOf(model)) : [];
}

const resultMetadataList: Array<Metadata<Constructible>> = [];
while (i--) {
for (let i = 0; i < metadataList.length; i++) {
const metadata = metadataList[i];
// skip existing
if (resultMetadataList.some(([metaKey]) => metaKey === metadata[0])) {
if (resultMetadataList.some(([metaKey]) => isSamePath(metaKey, metadata[0]))) {
continue;
}
resultMetadataList.push(metadataList[i]);
Expand All @@ -40,9 +40,9 @@ export class ClassMetadataStorage implements MetadataStorage<Constructible> {

getMetadataForKey(
model: Constructible,
key: string
key: string[]
): Metadata<Constructible> | undefined {
return this.getMetadata(model).find(([metaKey]) => metaKey === key);
return this.getMetadata(model).find(([metaKey]) => isSamePath(metaKey, key));
}

addMetadata(model: Constructible, metadata: Metadata<Constructible>): void {
Expand All @@ -56,7 +56,7 @@ export class ClassMetadataStorage implements MetadataStorage<Constructible> {
const merged = [...protoExists, ...exists];

// if already exists, break
if (merged.some(([existKey]) => existKey === metadata[0])) {
if (merged.some(([existKey]) => isSamePath(existKey, metadata[0]))) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/classes/src/lib/utils/explore-metadata.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export function exploreMetadata(
propertyKey,
{ typeFn, depth, isGetterOnly },
] of metadataList) {
metadataStorage.addMetadata(model, [propertyKey, typeFn, isGetterOnly]);
metadataStorage.addMetadata(model, [[propertyKey], typeFn, isGetterOnly]);
if (depth != null) {
instanceStorage.setDepth(model, propertyKey, depth);
instanceStorage.setDepth(model, [propertyKey], depth);
}
}
}
Expand Down
34 changes: 17 additions & 17 deletions packages/classes/src/lib/utils/instantiate.util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
get,
setMutate,
isDateConstructor,
isDefined,
isEmpty,
isPrimitiveConstructor,
isPrimitiveConstructor
} from '@automapper/core';
import type { Dictionary } from '@automapper/types';
import type { ClassInstanceStorage, ClassMetadataStorage } from '../storages';
Expand Down Expand Up @@ -37,10 +39,9 @@ export function instantiate<TModel extends Dictionary<TModel>>(

// initialize a nestedConstructible with empty []
const nestedConstructible: unknown[] = [];
let i = metadata.length;

// reversed loop
while (i--) {
for (let i = 0; i < metadata.length; i++) {
// destructure
const [key, meta, isGetterOnly] = metadata[i];

Expand All @@ -50,25 +51,27 @@ export function instantiate<TModel extends Dictionary<TModel>>(
}

// get the value at the current key
const valueAtKey = (instance as Record<string, unknown>)[key];
const valueAtKey = get(instance as Record<string, unknown>, key);

// call the meta fn to get the metaResult of the current key
const metaResult = meta();

// if is String, Number, Boolean, Array, assign valueAtKey or undefined
// null meta means this has any type or an arbitrary object, treat as primitives
if (isPrimitiveConstructor(metaResult) || metaResult === null) {
(instance as Record<string, unknown>)[key] = isDefined(valueAtKey, true)
const value = isDefined(valueAtKey, true)
? valueAtKey
: undefined;
setMutate(instance as Record<string, unknown>, key, value);
continue;
}

// if is Date, assign a new Date value if valueAtKey is defined, otherwise, undefined
if (isDateConstructor(metaResult)) {
(instance as Record<string, unknown>)[key] = isDefined(valueAtKey)
const value = isDefined(valueAtKey)
? new Date(valueAtKey as number)
: undefined;
setMutate(instance as Record<string, unknown>, key, value);
continue;
}

Expand All @@ -79,15 +82,16 @@ export function instantiate<TModel extends Dictionary<TModel>>(
// if the value at key is an array
if (Array.isArray(valueAtKey)) {
// loop through each value and recursively call instantiate with each value
(instance as Record<string, unknown>)[key] = valueAtKey.map((val) => {
const value = valueAtKey.map((val) => {
const [instantiateResultItem] = instantiate(
instanceStorage,
metadataStorage,
metaResult as Constructible,
val
);
return instantiateResultItem;
});
})
setMutate(instance as Record<string, unknown>, key, value);
continue;
}

Expand All @@ -100,14 +104,14 @@ export function instantiate<TModel extends Dictionary<TModel>>(
metaResult as Constructible,
valueAtKey as Dictionary<unknown>
);
(instance as Record<string, unknown>)[key] = definedInstantiateResult;
setMutate(instance as Record<string, unknown>, key, definedInstantiateResult);
continue;
}

// if value is null/undefined but defaultValue is not
// should assign straightaway
if (isDefined(defaultValue)) {
(instance as Record<string, unknown>)[key] = valueAtKey;
setMutate(instance as Record<string, unknown>, key, valueAtKey);
continue;
}

Expand All @@ -117,19 +121,15 @@ export function instantiate<TModel extends Dictionary<TModel>>(

// if no depth, just instantiate with new keyword without recursive
if (depth === 0) {
(instance as Record<string, unknown>)[
key
] = new (metaResult as Constructible)();
setMutate(instance as Record<string, unknown>, key, new (metaResult as Constructible)());
continue;
}

// if depth equals count, meaning instantiate has run enough loop.
// reset the count then assign with new keyword
if (depth === count) {
instanceStorage.resetCount(model, key);
(instance as Record<string, unknown>)[
key
] = new (metaResult as Constructible)();
setMutate(instance as Record<string, unknown>, key, new (metaResult as Constructible)());
continue;
}

Expand All @@ -140,7 +140,7 @@ export function instantiate<TModel extends Dictionary<TModel>>(
metadataStorage,
metaResult as Constructible
);
(instance as Record<string, unknown>)[key] = instantiateResult;
setMutate(instance as Record<string, unknown>, key, instantiateResult);
}

// after all, resetAllCount on the current model
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export function isDestinationPathOnSource(
sourceProto: Record<string, unknown>
) {
return (sourceObj: any, sourcePath: string) => {
return !(
!sourceObj.hasOwnProperty(sourcePath) &&
!sourceProto.hasOwnProperty(sourcePath) &&
!Object.getPrototypeOf(sourceObj).hasOwnProperty(sourcePath)
return (sourceObj: any, sourcePath: string[]) => {
return sourcePath.length === 1 && !(
!sourceObj.hasOwnProperty(sourcePath[0]) &&
!sourceProto.hasOwnProperty(sourcePath[0]) &&
!Object.getPrototypeOf(sourceObj).hasOwnProperty(sourcePath[0])
);
};
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { isClass } from './is-class.util';

export function isMultipartSourcePathsInSource(
dottedSourcePaths: string[],
sourcePaths: string[],
sourceInstance: Record<string, unknown>
) {
return !(
dottedSourcePaths.length > 1 &&
(!sourceInstance.hasOwnProperty(dottedSourcePaths[0]) ||
(sourceInstance[dottedSourcePaths[0]] &&
sourcePaths.length > 1 &&
(!sourceInstance.hasOwnProperty(sourcePaths[0]) ||
(sourceInstance[sourcePaths[0]] &&
// eslint-disable-next-line @typescript-eslint/ban-types
isClass((sourceInstance[dottedSourcePaths[0]] as unknown) as Function)))
isClass((sourceInstance[sourcePaths[0]]) as Function)))
);
}
Loading

0 comments on commit d201093

Please sign in to comment.