Skip to content

Commit d74ff7f

Browse files
First set of tests, Collection is now order-preserving, custom json can use primitives, getters without setters
1 parent 9cb7f22 commit d74ff7f

9 files changed

+276
-28
lines changed

.travis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
language: node_js
2+
install:
3+
- npm install
4+
script: npm run all
5+
after_success:
6+
- cat ./coverage/lcov.info|./node_modules/coveralls/bin/coveralls.js
7+
node_js:
8+
- 6

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,4 @@ When the type changes, the previous instance has its `dispose` method called, if
110110
## Undo
111111

112112
When you construct an `Undo` object you pass it the root object-with-a-`json`-property and it immediately captures the current state. It does this inside `autorun`, so if the state changes it will be recaptured. The second time this happens, the previous state is pushed onto the undo stack. `Undo` has public properties `canUndo` and `canRedo`, and methods `undo` and `redo`, so you can link those up to a couple of toolbar buttons in an editor.
113+

package.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
{
22
"name": "json-mobx",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "Simple undo/redo and persistence for MobX",
55
"main": "built/index.js",
66
"types": "built/index.d.ts",
77
"scripts": {
8-
"test": "echo \"Error: no test specified\" && exit 1",
9-
"prepublish": "tsc"
8+
"build": "tsc",
9+
"test": "tape built/test/**/*.js",
10+
"coverage": "istanbul cover tape built/test/**/*.js",
11+
"prepublish": "npm run build && npm run test",
12+
"all": "npm run build && npm run test && npm run coverage"
1013
},
1114
"repository": {
1215
"type": "git",
1316
"url": "git+https://github.com/danielearwicker/json-mobx.git"
1417
},
18+
"keywords": [
19+
"mobx",
20+
"persistence",
21+
"json"
22+
],
1523
"author": "Daniel Earwicker <[email protected]>",
1624
"license": "MIT",
1725
"dependencies": {
1826
"mobx": "^3.0.0"
1927
},
2028
"devDependencies": {
21-
"typescript": "^2.1.5"
29+
"@types/blue-tape": "^0.1.30",
30+
"blue-tape": "^1.0.0",
31+
"coveralls": "^2.11.16",
32+
"istanbul": "^0.4.5",
33+
"typescript": "^2.1.6"
2234
}
2335
}

