Skip to content

Commit

Permalink
@dandi/model and @dandi/model-builder - add @SourceAccessor decorator…
Browse files Browse the repository at this point in the history
… to allow defining a different or more complex source for a member when constructing a model

supports #13
  • Loading branch information
DanielSchaffer committed Oct 5, 2018
1 parent b26f40b commit 7dd16f0
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 3 deletions.
1 change: 1 addition & 0 deletions model-builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './src/metadata.model.validator';
export * from './src/metadata.validation.error';
export * from './src/model.builder';
export * from './src/model.validation.error';
export * from './src/nested.key.transformer';
export * from './src/one.of.conversion.error';
export * from './src/primitive.type.converter';
export * from './src/required.property.error';
Expand Down
68 changes: 67 additions & 1 deletion model-builder/src/decorator.model.builder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Uuid } from '@dandi/common';
import { MemberMetadata, OneOf, Property } from '@dandi/model';
import { MemberMetadata, OneOf, Property, SourceAccessor } from '@dandi/model';

import { expect } from 'chai';
import { createStubInstance, SinonSpy, SinonStubbedInstance, spy, stub } from 'sinon';
Expand Down Expand Up @@ -79,6 +79,72 @@ describe('DecoratorModelBuilder', () => {
expect(secondCallValue).to.equal('123');
});

it('uses the SourceAccessorFn to access the source value if provided', () => {
class AccessorFnTest {
@SourceAccessor((source: any) => source.foo.bar)
@Property(String)
public fooBar: string;

@Property(Boolean)
public isAwesome;
}

builder.constructModel(AccessorFnTest, { foo: { bar: 'yup' }, isAwesome: 'true' });

expect((builder as any).constructMemberInternal).to.have.been.calledTwice;

const firstCallMeta: MemberMetadata = constructMember.firstCall.args[0];
expect(firstCallMeta.type).to.equal(String);

const firstCallKey = constructMember.firstCall.args[1];
expect(firstCallKey).to.equal('fooBar');

const firstCallValue = constructMember.firstCall.args[2];
expect(firstCallValue).to.equal('yup');

const secondCallMeta: MemberMetadata = constructMember.secondCall.args[0];
expect(secondCallMeta.type).to.equal(Boolean);

const secondCallKey = constructMember.secondCall.args[1];
expect(secondCallKey).to.equal('isAwesome');

const secondCallValue = constructMember.secondCall.args[2];
expect(secondCallValue).to.equal('true');
});

it('uses the SourceAccessor path to access the source value if provided', () => {
class AccessorPathTest {
@SourceAccessor('foo.bar')
@Property(String)
public fooBar: string;

@Property(Boolean)
public isAwesome;
}

builder.constructModel(AccessorPathTest, { foo: { bar: 'yup' }, isAwesome: 'true' });

expect((builder as any).constructMemberInternal).to.have.been.calledTwice;

const firstCallMeta: MemberMetadata = constructMember.firstCall.args[0];
expect(firstCallMeta.type).to.equal(String);

const firstCallKey = constructMember.firstCall.args[1];
expect(firstCallKey).to.equal('fooBar');

const firstCallValue = constructMember.firstCall.args[2];
expect(firstCallValue).to.equal('yup');

const secondCallMeta: MemberMetadata = constructMember.secondCall.args[0];
expect(secondCallMeta.type).to.equal(Boolean);

const secondCallKey = constructMember.secondCall.args[1];
expect(secondCallKey).to.equal('isAwesome');

const secondCallValue = constructMember.secondCall.args[2];
expect(secondCallValue).to.equal('true');
});

it('assigns the result of constructMemberInternal to each key', () => {
const value = { prop1: 'foo', prop2: '123' };
constructMember.restore();
Expand Down
19 changes: 18 additions & 1 deletion model-builder/src/decorator.model.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export class DecoratorModelBuilder implements ModelBuilder {

typeKeys.forEach((key) => {
key = this.transformKey(key, options);
const objValue = obj[key];
const memberMetadata = modelMetadata[key];
const objValue = this.getSourceValue(obj, key, memberMetadata);
try {
result[key] = this.constructMemberInternal(memberMetadata, this.getKey(parentKey, key), objValue, options);
} catch (err) {
Expand All @@ -52,6 +52,23 @@ export class DecoratorModelBuilder implements ModelBuilder {
return result;
}

private getSourceValue(source: any, key: string, memberMetadata: MemberMetadata): any {
if (!memberMetadata.sourceAccessor) {
return source[key];
}

if (typeof memberMetadata.sourceAccessor === 'function') {
return memberMetadata.sourceAccessor(source);
}

return memberMetadata.sourceAccessor.split('.').reduce((source, segment) => {
if (!source) {
return source;
}
return source[segment];
}, source);
}

public constructMember(metadata: MemberMetadata, key: string, value: any, options?: MemberBuilderOptions): any {
return this.constructMemberInternal(metadata, this.transformKey(key, options || {}), value, options || {});
}
Expand Down
2 changes: 2 additions & 0 deletions model-builder/src/nested.key.transformer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable } from '@dandi/core';
import { DataTransformer } from './data.transformer';

@Injectable()
export class NestedKeyTransformer implements DataTransformer {
public transform(obj: any): any {
const emptySegments = new Map<any, Set<string>>();
Expand Down
5 changes: 5 additions & 0 deletions model/src/member.metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Constructor } from '@dandi/common';

export type SourceAccessorFn = <TSource, TMember>(source: TSource) => TMember;

export type MemberSourceAccessor = string | SourceAccessorFn;

export interface MemberMetadata {
type?: Constructor<any>;
keyType?: Constructor<any>;
Expand All @@ -13,6 +17,7 @@ export interface MemberMetadata {
format?: string;
oneOf?: Array<Constructor<any>>;
json?: boolean;
sourceAccessor?: MemberSourceAccessor;
}

export interface ModelMetadata {
Expand Down
8 changes: 7 additions & 1 deletion model/src/model.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Constructor, Url } from '@dandi/common';
import { DateTime } from 'luxon';

import { EMAIL_PATTERN, URL_PATTERN } from './pattern';
import { getMemberMetadata, MemberMetadata } from './member.metadata';
import { getMemberMetadata, MemberMetadata, MemberSourceAccessor } from './member.metadata';

const EMAIL_MIN_LENGTH = 6;
const EMAIL_MAX_LENGTH = 254;
Expand Down Expand Up @@ -97,3 +97,9 @@ export function OneOf(...oneOf: Array<Constructor<any>>) {
oneOf,
});
}

export function SourceAccessor(sourceAccessor: MemberSourceAccessor): PropertyDecorator {
return modelDecorator.bind(null, {
sourceAccessor,
});
}

0 comments on commit 7dd16f0

Please sign in to comment.