Skip to content

Commit

Permalink
Merge pull request #16 from marquesVF/feature/build-reducer
Browse files Browse the repository at this point in the history
Feature/build reducer
  • Loading branch information
marquesVF authored Oct 20, 2020
2 parents db9ecc7 + ba926c7 commit 4b89b02
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 5 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/build-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@ export function buildObject<T>(
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<any>(objPropName.split('.'), jsonObj) !== undefined
? path<any>(objPropName.split('.'), jsonObj)
: path<any>([propertyKey], jsonObj)
Expand Down
24 changes: 24 additions & 0 deletions lib/core/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,6 +15,8 @@ export class Metadata {
readonly mapPropertyMetadata: Map<string, Array<MapPropertyMetadata<any, any>>> = new Map()
// eslint-disable-next-line max-len
readonly transformationMetadata: Map<string, Array<TransformationMetadata<any, any>>> = new Map()
// eslint-disable-next-line max-len
readonly reducerMetadata: Map<string, Array<ReductionMetadata<any>>> = new Map()

static getInstance(): Metadata {
return Metadata._instance
Expand Down Expand Up @@ -55,6 +58,19 @@ export class Metadata {
}
}

registerBuildReduction<T>(
className: string,
metadata: ReductionMetadata<T>
) {
const properties = this.reducerMetadata.get(className)

if (!properties) {
this.reducerMetadata.set(className, [metadata])
} else {
properties.push(metadata)
}
}

findProperties(
klass: ClassConstructor<any>,
namedOnly?: boolean
Expand Down Expand Up @@ -119,4 +135,12 @@ export class Metadata {
?.filter(metadata => metadata.scope === scope)
}

findReductions<T>(
klass: ClassConstructor<T>
): Array<ReductionMetadata<any>> | undefined {
const klassName = klass.name ?? klass.constructor.name

return this.reducerMetadata.get(klassName)
}

}
16 changes: 16 additions & 0 deletions lib/decorators/build-reduction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Metadata } from '../core/metadata'
import { Reducer, ReductionMetadata } from '../types/reduction'

export function BuildReduction<T>(reducer: Reducer<T>): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol) {
const className = target.constructor.name

const metadata: ReductionMetadata<T> = {
propertyKey: propertyKey.toString(),
reducer
}

Metadata.getInstance()
.registerBuildReduction(className, metadata)
}
}
1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './map-property-metadata'
export * from './object-handler-validation'
export * from './property-metadata'
export * from './transformation'
export * from './reduction'
10 changes: 10 additions & 0 deletions lib/types/reduction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type ReductionFn<T> = (obj: object) => T

export interface Reducer<T> {
reduce: ReductionFn<T>
}

export interface ReductionMetadata<T> {
propertyKey: string
reducer: Reducer<T>
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/reductionable.ts
Original file line number Diff line number Diff line change
@@ -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

}
6 changes: 3 additions & 3 deletions test/lib/property-transformation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -35,7 +35,7 @@ describe('property transformation', () => {
})
})

describe('when object is invalid', () => {
describe('when object has property with invalid type', () => {
const invalidObject = {
time: 34
}
Expand All @@ -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, { })

Expand Down
18 changes: 18 additions & 0 deletions test/lib/reduce-object.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 4b89b02

Please sign in to comment.