src/Collection.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,46 @@ export class Collection<T extends (Identified & Partial<Disposable>)> {
1515
@computed get json() {
1616
return this.items.map(json.save);
1717
}
18-
set json(data) {
19-
// Do a simple diff/merge to avoid recreating items unnecessarily
20-
const existing: { [id: string]: boolean } = {};
21-
for (let i = 0; i < this.items.length; i++) {
22-
const item = this.items[i];
23-
const itemJson = data.find(w => w.id === item.id);
24-
if (itemJson) {
18+
set json(data) {
19+
20+
// Build map of existing items by id (check for uniqueness)
21+
const existing: { [id: string]: T } = {};
22+
for (const item of this.items) {
23+
if (existing[item.id]) {
24+
throw new Error(`Duplicate item id ${item.id}`);
25+
}
26+
existing[item.id] = item;
27+
}
28+
29+
// Bring into line with supplied data
30+
if (data && Array.isArray(data)) {
31+
for (let i = 0; i < data.length; i++) {
32+
const itemJson = data[i];
33+
34+
// Reuse existing item with same id
35+
let item = existing[itemJson.id];
36+
if (item) {
37+
delete existing[itemJson.id];
38+
} else {
39+
item = this.factory();
40+
}
41+
2542
json.load(item, itemJson);
26-
existing[item.id] = true;
27-
} else {
28-
if (item.dispose) {
29-
item.dispose();
30-
}
31-
this.items.splice(i, 1);
32-
i--;
43+
if (item.id !== itemJson.id) {
44+
throw new Error("Items must have persistent id property");
45+
}
46+
47+
this.items[i] = item;
3348
}
49+
} else {
50+
this.items.length = 0;
3451
}
35-
for (const itemJson of data) {
36-
if (!existing[itemJson.id]) {
37-
const newItem = this.factory();
38-
json.load(newItem, itemJson);
39-
this.items.push(newItem);
52+
53+
// Dispose any items not reused
54+
for (const key of Object.keys(existing)) {
55+
const item = existing[key];
56+
if (item.dispose) {
57+
item.dispose();
4058
}
4159
}
4260
}

src/json.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ import { computed, IComputedValue } from "mobx";
33
const jsonPropertiesKey = "$jsonProperties";
44
const jsonJsonKey = "$jsonJson";
55

6-
function jsonImpl(prototype: any, propertyName: any) {
6+
function getPropertyDescriptor(obj: any, propertyName: string) {
7+
while (obj) {
8+
const desc = Object.getOwnPropertyDescriptor(obj, propertyName);
9+
if (desc) {
10+
return desc;
11+
}
12+
obj = Object.getPrototypeOf(obj);
13+
}
14+
return undefined;
15+
}
16+
17+
function jsonImpl(prototype: any, propertyName: string) {
718

819
function getJsonComputed(that: any) {
920

@@ -21,10 +32,13 @@ function jsonImpl(prototype: any, propertyName: any) {
2132
for (const propertyName of that[jsonPropertiesKey]) {
2233
const source = data[propertyName];
2334
const target = that[propertyName];
24-
if (target && typeof target === "object" && "json" in target) {
35+
if (source && target && typeof target === "object" && "json" in target) {
2536
target.json = source;
2637
} else {
27-
that[propertyName] = source;
38+
const prop = getPropertyDescriptor(that, propertyName);
39+
if (!prop || prop.set || !prop.get) {
40+
that[propertyName] = source;
41+
}
2842
}
2943
}
3044
});
@@ -62,13 +76,13 @@ function jsonImpl(prototype: any, propertyName: any) {
6276
}
6377

6478
function checkJsonProperty(obj: any) {
65-
if (!obj && !("json" in obj)) {
79+
if (typeof obj !== "object" || !("json" in obj)) {
6680
throw new Error("Cannot load/save objects without json property");
6781
}
6882
}
6983

7084
function load(obj: any, data: any) {
71-
if (data) {
85+
if (data !== "undefined" && obj) {
7286
checkJsonProperty(obj);
7387
obj.json = data;
7488
}

test/customFormat.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as test from "tape";
2+
import { json } from "../index"
3+
4+
class C1 {
5+
6+
get json() {
7+
return { mandatory: true };
8+
}
9+
set json(data: any) {
10+
if (!data.mandatory) {
11+
throw new Error("missing!");
12+
}
13+
}
14+
}
15+
16+
class C2 {
17+
@json c1 = new C1();
18+
}
19+
20+
class C3 {
21+
22+
b = false;
23+
24+
get json() {
25+
return this.b;
26+
}
27+
set json(data: any) {
28+
this.b = data;
29+
}
30+
}
31+
32+
test(`customFormat`, t => {
33+
34+
const o1 = new C1();
35+
const j1 = json.save(o1);
36+
t.same(j1, { mandatory: true });
37+
json.load(o1, { mandatory: true});
38+
39+
const o2 = new C2();
40+
const j2 = json.save(o2);
41+
t.same(j2, { c1: { mandatory: true } });
42+
json.load(o2, { c1: { mandatory: true} });
43+
44+
const o3 = new C3();
45+
const j3 = json.save(o3);
46+
t.equal(j3, false);
47+
json.load(o3, true);
48+
t.equal(o3.b, true);
49+
json.load(o3, false);
50+
t.equal(o3.b, false);
51+
52+
t.end();
53+
});

test/deepTree.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as test from "tape";
2+
import { observable } from "mobx"
3+
import { json } from "../index"
4+
5+
class Node {
6+
@json @observable tag: number;
7+
@json @observable next?: Node;
8+
9+
constructor(tag: number, next?: Node) {
10+
this.tag = tag;
11+
this.next = next;
12+
}
13+
}
14+
15+
test(`deepTree`, t => {
16+
17+
let head: Node | undefined = undefined;
18+
for (let n = 0; n < 20; n++) {
19+
head = new Node(n, head);
20+
}
21+
22+
const j1 = json.save(head);
23+
24+
t.equal(JSON.stringify(j1), '{"tag":19,"next":{"tag":18,"next":{"tag":17,"next":{"tag":16,"next":{"tag":15,"next":{"tag":14,"next":{"tag":13,"next":{"tag":12,"next":{"tag":11,"next":{"tag":10,"next":{"tag":9,"next":{"tag":8,"next":{"tag":7,"next":{"tag":6,"next":{"tag":5,"next":{"tag":4,"next":{"tag":3,"next":{"tag":2,"next":{"tag":1,"next":{"tag":0}}}}}}}}}}}}}}}}}}}}');
25+
26+
json.load(head, JSON.parse('{"tag":4,"next":{"tag":3,"next":{"tag":2,"next":{"tag":1,"next":{"tag":0}}}}}'));
27+
28+
t.equal(head!.next!.next!.next!.next!.next!, undefined);
29+
t.equal(head!.next!.next!.next!.next!.tag, 0);
30+
t.equal(head!.next!.next!.next!.tag, 1);
31+
t.equal(head!.next!.next!.tag, 2);
32+
t.equal(head!.next!.tag, 3);
33+
t.equal(head!.tag, 4);
34+
35+
t.end();
36+
});

test/getterWithoutSetter.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as test from "tape";
2+
import { observable } from "mobx"
3+
import { json } from "../index"
4+
5+
class C1 {
6+
@json yourName = "Ted";
7+
8+
@json get message() {
9+
return `Hello, ${this.yourName}`;
10+
}
11+
}
12+
13+
test(`getterWithoutSetter - with plain value`, t => {
14+
15+
const o1 = new C1();
16+
17+
const j = json.save(o1);
18+
19+
t.equal(j.yourName, "Ted");
20+
t.equal(j.message, "Hello, Ted");
21+
22+
j.yourName = "Bill";
23+
24+
json.load(o1, j);
25+
26+
t.equal(o1.yourName, "Bill");
27+
t.equal(o1.message, "Hello, Bill");
28+
29+
t.end();
30+
});
31+
32+
class C2 {
33+
@json @observable yourName = "Ted";
34+
35+
@json get message() {
36+
return `Hello, ${this.yourName}`;
37+
}
38+
}
39+
40+
test(`getterWithoutSetter - with observable value`, t => {
41+
42+
const o1 = new C2();
43+
44+
const j = json.save(o1);
45+
46+
t.equal(j.yourName, "Ted");
47+
t.equal(j.message, "Hello, Ted");
48+
49+
j.yourName = "Bill";
50+
51+
json.load(o1, j);
52+
53+
t.equal(o1.yourName, "Bill");
54+
t.equal(o1.message, "Hello, Bill");
55+
56+
t.end();
57+
});
58+

test/stableCollection.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as test from "tape";
2+
//import { observable } from "mobx"
3+
import { json, Collection } from "../index"
4+
5+
class I {
6+
@json id = I.nextId++;
7+
@json message = ""
8+
9+
selected = false;
10+
11+
constructor(message?: string) {
12+
this.message = message || "";
13+
}
14+
15+
static nextId = 1;
16+
}
17+
18+
class C {
19+
@json list = new Collection<I>(() => new I());
20+
21+
messages() {
22+
return this.list.items.map(i => i.message).join(",");
23+
}
24+
}
25+
26+
test(`stableCollection`, t => {
27+
28+
const c = new C();
29+
30+
c.list.items.push(new I("a"));
31+
c.list.items.push(new I("b"));
32+
t.equal(c.messages(), "a,b");
33+
34+
c.list.items[1].selected = true;
35+
36+
json.load(c, {
37+
list: [
38+
{id: 1, message: "a"},
39+
{id: 3, message: "c"},
40+
{id: 2, message: "b"}
41+
]
42+
});
43+
44+
t.equal(c.messages(), "a,c,b");
45+
t.equal(c.list.items[2].selected, true);
46+
47+
t.end();
48+
});

0 commit comments

Comments
 (0)