diff --git a/README.md b/README.md index 2194a71..0a6ab91 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,37 @@ const transformableObj: Transformable = { extractObject(transformableObj, Transformable) ``` +#### `@BuildReduction` + +Sets a reduction function. It can be useful when there's a need to combine multiple +properties from a json object into a single typed object property. + +- Parameter: + - **transformer**: a `ExtractTransformer` object. + +- Example: +```typescript +import { extractObject } from 'objectypes' + +class ReducibleObject { + @Property() + @BuildReduction({ + reducer: (obj: object): string => `${obj.firstProp} ${obj.secondProp}` + }) + reducedProp: string +} + +const jsonObject = { + firstProp: 'hello', + secondProp: 'world' +} + +// { +// reducedProp: 'hello world' +// } +buildObject(transformableObj, Transformable) +``` + ### Methods #### extractObject diff --git a/lib/build-object.ts b/lib/build-object.ts index bf0f763..72693a6 100644 --- a/lib/build-object.ts +++ b/lib/build-object.ts @@ -13,12 +13,24 @@ export function buildObject( const properties = metadataStorage.findProperties(targetKlass) const transformations = metadataStorage .findTransformations(targetKlass, 'build') + const reductions = metadataStorage.findReductions(targetKlass) if (properties) { for (const property of properties) { const { propertyKey, name, type, nullable, target } = property const objPropName = name ?? propertyKey + if (reductions) { + const reductionMetada = reductions + ?.find(metadata => metadata.propertyKey === propertyKey) + if (reductionMetada) { + const value = reductionMetada.reducer.reduce(jsonObj) + + Reflect.set(targetObj, propertyKey, value) + continue + } + } + let value = path(objPropName.split('.'), jsonObj) !== undefined ? path(objPropName.split('.'), jsonObj) : path([propertyKey], jsonObj) diff --git a/lib/core/metadata.ts b/lib/core/metadata.ts index 15838dc..4896747 100644 --- a/lib/core/metadata.ts +++ b/lib/core/metadata.ts @@ -3,6 +3,7 @@ import { ClassConstructor } from '../types/class-constructor' import { MapPropertyMetadata } from '../types/map-property-metadata' import { TransformationMetadata, TransformationScope } from '../types/transformation' +import { ReductionMetadata } from '../types' // TODO refactor this class - too many similar code export class Metadata { @@ -14,6 +15,8 @@ export class Metadata { readonly mapPropertyMetadata: Map>> = new Map() // eslint-disable-next-line max-len readonly transformationMetadata: Map>> = new Map() + // eslint-disable-next-line max-len + readonly reducerMetadata: Map>> = new Map() static getInstance(): Metadata { return Metadata._instance @@ -55,6 +58,19 @@ export class Metadata { } } + registerBuildReduction( + className: string, + metadata: ReductionMetadata + ) { + const properties = this.reducerMetadata.get(className) + + if (!properties) { + this.reducerMetadata.set(className, [metadata]) + } else { + properties.push(metadata) + } + } + findProperties( klass: ClassConstructor, namedOnly?: boolean @@ -119,4 +135,12 @@ export class Metadata { ?.filter(metadata => metadata.scope === scope) } + findReductions( + klass: ClassConstructor + ): Array> | undefined { + const klassName = klass.name ?? klass.constructor.name + + return this.reducerMetadata.get(klassName) + } + } diff --git a/lib/decorators/build-reduction.ts b/lib/decorators/build-reduction.ts new file mode 100644 index 0000000..c2d9a2a --- /dev/null +++ b/lib/decorators/build-reduction.ts @@ -0,0 +1,16 @@ +import { Metadata } from '../core/metadata' +import { Reducer, ReductionMetadata } from '../types/reduction' + +export function BuildReduction(reducer: Reducer): PropertyDecorator { + return function (target: Object, propertyKey: string | symbol) { + const className = target.constructor.name + + const metadata: ReductionMetadata = { + propertyKey: propertyKey.toString(), + reducer + } + + Metadata.getInstance() + .registerBuildReduction(className, metadata) + } +} diff --git a/lib/types/index.ts b/lib/types/index.ts index a15beef..3c09198 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -4,3 +4,4 @@ export * from './map-property-metadata' export * from './object-handler-validation' export * from './property-metadata' export * from './transformation' +export * from './reduction' diff --git a/lib/types/reduction.ts b/lib/types/reduction.ts new file mode 100644 index 0000000..a3229c8 --- /dev/null +++ b/lib/types/reduction.ts @@ -0,0 +1,10 @@ +export type ReductionFn = (obj: object) => T + +export interface Reducer { + reduce: ReductionFn +} + +export interface ReductionMetadata { + propertyKey: string + reducer: Reducer +} diff --git a/package-lock.json b/package-lock.json index 9896081..c3635a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "objectypes", - "version": "1.1.8", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3a43035..7f75af9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "objectypes", - "version": "1.1.8", + "version": "1.2.0", "description": "A type-safe library to transform and validate objects", "files": [ "dist" diff --git a/test/fixtures/reductionable.ts b/test/fixtures/reductionable.ts new file mode 100644 index 0000000..3c6d60b --- /dev/null +++ b/test/fixtures/reductionable.ts @@ -0,0 +1,12 @@ +import { Property } from '../../lib' +import { BuildReduction } from '../../lib/decorators/build-reduction' + +export class ReducibleObject { + + @Property() + @BuildReduction({ + reduce: (obj: any) => `${obj.firstProp} ${obj.secondProp}` + }) + combinedProp: string + +} diff --git a/test/lib/property-transformation.spec.ts b/test/lib/property-transformation.spec.ts index 2443541..1aea451 100644 --- a/test/lib/property-transformation.spec.ts +++ b/test/lib/property-transformation.spec.ts @@ -22,7 +22,7 @@ describe('property transformation', () => { }) describe('when building the object', () => { - describe('when object is valid', () => { + describe('when object has all required properties with expected types', () => { const jsonObject = { time: '2020-07-06T20:28:18.256Z', code: '11-11' @@ -35,7 +35,7 @@ describe('property transformation', () => { }) }) - describe('when object is invalid', () => { + describe('when object has property with invalid type', () => { const invalidObject = { time: 34 } @@ -47,7 +47,7 @@ describe('property transformation', () => { }) }) - describe('when typed object has a transformation for a optional property', () => { + describe('when typed object has a transformation for an optional property', () => { it('should not execute the transformation if property is undefined', () => { const builder = () => buildObject(OptionalBuildModel, { }) diff --git a/test/lib/reduce-object.spec.ts b/test/lib/reduce-object.spec.ts new file mode 100644 index 0000000..21e45c2 --- /dev/null +++ b/test/lib/reduce-object.spec.ts @@ -0,0 +1,18 @@ +import { buildObject } from '../../lib' +import { ReducibleObject } from '../fixtures/reductionable' + +describe('object reduction', () => { + const jsonObject = { + firstProp: 'hello', + secondProp: 'world' + } + const expectedObj: ReducibleObject = { + combinedProp: 'hello world' + } + + it('should use both properties in the transformation', () => { + const result = buildObject(ReducibleObject, jsonObject) + + expect(result).toEqual(expectedObj) + }) +})