Skip to content

Commit 4fa3c26

Browse files
committed
1.1.0 Added by-reference mappable option. Improved tests. Decorators
context bug workaround.
1 parent c2e475b commit 4fa3c26

9 files changed

+219
-19
lines changed

.angular-cli.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
"test": {
4444
"karma": {
4545
"config": "./karma.conf.js"
46+
},
47+
"codeCoverage": {
48+
"exclude": [
49+
"src/app/test-resources/**/*"
50+
]
4651
}
4752
},
4853
"defaults": {

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ export class MyWidget {
3838
}
3939
```
4040

41+
If providing view names, then you must provide the view models collection during import (see [Setup](#Setup)). Alternatively, as of v1.1.0+, you can provide classes directly to the `@mappable`:
42+
43+
```typescript
44+
export class MyWidget {
45+
Id: number; /* not visible to the mapper. */
46+
Name: string = null; /* visible due to null default. */
47+
get Display(): string {
48+
return `${Name} (Id: ${Id})`;
49+
}
50+
51+
@mappable(MyWidget)
52+
Wiggy: MyWidget = null;
53+
}
54+
```
55+
4156
If a source property exists while a destination does not, a warning will be issued by default.
4257
You can turn this off by providing a third parameter:
4358

@@ -54,6 +69,7 @@ mapper.MapJsonToVM(MyWidget, json, false);
5469

5570
Run `npm install --save-dev simple-mapper` inside of an Angular 4 project.
5671

72+
5773
## Setup
5874
Inside your application's app.module.ts file, make the following additions.
5975

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "simple-mapper",
33
"description": "Angular 2+ object-to-view-model mapper. Intended to facilitate working with JSON from web API endpoints.",
4-
"version": "1.0.5",
4+
"version": "1.1.0",
55
"license": "MIT",
66
"repository": "https://github.com/cdibbs/simple-mapper",
77
"scripts": {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* tslint:disable:no-unused-variable */
2+
import { ClassProvider, ValueProvider } from '@angular/core';
3+
import { TestBed, async, inject } from '@angular/core/testing';
4+
import { Observable } from 'rxjs';
5+
6+
import { MapperService, MapperConfiguration} from '../services/mapper.service';
7+
import { mappable, getMappableProperties } from '../decorators/mappable.decorator';
8+
import * as vm from '../test-resources/mappable.decorator.test-vms';
9+
import { IConfig } from '../services/i';
10+
11+
12+
describe('Mappable Decorator', () => {
13+
beforeEach(() => {
14+
TestBed.configureTestingModule({
15+
providers: [
16+
]
17+
});
18+
});
19+
it('should not throw when ref types defined in order.', () => {
20+
class ByRefOutOfOrderTwo {
21+
Name: string = "";
22+
get Calculated(): string { return this.Name + " else"; }
23+
}
24+
class ByRefOutOfOrder {
25+
Id: number = 3;
26+
@mappable(ByRefOutOfOrderTwo)
27+
Two: ByRefOutOfOrder = null;
28+
}
29+
30+
expect(1).toBe(1);
31+
});
32+
it('should throw when reference types defined out-of-order.', () => {
33+
try {
34+
class ByRefOutOfOrder {
35+
Id: number = 3;
36+
@mappable(ByRefOutOfOrderTwo)
37+
Two: ByRefOutOfOrder = null;
38+
}
39+
40+
class ByRefOutOfOrderTwo {
41+
Name: string = "";
42+
get Calculated(): string { return this.Name + " else"; }
43+
}
44+
expect("").toBe("Should have thrown an error.");
45+
} catch (err) {
46+
expect(err).toBeDefined();
47+
expect(err.toString()).toContain("Did you define the type before using it?");
48+
}
49+
});
50+
});
51+
52+
describe('getMappableProperties', () => {
53+
beforeEach(() => {
54+
TestBed.configureTestingModule({
55+
providers: [
56+
{ provide: "UnheardOf", useValue: vm.UnheardOf },
57+
{ provide: "Mine", useValue: vm.Mine }
58+
]
59+
});
60+
});
61+
it('should return an empty dictionary when no mappables configured.', inject([], () => {
62+
let dict = getMappableProperties(vm.UnheardOf);
63+
expect(dict).toBeDefined();
64+
expect(Object.keys(dict).length).toBe(0);
65+
}));
66+
it('should return a dictionary with one entry when class has one mappable.', inject([], () => {
67+
console.log("Here first, but shouldn't be.");
68+
console.log("Of interest", vm.Mine);
69+
let dict = getMappableProperties(vm.Mine);
70+
console.log(dict);
71+
expect(dict).toBeDefined();
72+
expect(Object.keys(dict).length).toBe(1);
73+
console.log(typeof dict["Prop"], typeof vm.UnheardOf);
74+
expect(dict["Prop"]).toBe(vm.UnheardOf);
75+
}));
76+
it('should not return other classes mappables.', inject([], () => {
77+
let dict = getMappableProperties(vm.UnheardOf);
78+
expect(dict).toBeDefined();
79+
expect(Object.keys(dict).length).toBe(0);
80+
}));
81+
});
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { URLSearchParams } from '@angular/http';
2+
import { MappableInfo } from './mappable-info';
23

3-
const mapMetadataKey = "its.appdev.map.decorator";
4-
export function mappable<T>(type: string)
4+
const mapMetadataKey = "us.dibbern.oss.map.decorator";
5+
type decoratorInput = { new(): any } | string;
6+
7+
export function mappable(type: decoratorInput)
58
{
6-
return (target: Object, propertyKey: string | symbol) => {
7-
let d = Reflect.get(target, mapMetadataKey) || {};
9+
return function (target: Object, propertyKey: string | symbol) {
10+
if (typeof type === "undefined") {
11+
throw new Error(`Provided type for property "${propertyKey}" on "${target.constructor.name}" is undefined. Did you define the type before using it?`);
12+
}
13+
let d: { [propKey: string]: decoratorInput }
14+
= Reflect.get(target, mapMetadataKey) || {};
815
d[propertyKey] = type;
9-
//console.log(target, mapMetadataKey, d, type);
1016
Reflect.set(target, mapMetadataKey, d);
1117
};
1218
}
1319

14-
export function getMappableProperties<T>(target: Object)
15-
: { [propKey : string]: string }
20+
export function getMappableProperties(target: Object)
21+
: { [propKey : string]: decoratorInput }
1622
{
23+
if (typeof target === 'function') {
24+
target = target['prototype'];
25+
}
26+
1727
return Reflect.get(target, mapMetadataKey) || {};
1828
}

src/app/services/mapper.service.spec.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ describe('MapperService', () => {
9090
expect(result.One).toBe(json.One);
9191
expect(warned).toBeFalsy();
9292
}));
93+
it('should error when view model missing.',
94+
inject([MapperService], (mapper: MapperService) => {
95+
try {
96+
var json = {
97+
Id: 3,
98+
Prop: "something"
99+
};
100+
var result = mapper.MapJsonToVM(vm.NestedTypo, json);
101+
expect("").toBe("Should have thrown error before here");
102+
} catch(err) {
103+
expect(err).toBeTruthy();
104+
}
105+
}));
93106
it('should map nested types.', inject([MapperService], (mapper: MapperService) => {
94107
var json = {
95108
Id: 3,
@@ -105,8 +118,29 @@ describe('MapperService', () => {
105118
expect(result.Prop.Another).toBe(json.Prop.Another);
106119
expect(result.Prop.Computed).toBe("for you and me");
107120
}));
108-
109-
it('should map nested types under an observable.', inject([MapperService], (mapper: MapperService) => {
121+
it('should map nested reference types.', inject([MapperService], (mapper: MapperService) => {
122+
var json = {
123+
Id: 3,
124+
Two: { Name: "something" }
125+
};
126+
var result = mapper.MapJsonToVM(vm.ByRefNested, json);
127+
expect(result.Id).toBe(3);
128+
expect(result.Two).toBeDefined();
129+
expect(result.Two.Name).toBe(json.Two.Name);
130+
expect(result.Two.Calculated).toBe("something else");
131+
}));
132+
it('should map nested array reference types.', inject([MapperService], (mapper: MapperService) => {
133+
var json = {
134+
Id: 3,
135+
Two: [{ Name: "something" }]
136+
};
137+
var result = mapper.MapJsonToVM(vm.ByRefNestedArray, json);
138+
expect(result.Id).toBe(3);
139+
expect(result.Two).toBeDefined();
140+
expect(result.Two[0].Name).toBe(json.Two[0].Name);
141+
expect(result.Two[0].Calculated).toBe("something else");
142+
}));
143+
it('should map nested types under an observable.', async(inject([MapperService], (mapper: MapperService) => {
110144
var json = {
111145
Id: 3,
112146
One: "something else",
@@ -122,7 +156,20 @@ describe('MapperService', () => {
122156
expect(result.Prop.Another).toBe(json.Prop.Another);
123157
expect(result.Prop.Computed).toBe("for you and me");
124158
});
125-
}));
159+
})));
160+
it('should map nested reference types under an observable.', async(inject([MapperService], (mapper: MapperService) => {
161+
var json = {
162+
Id: 3,
163+
Two: { Name: "something" },
164+
};
165+
Observable.from([json]).delay(100).subscribe(j => {
166+
var result = mapper.MapJsonToVM(vm.ByRefNested, json);
167+
expect(result.Id).toBe(3);
168+
expect(result.Two).toBeTruthy();
169+
expect(result.Two.Name).toBe("something");
170+
expect(result.Two.Calculated).toBe("something else");
171+
});
172+
})));
126173
it('should map nested array types.', inject([MapperService], (mapper: MapperService) => {
127174
var json = {
128175
Id: 3,

src/app/services/mapper.service.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class MapperService implements IMapperService {
2929
*/
3030
public MapJsonToVM<T extends { [key: string]: any }>(t: { new (): T }, json: any, unmappedWarning = true): T {
3131
let vm = new t();
32-
let tprops = getMappableProperties<T>(vm);
32+
let tprops = getMappableProperties(vm);
3333
let keys = Object.keys(json || {});
3434

3535
if (unmappedWarning) {
@@ -43,21 +43,28 @@ export class MapperService implements IMapperService {
4343
let desc = Object.getOwnPropertyDescriptor(vm, prop);
4444
if (!desc || !desc.writable)
4545
continue;
46-
4746
// If there is an explicit mappable, map no matter what.
4847
if (typeof tprops[prop] === "string") {
49-
if (! this.viewModels.hasOwnProperty(tprops[prop]))
50-
this.log.warn(`View model ${tprops[prop]} does not exist. Did you type it correctly?`);
48+
let p: string = <string>tprops[prop];
49+
if (! this.viewModels.hasOwnProperty(p))
50+
throw new Error(`View model ${tprops[prop]} does not exist. Did you type it correctly?`);
5151
// If either the source or destination is iterable,
5252
// map as an iterable. This is not ideal, but neither
5353
// is Typescript. :-P
5454
if (this.iterable(vm, json, prop)) {
55-
vm[prop] = this.MapJsonToVMArray(this.viewModels[tprops[prop]], json[prop]);
55+
vm[prop] = this.MapJsonToVMArray(this.viewModels[p], json[prop]);
5656
} else {
57-
vm[prop] = this.MapJsonToVM(this.viewModels[tprops[prop]], json[prop]);
57+
vm[prop] = this.MapJsonToVM(this.viewModels[p], json[prop]);
5858
}
59-
}
60-
else if (typeof vm[prop] !== "undefined") {
59+
} else if (typeof tprops[prop] === 'function') {
60+
61+
let p: { new(): any } = <{new():any}>tprops[prop];
62+
if (this.iterable(vm, json, prop)) {
63+
vm[prop] = this.MapJsonToVMArray(p, json[prop]);
64+
} else {
65+
vm[prop] = this.MapJsonToVM(p, json[prop]);
66+
}
67+
} else if (typeof vm[prop] !== "undefined") {
6168
vm[prop] = json[prop];
6269
}
6370
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { mappable } from '../decorators/mappable.decorator';
2+
3+
// Test view models
4+
export class UnheardOf {
5+
Id: number = 3;
6+
}
7+
export class Mine {
8+
Id: number = 4;
9+
@mappable(UnheardOf)
10+
Prop: UnheardOf = null;
11+
}

src/app/test-resources/view-models.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export class Nested_Mine {
3131
Prop: Nested_MineTwo = null;
3232
}
3333

34+
export class NestedTypo {
35+
Id: number = 3;
36+
@mappable("MySillyTypo")
37+
Prop: Nested_MineTwo = null;
38+
}
39+
3440
export class Observable_MineTwo {
3541
Id: number = 3;
3642
Another: string = "for me";
@@ -54,3 +60,20 @@ export class NestedArrayTypes_Mine {
5460
@mappable("NestedArrayTypes_MineTwo")
5561
Props: NestedArrayTypes_MineTwo[] = null;
5662
}
63+
64+
export class ByRefTwo {
65+
Name: string = "";
66+
get Calculated(): string { return this.Name + " else"; }
67+
}
68+
69+
export class ByRefNested {
70+
Id: number = 3;
71+
@mappable(ByRefTwo)
72+
Two: ByRefTwo = null;
73+
}
74+
75+
export class ByRefNestedArray {
76+
Id: number = 3;
77+
@mappable(ByRefTwo)
78+
Two: ByRefTwo[] = null;
79+
}

0 commit comments

Comments
 (0)