-
Notifications
You must be signed in to change notification settings - Fork 6
/
ModelBase.js
387 lines (321 loc) · 11.4 KB
/
ModelBase.js
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
export default class ModelBase {
static get _classNameKey() {
return "__RNM_CLASS_NAME__";
}
/**
* className used instead of the name because babel replaces him at run-time.
*/
static get className() {
return "ModelBase";
}
/**
* Create private properties and getProperty/setProperty methods for them.
*
* @param {object} properties propertyName: propertyType
* @returns {ModelBase}
*/
constructor(properties) {
for (const propertyName in properties) {
if (ModelBase._isOwnProperty(propertyName, properties) === false) {
continue;
}
if (propertyName.charAt(0) === "_") {
throw new Error("Properties beginning at underscore not supported.");
}
const propertyType = properties[propertyName];
if (ModelBase._checkType(propertyType, "String") === false) {
throw new TypeError(`${propertyName} type should be string.`);
}
const privatePropertyName = "_" + propertyName;
const propertyNameCapitalized =
propertyName.charAt(0).toUpperCase() +
propertyName.slice(1);
this[privatePropertyName] = null;
this["set" + propertyNameCapitalized] = (value) => {
if (ModelBase._checkType(value, propertyType) === false) {
throw new TypeError(`"${this.constructor.className}.${propertyName}" is of type "${ModelBase._getTypeName(value)}". Expected "${propertyType}".`);
}
this[privatePropertyName] = value;
};
this["get" + propertyNameCapitalized] = () => {
return this[privatePropertyName];
};
}
}
/**
* Create state object, equivalent this model.
*
* @returns {object} state
*/
createState() {
const state = Object.create(null);
for (const propertyName in this) {
if (ModelBase._isOwnProperty(propertyName, this) === false) {
continue;
}
if (propertyName.charAt(0) !== "_") {
continue;
}
const statePropery = propertyName.slice(1);
state[statePropery] = this[propertyName];
}
return state;
}
/**
* Populate members of the model from the state.
*
* @param {object} state state
*/
populateFromState(state) {
// Create the instance of ModelBase which has property's types in closure.
const constructor = ModelBase._getConstructor(this.constructor.className);
let typeCheckObject = null;
try {
typeCheckObject = new constructor();
} catch (error) {
throw new Error(error.message + " May be you forget specify default value in constructor?");
}
for (const propertyName in state) {
if (ModelBase._isOwnProperty(propertyName, state) === false) {
continue;
}
const privatePropertyName = "_" + propertyName;
const propertyNameCapitalized =
propertyName.charAt(0).toUpperCase() +
propertyName.slice(1);
if (!(privatePropertyName in this)) {
throw new Error(`Property "${propertyName}" does not exists in "${this.constructor.className}".`);
}
// Should throw exception if type of property is invalid.
const setter = "set" + propertyNameCapitalized;
typeCheckObject[setter](state[propertyName]);
this[privatePropertyName] = state[propertyName];
}
}
/**
* Create a new instance of the model from given state.
*
* @static
* @param {object} state
* @param {object} properties
* @return {ModelBase}
*/
static fromState(state, properties) {
const constructor = ModelBase._getConstructor(this.className);
let instance = null;
try {
instance = new constructor(properties);
} catch (error) {
throw new Error(error.message + " May be you forget specify default value in constructor?");
}
instance.populateFromState(state);
return instance;
}
static _isOwnProperty(propertyName, object) {
return Object.prototype.hasOwnProperty.apply(object, [propertyName]);
}
/**
* Check the type of value and return true if his matches with requiredType.
*
* @static
* @param {any} value
* @param {string} requiredType
* @returns {boolean}
*/
static _checkType(value, requiredType) {
return ModelBase._getTypeName(value) === requiredType;
}
static _getTypeName(value) {
if (value === undefined) {
return "Undefined";
}
if (value === null) {
return "Null";
}
if (value.constructor === undefined || value.constructor.className === undefined) {
return Object.prototype.toString.call(value).slice(8, -1);
}
return value.constructor.className;
}
/**
* Serialize self.
*
* @returns {string} JSON string
*/
serialize() {
return JSON.stringify(ModelBase._serialize(this));
}
/**
* Serialize recursively instance of ModelBase.
*
* @static
* @param {ModelBase} object
* @returns {object}
*/
static _serialize(object) {
const container = Object.create(null);
container[ModelBase._classNameKey] = object.constructor.className;
const data = Object.create(null);
for (const key in object) {
if (ModelBase._isOwnProperty(key, object) === false) {
continue;
}
if (key.charAt(0) !== "_") {
continue;
}
const value = object[key];
if (ModelBase._isObjectOrArray(value, "serialization")) {
data[key] = ModelBase._processObjectOrArray(value, "serialization");
} else {
data[key] = ModelBase._processScalar(value, "serialization");
}
}
container["data"] = data;
return container;
}
/**
* Deserialize JSON string
*
* @static
* @param {string} JSONString
*/
static deserialize(JSONString) {
return ModelBase._deserialize(JSON.parse(JSONString));
}
/**
* Deserialize instance of ModelBase.
*
* @static
* @param {object} container
* @return {ModelBase}
*/
static _deserialize(container) {
const className = container[ModelBase._classNameKey];
if (className === undefined) {
throw new Error("Invalid object");
}
const constructor = ModelBase._getConstructor(className);
let instance = null;
try {
instance = new constructor();
} catch (error) {
throw new Error(error.message + " May be you forget specify default value in constructor?");
}
const data = container["data"];
for (const key in data) {
if (ModelBase._isOwnProperty(key, data) === false) {
continue;
}
const value = data[key];
if (ModelBase._isObjectOrArray(value, "deserialization")) {
instance[key] = ModelBase._processObjectOrArray(value, "deserialization");
} else {
instance[key] = ModelBase._processScalar(value, "deserialization");
}
}
return instance;
}
/**
* Check if the value is a plain object or array.
*
* @static
* @param {any} value
* @returns {boolean}
*/
static _isObjectOrArray(value, action = "serialization") {
if (value === undefined || value === null) {
return false;
}
if (action === "serialization" && value instanceof ModelBase) {
return false;
}
if (action === "deserialization" && value[ModelBase._classNameKey] !== undefined) {
return false;
}
return ModelBase._checkType(value, "Object") || ModelBase._checkType(value, "Array");
}
/**
* Serialize/deserialize instance of ModelBase, Number, Boolean, String, Date.
*
* @static
* @param {any} scalar
* @param {string} action One of "serialization" or "deserialization".
* @returns {any}
*/
static _processScalar(scalar, action = "serialization") {
if (scalar === undefined || scalar === null) {
return scalar;
}
if (action === "serialization") {
if (scalar instanceof ModelBase) {
return ModelBase._serialize(scalar);
}
if (scalar instanceof Date) {
throw new Error("Serialization of Date objects not supported.");
}
if (scalar instanceof RegExp) {
throw new Error("Serialization of RegExp objects not supported.");
}
}
if (action === "deserialization") {
if (scalar[ModelBase._classNameKey]) {
return ModelBase._deserialize(scalar);
}
}
return scalar; /* Number, Boolean, String */
}
/**
* Serialize/deserialization recursively plain object or array.
*
* @param {any} iterable object or array
* @param {string} action One of "serialization" or "deserialization".
* @returns {any}
*/
static _processObjectOrArray(iterable, action = "serialization") {
let data = null;
if (Array.isArray(iterable)) {
data = [];
for (let i = 0; i < iterable.length; i++) {
const value = iterable[i];
if (ModelBase._isObjectOrArray(value, action)) {
data.push(ModelBase._processObjectOrArray(value, action));
} else {
data.push(ModelBase._processScalar(value, action));
}
}
} else {
data = Object.create(null);
for (const key in iterable) {
if (ModelBase._isOwnProperty(key, iterable) === false) {
continue;
}
const value = iterable[key];
if (ModelBase._isObjectOrArray(value, action)) {
data[key] = ModelBase._processObjectOrArray(value, action);
} else {
data[key] = ModelBase._processScalar(value, action);
}
}
}
return data;
}
static require(classConstructor) {
if (classConstructor === undefined) {
throw new Error("constructor is undefined");
}
if (classConstructor.className === undefined) {
throw new Error("constructor must have className static property.");
}
// if (classConstructor.className in ModelBase.classConstructors) {
// throw new Error(`${classConstructor.className} already in use.`);
// }
ModelBase.classConstructors[classConstructor.className] = classConstructor;
}
static _getConstructor(className) {
if (!(className in ModelBase.classConstructors)) {
throw new Error(`Unknow class. Use ${this.className}.require(${className}).`);
}
return ModelBase.classConstructors[className];
}
}
ModelBase.classConstructors = Object.create(null);