Skip to content

Commit

Permalink
feat: Morph interface, unified mapping for nested arrays, generic types
Browse files Browse the repository at this point in the history
  • Loading branch information
cmath10 committed Feb 13, 2024
1 parent 7e509fb commit 369e753
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 104 deletions.
22 changes: 16 additions & 6 deletions src/MorphEach.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import MorphOne from '@/MorphOne'
import type { ArrayElement } from '../types/scaffolding'
import type { Morph } from '../types'

export default class MorphEach {
private readonly _morph: MorphOne
export default class MorphEach<
Source extends unknown[] = unknown[],
Target extends unknown[] = unknown[]
> implements Morph<Source, Target> {
private readonly _morph: Morph<
ArrayElement<Source>,
ArrayElement<Target>
>

constructor (morph: MorphOne) {
constructor (morph: Morph<
ArrayElement<Source>,
ArrayElement<Target>
>) {
this._morph = morph
}

apply (source: unknown[]): unknown[] {
return source.map(v => this._morph.apply(v))
convert (source: Source): Target {
return source.map(v => this._morph.convert(v as ArrayElement<Source>)) as Target
}
}
110 changes: 59 additions & 51 deletions src/MorphOne.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,116 @@
import type {
Extractor,
Injector,
Morph,
Processor,
} from '../types'

import type { Recursive } from '../types/scaffolding'
import type {
Key,
Maybe,
Recursive,
Returns,
} from '../types/scaffolding'

import extract from '@/extract'
import flatten from '@/flatten'
import inject from '@/inject'

export default class MorphOne {
private _destination: () => unknown
private readonly _extractors: Map<string, Extractor>
private readonly _injectors: Map<string, Injector>
private readonly _processors: Map<string, (MorphOne | Processor)[]>

/**
* @param {() => unknown} destination Defaults to () => ({})
*/
constructor (destination: () => unknown = () => ({})) {
this._destination = destination
this._extractors = new Map<string, Extractor>()
this._injectors = new Map<string, Injector>()
this._processors = new Map<string, (MorphOne | Processor)[]>()
}
export default class MorphOne<
Source = unknown,
Target = unknown
> implements Morph<Source, Target> {
private readonly _target: Returns<Target>
private readonly _extractors: Map<Key, Extractor<Source>>
private readonly _injectors: Map<Key, Injector>
private readonly _processors: Map<Key, (Morph | Processor)[]>

/**
* Sets callback function will be used to create destination if it not set in mapper ::map method
*
* @param {() => unknown} destination
* @param {() => unknown} target Defaults to () => ({})
*/
destination (destination: () => unknown): MorphOne {
this._destination = destination
return this
constructor (target: Returns<Target> = () => ({} as Target)) {
this._target = target
this._extractors = new Map<Key, Extractor<Source>>()
this._injectors = new Map<Key, Injector>()
this._processors = new Map<Key, (Morph | Processor)[]>()
}

apply (source: unknown, destination?: unknown) {
const dst = destination !== undefined ? destination : this._destination()
convert (source: Source, target?: Target): Target {
const t = target !== undefined ? target : this._target()

this._extractors.forEach((extract, path) => {
const _inject = this._injectors.get(path) ?? inject
const processors = this._processors.get(path) ?? []

_inject(dst, path, processors.reduce((raw, process) => {
return process instanceof MorphOne ? process.apply(raw) : process(raw)
_inject(t, path, processors.reduce((raw, process) => {
return 'convert' in process ? process.convert(raw) : process(raw)
}, extract(source) as unknown))
})

return dst
return t
}

/**
* Associate a member to another member given their property paths.
*
* @param {string} dstPath
* @param {string | string[]} srcPath
* @param {Key | Key[]} srcPath
* @param {Key} dstPath
* @param fallback
*
* @return {this} Current instance
*/
move (
srcPath: string | string[],
dstPath: string,
srcPath: Key | Key[],
dstPath: Key,
fallback: unknown = undefined
): MorphOne {
return this.extract(dstPath, ((source: unknown) => extract(source, srcPath, fallback)) as Extractor)
): MorphOne<Source, Target> {
return this.extract(dstPath, (source: Source) => extract(source, srcPath, fallback))
}

/**
* Applies a field extractor policy to a member.
*
* @param {string} dstPath
* @param {Extractor} extractor
* @param {Key} path
* @param {Extractor} by
*
* @return {this} Current instance
*/
extract (dstPath: string, extractor: Extractor): MorphOne {
this._extractors.set(dstPath, extractor)

extract (path: Key, by: Extractor<Source>): MorphOne<Source, Target> {
this._extractors.set(path, by)
return this
}

inject (dstPath: string, injector: Injector): MorphOne {
this._injectors.set(dstPath, injector)
inject (path: Key, by: Injector<Target>): MorphOne<Source, Target> {
this._injectors.set(path, by)
return this
}

/**
* Applies a processor to the field.
*
* @param {string} dstPath
* @param {Recursive<MorphOne | Processor>} processor Map name or callback or processor instance
* @param {Key} path
* @param {Recursive<Morph | Processor>} by Morph or callback
*
* @return {this} Current instance
*/
process (dstPath: string, processor: Recursive<MorphOne | Processor>): MorphOne {
if (Array.isArray(processor)) {
this._processors.set(dstPath, flatten(processor))
process (path: Key, by: Recursive<Morph | Processor>): MorphOne<Source, Target> {
if (Array.isArray(by)) {
this._processors.set(path, flatten(by))
} else {
this._processors.set(dstPath, [processor])
this._processors.set(path, [by])
}

return this
}

/**
* Removes destination member
* Excludes destination member
*
* @param {string} dstPath
* @param {Key} dstPath Member to exclude
*
* @return {this} Current instance
*/
exclude (dstPath: string): MorphOne {
exclude (dstPath: Key): MorphOne<Source, Target> {
[this._extractors, this._processors, this._injectors].forEach(map => {
if (map.has(dstPath)) {
map.delete(dstPath)
Expand All @@ -122,13 +120,23 @@ export default class MorphOne {
return this
}

clone (): MorphOne {
const morph = new MorphOne(this._destination)
override <
NewTarget = unknown,
Factory extends Maybe<Returns<NewTarget>> = undefined
> (destination: Factory = undefined): MorphOne<Source, Factory extends undefined ? Target : ReturnType<Factory>> {
const morph = new MorphOne<
Source,
Factory extends undefined ? Target : ReturnType<Factory>
>((destination ?? this._target) as Returns<Factory extends undefined ? Target : ReturnType<Factory>>)

this._extractors.forEach((extractor, path) => {
morph._extractors.set(path, extractor)
})

this._injectors.forEach((injector, key) => {
morph._injectors.set(key, injector)
})

this._processors.forEach((processors, path) => {
morph._processors.set(path, processors)
})
Expand Down
14 changes: 10 additions & 4 deletions src/extract.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Key } from '../types/scaffolding'

import fail from '@/fail'

const _guard = (source: unknown, message: string): void|never => {
Expand All @@ -6,13 +8,13 @@ const _guard = (source: unknown, message: string): void|never => {
}
}

const _has = (object: Record<string, unknown>, property: string, message?: string): void => {
const _has = (object: Record<string, unknown>, property: Key, message?: string): void => {
if (!Object.prototype.hasOwnProperty.call(object, property)) {
fail(message || 'Object has no property ' + property)
}
}

const _extract = (source: Record<string, unknown>, path: string[], prev: string[]): unknown => {
const _extract = (source: Record<string, unknown>, path: Key[], prev: Key[]): unknown => {
if (path.length === 0) {
return source
}
Expand All @@ -35,10 +37,14 @@ const _extract = (source: Record<string, unknown>, path: string[], prev: string[

export default <Source = unknown>(
source: Source,
path: string | string[],
path: Key | Key[],
fallback: unknown = undefined
): unknown => {
const _path = typeof path === 'string' ? path.split('.') : path
const _path = typeof path === 'string'
? path.split('.')
: typeof path === 'number'
? [path]
: path

try {
_guard(source, 'Path extracting not available for scalar types')
Expand Down
8 changes: 5 additions & 3 deletions src/inject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Key } from '../types/scaffolding'

import fail from '@/fail'

const _inject = (destination: Record<string, unknown>, path: string, value: unknown): void => {
const _inject = (destination: Record<Key, unknown>, path: Key, value: unknown): void => {
if (typeof destination[path] === 'function') {
const fn = destination[path] as (value: unknown) => void

Expand All @@ -10,10 +12,10 @@ const _inject = (destination: Record<string, unknown>, path: string, value: unkn
}
}

export default (destination: unknown, path: string, value: unknown): void => {
export default (destination: unknown, path: Key, value: unknown): void => {
if (typeof destination !== 'object' || destination === null) {
return fail('Scalar destinations not supported by default injector')
}

_inject(destination as Record<string, unknown>, path, value)
_inject(destination as Record<Key, unknown>, path, value)
}
Loading

0 comments on commit 369e753

Please sign in to comment.