Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use array to store paths #303

Merged
merged 3 commits into from
Jun 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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