-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path5-struct.js
More file actions
145 lines (125 loc) · 3.62 KB
/
Copy path5-struct.js
File metadata and controls
145 lines (125 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
'use strict';
const typeOf = (value) => {
if (Array.isArray(value)) return 'array';
if (value === null) return 'null';
return typeof value;
};
class Record {
static immutable(defaults) {
return Record.#build(defaults, false);
}
static mutable(defaults) {
return Record.#build(defaults, true);
}
static #build(defaults, isMutable) {
const fields = Object.keys(defaults);
const schema = Object.create(null);
for (let i = 0; i < fields.length; i++) {
const key = fields[i];
schema[key] = typeOf(defaults[key]);
}
Object.freeze(schema);
class Struct {
static fields = fields.slice();
static schema = schema;
static mutable = isMutable;
constructor(data = {}) {
Struct.#validate(data);
for (let i = 0; i < fields.length; i++) {
const key = fields[i];
this[key] = key in data ? data[key] : defaults[key];
}
if (isMutable) Object.seal(this);
else Object.freeze(this);
}
static create(data) {
return new Struct(data);
}
static #validate(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const expected = schema[key];
if (!expected) {
throw new TypeError(`Unknown field "${key}"`);
}
const actual = typeOf(data[key]);
if (actual !== expected) {
throw new TypeError(
`Invalid type for "${key}": expected ${expected}, got ${actual}`,
);
}
}
}
update(updates) {
if (!isMutable) {
throw new Error('Cannot update immutable Record, use fork or branch');
}
Struct.#validate(updates);
return Object.assign(this, updates);
}
fork(updates = {}) {
return new Struct({ ...this.toObject(), ...updates });
}
branch(updates = {}) {
Struct.#validate(updates);
const obj = Object.create(this);
const keys = Object.keys(updates);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
Reflect.defineProperty(obj, key, {
value: updates[key],
writable: isMutable,
configurable: false,
enumerable: true,
});
}
return isMutable ? Object.seal(obj) : Object.freeze(obj);
}
toObject() {
const obj = {};
for (let i = 0; i < fields.length; i++) {
const key = fields[i];
obj[key] = this[key];
}
return obj;
}
}
return Struct;
}
}
// Usage
const City = Record.immutable({ name: 'Unknown' });
const User = Record.mutable({
id: 0,
name: 'Anonymous',
city: new City(),
email: '',
roles: [],
});
const rome = new City({ name: 'Rome' });
const marcus = new User({
id: 1,
name: 'Marcus',
city: rome,
email: 'marcus@metarhia.com',
});
marcus.update({ name: 'Marcus Aurelius' });
const lucius = marcus.fork({ name: 'Lucius Verus' });
lucius.update({ email: 'lucius@metarhia.com' });
const commodus = marcus.branch({ name: 'Commodus' });
console.log({ marcus, lucius, commodus: commodus.toObject() });
console.log('branch shares city:', commodus.city === marcus.city);
console.log('fork copies city ref:', lucius.city === marcus.city);
console.log('branch is a User:', commodus instanceof User);
try {
const invalid = new User({ id: 'one' });
console.log(invalid);
} catch (error) {
console.log('Type error:', error.message);
}
try {
rome.update({ name: 'Roma' });
} catch (error) {
console.log('Immutable error:', error.message);
}