├── .eslintrc.js ├── .gitignore ├── LICENSE ├── Model.js ├── ModelBase.js ├── README.md ├── package.json └── test ├── ExampleModelTest.js ├── ModelBaseTest.js ├── ModelTest.js └── models ├── AsyncStorageMock.js ├── AsyncStorageTestModel.js ├── ExampleModel.js └── TestModel.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "installedESLint": true, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": true 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 4 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "double" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ], 35 | "no-unused-vars": 0 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .babelrc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 nikches 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | software and associated documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 8 | to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 15 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 16 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 17 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 18 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Model.js: -------------------------------------------------------------------------------- 1 | import ModelBase from "./ModelBase"; 2 | let storageProvider = null; 3 | try { 4 | const ReactNative = require("react-native"); 5 | storageProvider = ReactNative.AsyncStorage; 6 | } catch (error) { 7 | storageProvider = require("./test/models/AsyncStorageMock").default; 8 | } 9 | 10 | export default class Model extends ModelBase { 11 | static get className() { 12 | return "Model"; 13 | } 14 | 15 | /** 16 | * Write self into Storage. 17 | * 18 | * @param {string} key Storage key. 19 | * @return {Promise} 20 | */ 21 | store(key) { 22 | if (typeof(key) !== "string") { 23 | key = this.constructor.className; 24 | } 25 | 26 | const lastChar = key.slice(-1); 27 | ["*", "/"].forEach((char) => { 28 | if (char === lastChar) { 29 | throw new Error(`Model key shouldn't ending at "${char}".`); 30 | } 31 | }); 32 | 33 | const itemsPath = Model.getItemsPath(key); 34 | return new Promise((resolve, reject) => { 35 | const data = this.serialize(); 36 | 37 | storageProvider.setItem(key, data).then(() => { 38 | if (itemsPath === null) { 39 | resolve(); 40 | } else { 41 | return storageProvider.getItem(itemsPath); 42 | } 43 | }).then((itemsJson) => { 44 | let items = []; 45 | 46 | if (itemsJson !== undefined && itemsJson !== null) { 47 | items = JSON.parse(itemsJson); 48 | } 49 | 50 | if (items.indexOf(key) === -1) { 51 | items.push(key); 52 | } 53 | 54 | return storageProvider.setItem(itemsPath, JSON.stringify(items)); 55 | }).then(() => { 56 | resolve(); 57 | }).catch((error) => { 58 | reject(error); 59 | }); 60 | }); 61 | } 62 | 63 | /** 64 | * Get path of items. 65 | * /example/items 66 | * @param {string} source path 67 | * @return {Promise} 68 | */ 69 | static getItemsPath(path) { 70 | if (path.charAt(0) !== "/") { 71 | return null; 72 | } 73 | 74 | const lastIndexOfSlash = path.lastIndexOf("/"); 75 | if (lastIndexOfSlash === -1) { 76 | return null; 77 | } 78 | 79 | return path.slice(0, lastIndexOfSlash) + "/_items"; 80 | } 81 | 82 | /** 83 | * Read object from Storage. 84 | * 85 | * @static 86 | * @param {string} key Storage key 87 | * @returns {Promise} 88 | */ 89 | static restore(key) { 90 | if (typeof(key) !== "string") { 91 | key = this.className; 92 | } 93 | 94 | const lastChar = key.slice(-1); 95 | const itemsPath = Model.getItemsPath(key); 96 | 97 | return new Promise((resolve, reject) => { 98 | if (itemsPath !== null && lastChar === "*") { 99 | storageProvider.getItem(itemsPath).then((itemKeysJson) => { 100 | if (itemKeysJson === undefined || itemKeysJson === null) { 101 | resolve([]); 102 | } 103 | 104 | const itemKeys = JSON.parse(itemKeysJson); 105 | const itemsPromises = []; 106 | itemKeys.forEach((item) => { 107 | itemsPromises.push(storageProvider.getItem(item)); 108 | }); 109 | 110 | return Promise.all(itemsPromises); 111 | }).then((itemsSerialized) => { 112 | const itemsDeserialized = []; 113 | itemsSerialized.forEach((item) => { 114 | itemsDeserialized.push(Model.deserialize(item)); 115 | }); 116 | 117 | resolve(itemsDeserialized); 118 | }).catch((error) => { 119 | reject(error); 120 | }); 121 | } else { 122 | storageProvider.getItem(key).then((data) => { 123 | if (data === null) { 124 | resolve(null); 125 | } else { 126 | const deserialized = Model.deserialize(data); 127 | resolve(deserialized); 128 | } 129 | }).catch((error) => { 130 | reject(error); 131 | }); 132 | } 133 | }); 134 | } 135 | 136 | /** 137 | * Remove element from storage and related value in _items array. 138 | * @param {string} key 139 | * @return {Promise} 140 | */ 141 | static remove(key) { 142 | if (typeof(key) !== "string") { 143 | key = this.className; 144 | } 145 | 146 | const itemsPath = Model.getItemsPath(key); 147 | return new Promise((resolve, reject) => { 148 | storageProvider.removeItem(key).then(() => { 149 | if (itemsPath === null) { 150 | resolve(); 151 | } else { 152 | return storageProvider.getItem(itemsPath); 153 | } 154 | }).then((itemsJson) => { 155 | 156 | if (itemsJson === undefined || itemsJson === null) { 157 | resolve(); 158 | } 159 | 160 | const items = JSON.parse(itemsJson); 161 | const itemIndex = items.indexOf(key); 162 | 163 | if (itemIndex !== -1) { 164 | items.splice(itemIndex, 1); 165 | } 166 | 167 | if (items.length === 0) { 168 | return storageProvider.removeItem(itemsPath); 169 | } else { 170 | return storageProvider.setItem(itemsPath, JSON.stringify(items)); 171 | } 172 | }).then(() => { 173 | resolve(); 174 | }); 175 | }); 176 | } 177 | 178 | static get _storageProvider() { 179 | return storageProvider; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /ModelBase.js: -------------------------------------------------------------------------------- 1 | export default class ModelBase { 2 | static get _classNameKey() { 3 | return "__RNM_CLASS_NAME__"; 4 | } 5 | 6 | /** 7 | * className used instead of the name because babel replaces him at run-time. 8 | */ 9 | static get className() { 10 | return "ModelBase"; 11 | } 12 | 13 | /** 14 | * Create private properties and getProperty/setProperty methods for them. 15 | * 16 | * @param {object} properties propertyName: propertyType 17 | * @returns {ModelBase} 18 | */ 19 | constructor(properties) { 20 | for (const propertyName in properties) { 21 | if (ModelBase._isOwnProperty(propertyName, properties) === false) { 22 | continue; 23 | } 24 | 25 | if (propertyName.charAt(0) === "_") { 26 | throw new Error("Properties beginning at underscore not supported."); 27 | } 28 | 29 | const propertyType = properties[propertyName]; 30 | if (ModelBase._checkType(propertyType, "String") === false) { 31 | throw new TypeError(`${propertyName} type should be string.`); 32 | } 33 | 34 | const privatePropertyName = "_" + propertyName; 35 | const propertyNameCapitalized = 36 | propertyName.charAt(0).toUpperCase() + 37 | propertyName.slice(1); 38 | 39 | this[privatePropertyName] = null; 40 | this["set" + propertyNameCapitalized] = (value) => { 41 | if (ModelBase._checkType(value, propertyType) === false) { 42 | throw new TypeError(`"${this.constructor.className}.${propertyName}" is of type "${ModelBase._getTypeName(value)}". Expected "${propertyType}".`); 43 | } 44 | 45 | this[privatePropertyName] = value; 46 | }; 47 | 48 | this["get" + propertyNameCapitalized] = () => { 49 | return this[privatePropertyName]; 50 | }; 51 | } 52 | } 53 | 54 | /** 55 | * Create state object, equivalent this model. 56 | * 57 | * @returns {object} state 58 | */ 59 | createState() { 60 | const state = Object.create(null); 61 | for (const propertyName in this) { 62 | if (ModelBase._isOwnProperty(propertyName, this) === false) { 63 | continue; 64 | } 65 | 66 | if (propertyName.charAt(0) !== "_") { 67 | continue; 68 | } 69 | 70 | const statePropery = propertyName.slice(1); 71 | state[statePropery] = this[propertyName]; 72 | } 73 | 74 | return state; 75 | } 76 | 77 | /** 78 | * Populate members of the model from the state. 79 | * 80 | * @param {object} state state 81 | */ 82 | populateFromState(state) { 83 | // Create the instance of ModelBase which has property's types in closure. 84 | const constructor = ModelBase._getConstructor(this.constructor.className); 85 | 86 | let typeCheckObject = null; 87 | try { 88 | typeCheckObject = new constructor(); 89 | } catch (error) { 90 | throw new Error(error.message + " May be you forget specify default value in constructor?"); 91 | } 92 | 93 | for (const propertyName in state) { 94 | if (ModelBase._isOwnProperty(propertyName, state) === false) { 95 | continue; 96 | } 97 | 98 | const privatePropertyName = "_" + propertyName; 99 | const propertyNameCapitalized = 100 | propertyName.charAt(0).toUpperCase() + 101 | propertyName.slice(1); 102 | 103 | if (!(privatePropertyName in this)) { 104 | throw new Error(`Property "${propertyName}" does not exists in "${this.constructor.className}".`); 105 | } 106 | 107 | // Should throw exception if type of property is invalid. 108 | const setter = "set" + propertyNameCapitalized; 109 | typeCheckObject[setter](state[propertyName]); 110 | 111 | this[privatePropertyName] = state[propertyName]; 112 | } 113 | } 114 | 115 | /** 116 | * Create a new instance of the model from given state. 117 | * 118 | * @static 119 | * @param {object} state 120 | * @param {object} properties 121 | * @return {ModelBase} 122 | */ 123 | static fromState(state, properties) { 124 | const constructor = ModelBase._getConstructor(this.className); 125 | 126 | let instance = null; 127 | try { 128 | instance = new constructor(properties); 129 | } catch (error) { 130 | throw new Error(error.message + " May be you forget specify default value in constructor?"); 131 | } 132 | 133 | instance.populateFromState(state); 134 | return instance; 135 | } 136 | 137 | static _isOwnProperty(propertyName, object) { 138 | return Object.prototype.hasOwnProperty.apply(object, [propertyName]); 139 | } 140 | 141 | /** 142 | * Check the type of value and return true if his matches with requiredType. 143 | * 144 | * @static 145 | * @param {any} value 146 | * @param {string} requiredType 147 | * @returns {boolean} 148 | */ 149 | static _checkType(value, requiredType) { 150 | return ModelBase._getTypeName(value) === requiredType; 151 | } 152 | 153 | static _getTypeName(value) { 154 | if (value === undefined) { 155 | return "Undefined"; 156 | } 157 | 158 | if (value === null) { 159 | return "Null"; 160 | } 161 | 162 | if (value.constructor === undefined || value.constructor.className === undefined) { 163 | return Object.prototype.toString.call(value).slice(8, -1); 164 | } 165 | 166 | return value.constructor.className; 167 | } 168 | 169 | /** 170 | * Serialize self. 171 | * 172 | * @returns {string} JSON string 173 | */ 174 | serialize() { 175 | return JSON.stringify(ModelBase._serialize(this)); 176 | } 177 | 178 | /** 179 | * Serialize recursively instance of ModelBase. 180 | * 181 | * @static 182 | * @param {ModelBase} object 183 | * @returns {object} 184 | */ 185 | static _serialize(object) { 186 | const container = Object.create(null); 187 | container[ModelBase._classNameKey] = object.constructor.className; 188 | 189 | const data = Object.create(null); 190 | for (const key in object) { 191 | if (ModelBase._isOwnProperty(key, object) === false) { 192 | continue; 193 | } 194 | 195 | if (key.charAt(0) !== "_") { 196 | continue; 197 | } 198 | 199 | const value = object[key]; 200 | 201 | if (ModelBase._isObjectOrArray(value, "serialization")) { 202 | data[key] = ModelBase._processObjectOrArray(value, "serialization"); 203 | } else { 204 | data[key] = ModelBase._processScalar(value, "serialization"); 205 | } 206 | } 207 | 208 | container["data"] = data; 209 | return container; 210 | } 211 | 212 | /** 213 | * Deserialize JSON string 214 | * 215 | * @static 216 | * @param {string} JSONString 217 | */ 218 | static deserialize(JSONString) { 219 | return ModelBase._deserialize(JSON.parse(JSONString)); 220 | } 221 | 222 | /** 223 | * Deserialize instance of ModelBase. 224 | * 225 | * @static 226 | * @param {object} container 227 | * @return {ModelBase} 228 | */ 229 | static _deserialize(container) { 230 | const className = container[ModelBase._classNameKey]; 231 | 232 | if (className === undefined) { 233 | throw new Error("Invalid object"); 234 | } 235 | 236 | const constructor = ModelBase._getConstructor(className); 237 | let instance = null; 238 | try { 239 | instance = new constructor(); 240 | } catch (error) { 241 | throw new Error(error.message + " May be you forget specify default value in constructor?"); 242 | } 243 | 244 | const data = container["data"]; 245 | for (const key in data) { 246 | if (ModelBase._isOwnProperty(key, data) === false) { 247 | continue; 248 | } 249 | 250 | const value = data[key]; 251 | 252 | if (ModelBase._isObjectOrArray(value, "deserialization")) { 253 | instance[key] = ModelBase._processObjectOrArray(value, "deserialization"); 254 | } else { 255 | instance[key] = ModelBase._processScalar(value, "deserialization"); 256 | } 257 | } 258 | 259 | return instance; 260 | } 261 | 262 | /** 263 | * Check if the value is a plain object or array. 264 | * 265 | * @static 266 | * @param {any} value 267 | * @returns {boolean} 268 | */ 269 | static _isObjectOrArray(value, action = "serialization") { 270 | if (value === undefined || value === null) { 271 | return false; 272 | } 273 | 274 | if (action === "serialization" && value instanceof ModelBase) { 275 | return false; 276 | } 277 | 278 | if (action === "deserialization" && value[ModelBase._classNameKey] !== undefined) { 279 | return false; 280 | } 281 | 282 | return ModelBase._checkType(value, "Object") || ModelBase._checkType(value, "Array"); 283 | } 284 | 285 | /** 286 | * Serialize/deserialize instance of ModelBase, Number, Boolean, String, Date. 287 | * 288 | * @static 289 | * @param {any} scalar 290 | * @param {string} action One of "serialization" or "deserialization". 291 | * @returns {any} 292 | */ 293 | static _processScalar(scalar, action = "serialization") { 294 | if (scalar === undefined || scalar === null) { 295 | return scalar; 296 | } 297 | 298 | if (action === "serialization") { 299 | if (scalar instanceof ModelBase) { 300 | return ModelBase._serialize(scalar); 301 | } 302 | 303 | if (scalar instanceof Date) { 304 | throw new Error("Serialization of Date objects not supported."); 305 | } 306 | 307 | if (scalar instanceof RegExp) { 308 | throw new Error("Serialization of RegExp objects not supported."); 309 | } 310 | } 311 | 312 | if (action === "deserialization") { 313 | if (scalar[ModelBase._classNameKey]) { 314 | return ModelBase._deserialize(scalar); 315 | } 316 | } 317 | 318 | return scalar; /* Number, Boolean, String */ 319 | } 320 | 321 | /** 322 | * Serialize/deserialization recursively plain object or array. 323 | * 324 | * @param {any} iterable object or array 325 | * @param {string} action One of "serialization" or "deserialization". 326 | * @returns {any} 327 | */ 328 | static _processObjectOrArray(iterable, action = "serialization") { 329 | let data = null; 330 | 331 | if (Array.isArray(iterable)) { 332 | data = []; 333 | for (let i = 0; i < iterable.length; i++) { 334 | const value = iterable[i]; 335 | if (ModelBase._isObjectOrArray(value, action)) { 336 | data.push(ModelBase._processObjectOrArray(value, action)); 337 | } else { 338 | data.push(ModelBase._processScalar(value, action)); 339 | } 340 | } 341 | } else { 342 | data = Object.create(null); 343 | 344 | for (const key in iterable) { 345 | if (ModelBase._isOwnProperty(key, iterable) === false) { 346 | continue; 347 | } 348 | 349 | const value = iterable[key]; 350 | 351 | if (ModelBase._isObjectOrArray(value, action)) { 352 | data[key] = ModelBase._processObjectOrArray(value, action); 353 | } else { 354 | data[key] = ModelBase._processScalar(value, action); 355 | } 356 | } 357 | } 358 | 359 | return data; 360 | } 361 | 362 | static require(classConstructor) { 363 | if (classConstructor === undefined) { 364 | throw new Error("constructor is undefined"); 365 | } 366 | 367 | if (classConstructor.className === undefined) { 368 | throw new Error("constructor must have className static property."); 369 | } 370 | 371 | // if (classConstructor.className in ModelBase.classConstructors) { 372 | // throw new Error(`${classConstructor.className} already in use.`); 373 | // } 374 | 375 | ModelBase.classConstructors[classConstructor.className] = classConstructor; 376 | } 377 | 378 | static _getConstructor(className) { 379 | if (!(className in ModelBase.classConstructors)) { 380 | throw new Error(`Unknow class. Use ${this.className}.require(${className}).`); 381 | } 382 | 383 | return ModelBase.classConstructors[className]; 384 | } 385 | } 386 | 387 | ModelBase.classConstructors = Object.create(null); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-models 2 | Implementation of the models for React Native. 3 | 4 | ## Features 5 | - serialization/deserialization and saving of models in AsyncStorage; 6 | - support of nested models; 7 | - setters/getters for model's properties; 8 | - verification of property types; 9 | - filling models from the state; 10 | - path like syntax for keys; 11 | - serialization/deserialization of Date and RegExp objects not supported yet. Instead of it should be used strings. 12 | 13 | ## Methods 14 | 15 | ### constructor 16 | 17 | ```javascript 18 | constructor(properties: object): Model 19 | ``` 20 | 21 | Create instance of Model. Properties is a plain object: 22 | 23 | ```javascript 24 | { 25 | number: "Number", 26 | string: "String", 27 | boolean: "Boolean", 28 | object: "Object", 29 | array: "Array", 30 | modelBase: "Model", 31 | } 32 | ``` 33 | 34 | ### store 35 | 36 | ```javascript 37 | store(key?:string): Promise 38 | ``` 39 | 40 | Save model in `Storage`. This method serialize model and all nested models. If key doesn't specified used `className` property. Key support path syntax. For example: 41 | 42 | ```javascript 43 | /book/0 44 | /book/1 45 | /book/2 46 | ``` 47 | 48 | ### restore 49 | 50 | ```javascript 51 | static restore(key?:string): Promise 52 | ``` 53 | 54 | Restore model from `Storage`. If key doesn't specified using `className` property. If store models with keys `/book/0` and `/book/1`, possible to restore them by `/book/*` key. 55 | 56 | ### remove 57 | 58 | ```javascript 59 | static remove(key?:string): Promise 60 | ``` 61 | 62 | Remove value from `Store` and related record in `_items` record. 63 | 64 | ### serialize 65 | 66 | ```javascript 67 | serialize(): string 68 | ``` 69 | 70 | Serialize object. 71 | 72 | ### deserialize 73 | 74 | ```javascript 75 | static deserialize(): Model 76 | ``` 77 | 78 | Deserialize object from string. 79 | 80 | ### populateFromState 81 | 82 | ```javascript 83 | populateFromState(state: object) 84 | ``` 85 | 86 | Fill model's properties from given state. 87 | 88 | ### fromState 89 | 90 | ```javascript 91 | static fromState(state: object): Model 92 | ``` 93 | 94 | Create new instance of `Model`. This method check type whenever set model's property. 95 | 96 | ### require 97 | 98 | ```javascript 99 | static require(constructor: Model) 100 | ``` 101 | 102 | Bind class name with its constructor. Need for deserialization. 103 | 104 | ## Examples 105 | 106 | ### Properties 107 | 108 | ```javascript 109 | import Model from "react-native-models"; 110 | 111 | export default class MyModel extends Model { 112 | // className used instead name because babel replaces him at run-time. 113 | static get className() { 114 | return "MyModel"; 115 | } 116 | 117 | constructor(a = 0, b = "foo", c = new Model()) { 118 | super({ 119 | a: "Number", 120 | b: "String", 121 | c: "Model" // Nested model 122 | }); 123 | 124 | // Now MyModel has two members 125 | // this._a === null 126 | // this._b === null 127 | 128 | this._a = a; // this._a === 0 129 | this._b = b; // this._b === "foo" 130 | this._c = c; // this._c === instanceOf Model 131 | 132 | // or with validation of type 133 | this.setA(a); 134 | this.setB(b); 135 | this.setC(c); 136 | } 137 | 138 | test() { 139 | this.setA(1); 140 | this.setB("bar"); 141 | 142 | const a = this.getA(); // a === 1 143 | const b = this.getB(); // b === "bar" 144 | 145 | try { 146 | this.setA("1"); 147 | } catch (error) { 148 | return "exception"; 149 | } 150 | 151 | return "no exception"; 152 | } 153 | } 154 | ``` 155 | 156 | ### Store/restore 157 | 158 | ```javascript 159 | const myModel = new MyModel(); 160 | myModel.setA(10); 161 | myModel.store().then(() => { 162 | // ok 163 | }).catch((error) => { 164 | // handle error 165 | }); 166 | 167 | MyModel.restore().then((myModel) => { 168 | // ok 169 | }).catch((error) => { 170 | // handle error 171 | }); 172 | ``` 173 | 174 | ### Store/restore (path like syntax) 175 | 176 | ```javascript 177 | const myModel = new MyModel(1, "My model"); 178 | const anotherModel = new MyModel(2, "Another model"); 179 | 180 | myModel.store("/myModel").then(() => { 181 | return anotherModel.store("/anotherModel"); 182 | }).then(() => { 183 | MyModel.require(MyModel); 184 | return MyModel.restore("/*"); 185 | }).then((models) => { 186 | const myModel = models[0]; 187 | const anotherModel = model[1]; 188 | 189 | // myModel.getA() === 1 190 | // myModel.getB() === "My model" 191 | 192 | // anotherModel.getA() === 2 193 | // anotherModel.getB() === "Another model" 194 | }); 195 | ``` 196 | 197 | ### Filling state 198 | 199 | ```javascript 200 | import React from "react"; 201 | import Model from "react-native-models"; 202 | import MyModel from "./MyModel"; 203 | 204 | export default class MyComponent extends React.Component { 205 | constructor(props) { 206 | super(props); 207 | // Use default values of model 208 | this.state = (new MyModel()).createState(); 209 | } 210 | 211 | componentWillMount() { 212 | // Required for instancing of models objects. 213 | MyModel.require(MyModel); 214 | 215 | MyModel.restore().then((myModel) => { 216 | if (myModel!== null) { 217 | this.setState(myModel.createState()); 218 | } 219 | }).catch((error) => { 220 | // error handling 221 | }); 222 | } 223 | } 224 | ``` 225 | 226 | ### Serialization/deserialization 227 | 228 | ```javascript 229 | const myModel = new MyModel(); 230 | const serialized = myModel.serialize(); 231 | const myModel2 = MyModel.deserialize(serialized); 232 | ``` 233 | 234 | ## Testing 235 | 236 | ``` 237 | echo '{ "presets": ["es2015"] }' > .babelrc 238 | npm test 239 | ``` 240 | 241 | ## License 242 | 243 | [MIT](https://opensource.org/licenses/MIT) 244 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-models", 3 | "version": "0.7.9", 4 | "description": "Implementation of the models for React Native. Allow serialize/deserialize classes and store them in AsyncStorage.", 5 | "main": "Model.js", 6 | "scripts": { 7 | "test": "mocha --require babel-register" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "models", 12 | "serialization", 13 | "deserialization", 14 | "AsyncStorage", 15 | "Storage" 16 | ], 17 | "author": "nikches", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-core": "^6.18.2", 21 | "babel-preset-es2015": "^6.18.0", 22 | "babel-register": "^6.18.0", 23 | "mocha": "^3.1.2" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/nikches/react-native-models" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/ExampleModelTest.js: -------------------------------------------------------------------------------- 1 | import Assert from "assert"; 2 | import ExampleModel from "./models/ExampleModel"; 3 | /* global describe */ 4 | /* global it */ 5 | /* global before */ 6 | 7 | describe("ExampleModel", () => { 8 | describe("constructor", () => { 9 | it("should create instance of ExampleModel", () => { 10 | const model = new ExampleModel(); 11 | Assert.equal(model.getA(), 0); 12 | Assert.equal(model.getB(), "foo"); 13 | }); 14 | }); 15 | 16 | describe("test", () => { 17 | it("should properly set values of model", () => { 18 | const model = new ExampleModel(); 19 | Assert.equal(model.test(), "exception"); 20 | Assert.equal(model.getA(), 1); 21 | Assert.equal(model.getB(), "bar"); 22 | }); 23 | }); 24 | }); -------------------------------------------------------------------------------- /test/ModelBaseTest.js: -------------------------------------------------------------------------------- 1 | import Assert from "assert"; 2 | import ModelBase from "../ModelBase"; 3 | import TestModel from "./models/TestModel"; 4 | /* global describe */ 5 | /* global it */ 6 | /* global before */ 7 | 8 | describe("ModelBase", () => { 9 | describe("constructor", () => { 10 | it("should create correct properties for model", () => { 11 | const testModel = new TestModel(42); 12 | Assert.equal(testModel._number, 42); 13 | }); 14 | 15 | it("should throw error if properties beginning by underscore", () => { 16 | Assert.throws(() => { 17 | const modelBase = new ModelBase({ 18 | _property: "Number" 19 | }); 20 | }); 21 | }); 22 | 23 | it("should throw error if property type is not a \"String\"", () => { 24 | Assert.throws(() => { 25 | const modelBase = new ModelBase({ 26 | property: 0, 27 | }); 28 | }); 29 | }); 30 | }); 31 | 32 | describe("_checkType", () => { 33 | it("should return true if passed correct type", () => { 34 | Assert.equal(ModelBase._checkType(1, "Number" ), true); 35 | Assert.equal(ModelBase._checkType(false, "Boolean" ), true); 36 | Assert.equal(ModelBase._checkType("string", "String" ), true); 37 | Assert.equal(ModelBase._checkType(() => {}, "Function" ), true); 38 | Assert.equal(ModelBase._checkType({}, "Object" ), true); 39 | Assert.equal(ModelBase._checkType([], "Array" ), true); 40 | Assert.equal(ModelBase._checkType(Object.create(null), "Object" ), true); 41 | Assert.equal(ModelBase._checkType(new Date(), "Date" ), true); 42 | Assert.equal(ModelBase._checkType(new RegExp(), "RegExp" ), true); 43 | Assert.equal(ModelBase._checkType(new ModelBase(), "ModelBase" ), true); 44 | Assert.equal(ModelBase._checkType(new TestModel(), "TestModel" ), true); 45 | Assert.equal(ModelBase._checkType(undefined, "Undefined" ), true); 46 | Assert.equal(ModelBase._checkType(null, "Null" ), true); 47 | Assert.equal(ModelBase._checkType(new Number(1), "Number" ), true); 48 | Assert.equal(ModelBase._checkType(new String("test"), "String" ), true); 49 | Assert.equal(ModelBase._checkType(new Boolean(false), "Boolean" ), true); 50 | Assert.equal(ModelBase._checkType(new Function("return 0"), "Function"), true); 51 | Assert.equal(ModelBase._checkType(undefined, undefined ), false); 52 | }); 53 | }); 54 | 55 | describe("setter", () => { 56 | it("should set property value", () => { 57 | const testModel = new TestModel(); 58 | 59 | Assert.doesNotThrow(() => { 60 | testModel.setNumber(0); 61 | testModel.setString("string"); 62 | testModel.setBoolean(false); 63 | testModel.setObject({}); 64 | testModel.setArray([]); 65 | testModel.setModelBase(new ModelBase()); 66 | }); 67 | }); 68 | 69 | it("should throw error if passed value of incorrect type", () => { 70 | const testModel = new TestModel(); 71 | Assert.throws(() => { 72 | testModel.setNumber("1"); 73 | }); 74 | }); 75 | }); 76 | 77 | describe("getter", () => { 78 | it("should return correct value", () => { 79 | const testModel = new TestModel(); 80 | Assert.equal(testModel.getNumber(), 0); 81 | 82 | const modelBase = new ModelBase({ 83 | number: "Number", 84 | }); 85 | 86 | Assert.equal(modelBase.getNumber(), null); 87 | }); 88 | }); 89 | 90 | describe("createState", () => { 91 | it("should create state according to model's properties", () => { 92 | const testModel = new TestModel(); 93 | const state = testModel.createState(); 94 | 95 | Assert.equal (state.unknow, undefined); 96 | Assert.notEqual(state.number, undefined); 97 | Assert.notEqual(state.string, undefined); 98 | Assert.notEqual(state.object, undefined); 99 | }); 100 | }); 101 | 102 | describe("populateFromState", () => { 103 | it("should correct read values of properties", () => { 104 | const testModel = new TestModel(); 105 | 106 | const state = { 107 | number: 0, 108 | string: "string", 109 | object: { 110 | a: 0, 111 | b: false, 112 | c: "", 113 | } 114 | }; 115 | 116 | testModel.populateFromState(state); 117 | 118 | Assert.equal(testModel.getNumber(), state.number); 119 | Assert.equal(testModel.getString(), state.string); 120 | 121 | const testModelObject = testModel.getObject(); 122 | for (const key in testModelObject) { 123 | Assert.equal(testModelObject[key], state.object[key]); 124 | } 125 | }); 126 | 127 | it("should throw exception if state has unknown property", () => { 128 | const testModel = new TestModel(); 129 | const state = { 130 | unknownProperty: 0 131 | }; 132 | 133 | Assert.throws(() => { 134 | testModel.populateFromState(state); 135 | }); 136 | }); 137 | 138 | it("should throw exception if state has invalid property's type", () => { 139 | const testModel = new TestModel(); 140 | const state = { 141 | number: "invalid value", 142 | }; 143 | 144 | Assert.throws(() => { 145 | testModel.populateFromState(state); 146 | }); 147 | }); 148 | }); 149 | 150 | describe("_isObjectOrArray", () => { 151 | it("should correct distinguish an array and object from string, number, boolean or other not plain object", () => { 152 | Assert.equal(ModelBase._isObjectOrArray({}), true); 153 | Assert.equal(ModelBase._isObjectOrArray([]), true); 154 | Assert.equal(ModelBase._isObjectOrArray(new Object()), true); 155 | Assert.equal(ModelBase._isObjectOrArray(new Array()), true); 156 | 157 | Assert.equal(ModelBase._isObjectOrArray(new ModelBase()), false); 158 | Assert.equal(ModelBase._isObjectOrArray(new Date()), false); 159 | Assert.equal(ModelBase._isObjectOrArray(new RegExp()), false); 160 | Assert.equal(ModelBase._isObjectOrArray(new Number()), false); 161 | Assert.equal(ModelBase._isObjectOrArray(new Boolean()), false); 162 | Assert.equal(ModelBase._isObjectOrArray(new String()), false); 163 | Assert.equal(ModelBase._isObjectOrArray(0), false); 164 | Assert.equal(ModelBase._isObjectOrArray("string"), false); 165 | Assert.equal(ModelBase._isObjectOrArray(false), false); 166 | }); 167 | }); 168 | 169 | describe("fromState", () => { 170 | it("should instance new Model from given state", () => { 171 | const state = { 172 | number: 1, 173 | string: "string", 174 | boolean: true, 175 | object: { a: 1 }, 176 | array: [ 1 ], 177 | modelBase: new ModelBase(), 178 | }; 179 | 180 | const testModel = TestModel.fromState(state); 181 | }); 182 | }); 183 | 184 | describe("_serialize", () => { 185 | const testModel = new TestModel(); 186 | 187 | testModel.setNumber(-3.14); 188 | testModel.setBoolean(true); 189 | testModel.setString("string"); 190 | testModel.setObject({ 191 | number: 0, 192 | string: "string", 193 | object: Object.create(null), 194 | array: [] 195 | }); 196 | testModel.setArray([ 197 | 0, "string", {}, [] 198 | ]); 199 | 200 | testModel.setModelBase(new ModelBase()); 201 | 202 | const serialized = TestModel._serialize(testModel); 203 | 204 | Assert.notEqual(serialized[TestModel._classNameKey], undefined); 205 | Assert.notEqual(serialized["data"], undefined); 206 | 207 | const data = serialized["data"]; 208 | Assert.equal(Math.abs(data["_number"] + 3.14) < Number.EPSILON, true); 209 | Assert.equal(data["_boolean"], true); 210 | Assert.equal(data["_string"], "string"); 211 | 212 | const dataObject = data["_object"]; 213 | Assert.equal(dataObject["number"], 0); 214 | Assert.equal(dataObject["string"], "string"); 215 | Assert.equal(ModelBase._checkType(dataObject["object"], "Object"), true); 216 | Assert.equal(Array.isArray(dataObject["array"]), true); 217 | 218 | const dataArray = data["_array"]; 219 | Assert.equal(dataArray[0], 0); 220 | Assert.equal(dataArray[1], "string"); 221 | Assert.equal(ModelBase._checkType(dataArray[2], "Object"), true); 222 | Assert.equal(ModelBase._checkType(dataArray[3], "Array"), true); 223 | 224 | const dataModel = data["_modelBase"]; 225 | Assert.notEqual(dataModel[ModelBase._classNameKey], undefined); 226 | Assert.notEqual(dataModel["data"], undefined); 227 | }); 228 | 229 | describe("_deserialize", () => { 230 | TestModel.require(TestModel); 231 | TestModel.require(ModelBase); 232 | let testModel = new TestModel(); 233 | 234 | let nestedModel = new ModelBase({ 235 | number: "Number", 236 | }); 237 | 238 | testModel.setNumber(42); 239 | nestedModel.setNumber(42); 240 | testModel.setModelBase(nestedModel); 241 | 242 | const serialized = testModel.serialize(); 243 | const deserialized = TestModel.deserialize(serialized); 244 | 245 | Assert.equal(deserialized.getNumber(), 42); 246 | nestedModel = deserialized.getModelBase(); 247 | Assert.equal(nestedModel._number, 42); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /test/ModelTest.js: -------------------------------------------------------------------------------- 1 | import Assert from "assert"; 2 | import AsyncStorageTestModel from "./models/AsyncStorageTestModel"; 3 | import AsyncStorageMock from "./models/AsyncStorageMock"; 4 | /* global describe */ 5 | /* global it */ 6 | 7 | describe("Model", () => { 8 | describe("constructor", () => { 9 | it("should create instance of Model with AsyncStorageMock provider", () => { 10 | const model = new AsyncStorageTestModel(); 11 | Assert.equal(AsyncStorageMock.name, "AsyncStorageMock"); 12 | }); 13 | }); 14 | 15 | describe("store", () => { 16 | it("should correct storing values and shouldn't create index", () => { 17 | const model = new AsyncStorageTestModel(); 18 | Assert.equal(model.getNumber(), 0); 19 | model.store().then(() => {}); 20 | 21 | Assert.equal("AsyncStorageTestModel" in AsyncStorageTestModel._storageProvider.items, true); 22 | AsyncStorageTestModel._storageProvider.clear(); 23 | Assert.equal("AsyncStorageTestModel" in AsyncStorageTestModel._storageProvider.items, false); 24 | }); 25 | 26 | it("should throwing an error if key ending at \"/\" or \"*\"", () => { 27 | const model = new AsyncStorageTestModel(); 28 | Assert.throws(() => { 29 | model.store("key/"); 30 | }); 31 | 32 | Assert.throws(() => { 33 | model.store("key/*"); 34 | }); 35 | 36 | AsyncStorageTestModel._storageProvider.clear(); 37 | }); 38 | 39 | it("should create index of value if key beginning at \"/\"", (done) => { 40 | const model = new AsyncStorageTestModel(); 41 | model.store("/a").then(() => { 42 | return model.store("/b"); 43 | }).then(() => { 44 | return model.store("/a/1"); 45 | }).then(() => { 46 | return model.store("/a/2"); 47 | }).then(() => { 48 | let items = AsyncStorageTestModel._storageProvider.items["/_items"]; 49 | ["/a", "/b"].forEach((item) => { 50 | if (items.indexOf(item) === -1) { 51 | done(new TypeError("Incorrect values in index.")); 52 | } 53 | }); 54 | 55 | items = AsyncStorageTestModel._storageProvider.items["/a/_items"]; 56 | ["/a/1", "/a/2"].forEach((item) => { 57 | if (items.indexOf(item) === -1) { 58 | done(new TypeError("Incorrect values in index.")); 59 | } 60 | }); 61 | 62 | AsyncStorageTestModel._storageProvider.clear(); 63 | done(); 64 | }).catch((error) => { 65 | done(error); 66 | }); 67 | }); 68 | }); 69 | 70 | describe("restore", () => { 71 | it("should correct restoring single value from storage", (done) => { 72 | AsyncStorageTestModel.require(AsyncStorageTestModel); 73 | 74 | const model = new AsyncStorageTestModel(); 75 | model.store("/a").then(() => { 76 | return model.store("/a/1"); 77 | }).then(() => { 78 | return model.store("/a/2"); 79 | }).then(() => { 80 | return AsyncStorageTestModel.restore("/a/2"); 81 | }).then((value) => { 82 | if ((value instanceof AsyncStorageTestModel) === false) { 83 | done(new Error(`Value should be instance of AsyncStorageTestModel but given ${value}`)); 84 | } 85 | 86 | if (value.getNumber() !== 0) { 87 | done(new Error("Value should have number property equal 0.")); 88 | } 89 | 90 | AsyncStorageTestModel._storageProvider.clear(); 91 | done(); 92 | }).catch((error) => { 93 | done(error); 94 | }); 95 | }); 96 | 97 | it("should correct restore multiple values from storage", (done) => { 98 | AsyncStorageTestModel.require(AsyncStorageTestModel); 99 | 100 | const modelA = new AsyncStorageTestModel(1, "A"); 101 | const modelB = new AsyncStorageTestModel(2, "B"); 102 | const modelC = new AsyncStorageTestModel(3, "C"); 103 | 104 | modelA.store("/a").then(() => { 105 | return modelB.store("/b"); 106 | }).then(() => { 107 | return modelC.store("/c"); 108 | }).then(() => { 109 | return modelC.store("/a/b/c"); 110 | }).then(() => { 111 | return AsyncStorageTestModel.restore("/*"); 112 | }).then((items) => { 113 | if (AsyncStorageTestModel._checkType(items, "Array") === false) { 114 | done(new TypeError("Array expected")); 115 | } 116 | 117 | const modelA = items[0]; 118 | const modelB = items[1]; 119 | const modelC = items[2]; 120 | 121 | if (modelA.getNumber() !== 1 || modelA.getString() !== "A") { 122 | done(new Error("Incorrect properties of modelA.")); 123 | } 124 | 125 | if (modelB.getNumber() !== 2 || modelB.getString() !== "B") { 126 | done(new Error("Incorrect properties of modelB.")); 127 | } 128 | 129 | if (modelC.getNumber() !== 3 || modelC.getString() !== "C") { 130 | done(new Error("Incorrect properties of modelC.")); 131 | } 132 | 133 | AsyncStorageTestModel._storageProvider.clear(); 134 | done(); 135 | }).catch((error) => { 136 | done(error); 137 | }); 138 | }); 139 | }); 140 | 141 | describe("remove", () => { 142 | it("should correct remove values from storage", (done) => { 143 | AsyncStorageTestModel.require(AsyncStorageTestModel); 144 | 145 | const model = new AsyncStorageTestModel(); 146 | model.store("/a").then(() => { 147 | const items = AsyncStorageTestModel._storageProvider.items["/_items"]; 148 | if (!("/a" in AsyncStorageTestModel._storageProvider.items)) { 149 | done(new Error("Model should create index value.")); 150 | } 151 | 152 | return AsyncStorageTestModel.remove("/a"); 153 | }).then(() => { 154 | const items = AsyncStorageTestModel._storageProvider.items["/_items"]; 155 | if (items !== undefined) { 156 | done(new Error("Model should removing index if current path doesn't contains values.")); 157 | } 158 | 159 | if ("/a" in AsyncStorageTestModel._storageProvider.items) { 160 | done(new Error("Model should removing keky from index.")); 161 | } 162 | 163 | done(); 164 | }).catch((error) => { 165 | done(error); 166 | }); 167 | }); 168 | }); 169 | }); -------------------------------------------------------------------------------- /test/models/AsyncStorageMock.js: -------------------------------------------------------------------------------- 1 | export default class AsyncStorageMock { 2 | static setItem(key, value) { 3 | if (typeof(value) !== "string") { 4 | throw new Error ("Second argument should be string."); 5 | } 6 | 7 | return new Promise((resolve, reject) => { 8 | AsyncStorageMock.items[key] = value; 9 | resolve(); 10 | }); 11 | } 12 | 13 | static getItem(key) { 14 | return new Promise((resolve, reject) => { 15 | if (key in AsyncStorageMock.items) { 16 | resolve(AsyncStorageMock.items[key]); 17 | } else { 18 | resolve(null); 19 | } 20 | }); 21 | } 22 | 23 | static removeItem(key) { 24 | return new Promise((resolve, reject) => { 25 | delete AsyncStorageMock.items[key]; 26 | resolve(); 27 | }); 28 | } 29 | 30 | static clear() { 31 | return new Promise((resolve, reject) => { 32 | AsyncStorageMock.items = Object.create(null); 33 | resolve(); 34 | }); 35 | } 36 | } 37 | 38 | AsyncStorageMock.items = Object.create(null); -------------------------------------------------------------------------------- /test/models/AsyncStorageTestModel.js: -------------------------------------------------------------------------------- 1 | import Model from "../../Model"; 2 | import ModelBase from "../../ModelBase"; 3 | import AsyncStorageMock from "./AsyncStorageMock"; 4 | 5 | export default class AsyncStorageTestModel extends Model { 6 | static get className() { 7 | return "AsyncStorageTestModel"; 8 | } 9 | 10 | constructor(number = 0, string = "", boolean = false, object = {}, array = []) { 11 | super({ 12 | number: "Number", 13 | string: "String", 14 | boolean: "Boolean", 15 | object: "Object", 16 | array: "Array", 17 | modelBase: "ModelBase", 18 | }, AsyncStorageMock); 19 | 20 | this.setNumber(number); 21 | this.setString(string); 22 | this.setBoolean(boolean); 23 | this.setObject(object); 24 | this.setArray(array); 25 | this.setModelBase(new ModelBase()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/models/ExampleModel.js: -------------------------------------------------------------------------------- 1 | import Model from "../../Model"; 2 | 3 | export default class MyModel extends Model { 4 | // className used instead name because babel replaces him at run-time. 5 | static get className() { 6 | return "MyModel"; 7 | } 8 | 9 | constructor(a = 0, b = "foo", c = new Model()) { 10 | super({ 11 | a: "Number", 12 | b: "String", 13 | c: "Model" // Nested model 14 | }); 15 | 16 | // Now MyModel has two members 17 | // this._a === null 18 | // this._b === null 19 | 20 | this._a = a; // this._a === 0 21 | this._b = b; // this._b === "foo" 22 | this._c = c; // this._c === instanceOf Model 23 | 24 | // or with validation of type 25 | this.setA(a); 26 | this.setB(b); 27 | this.setC(c); 28 | } 29 | 30 | test() { 31 | this.setA(1); 32 | this.setB("bar"); 33 | 34 | const a = this.getA(); // a === 1 35 | const b = this.getB(); // b === "bar" 36 | 37 | try { 38 | this.setA("1"); 39 | } catch (error) { 40 | return "exception"; 41 | } 42 | 43 | return "no exception"; 44 | } 45 | } -------------------------------------------------------------------------------- /test/models/TestModel.js: -------------------------------------------------------------------------------- 1 | import ModelBase from "../../ModelBase"; 2 | 3 | export default class TestModel extends ModelBase { 4 | static get className() { 5 | return "TestModel"; 6 | } 7 | 8 | constructor(number = 0, string = "", boolean = false, object = {}, array = []) { 9 | super({ 10 | number: "Number", 11 | string: "String", 12 | boolean: "Boolean", 13 | object: "Object", 14 | array: "Array", 15 | modelBase: "ModelBase", 16 | }); 17 | 18 | this.setNumber(number); 19 | this.setString(string); 20 | this.setBoolean(boolean); 21 | this.setObject(object); 22 | this.setArray(array); 23 | this.setModelBase(new ModelBase()); 24 | } 25 | } 26 | --------------------------------------------------------------------------------