├── test ├── misc.js ├── methods.js ├── virtuals.js └── types │ ├── collections.js │ ├── utils.js │ ├── number.js │ ├── boolean.js │ ├── string.js │ ├── constant.js │ ├── error.js │ ├── date.js │ ├── regexp.js │ ├── defined-object.js │ ├── collection.js │ ├── union.js │ ├── object.js │ ├── model.js │ └── array.js ├── .eslintignore ├── src ├── types │ ├── nil.js │ ├── any.js │ ├── object-id.js │ ├── tuple.js │ ├── collections.js │ ├── date.js │ ├── constant.js │ ├── collection.js │ ├── error.js │ ├── regexp.js │ ├── reference.js │ ├── basic.js │ ├── model.js │ └── union.js ├── modifiers │ ├── bare.js │ ├── reducer.js │ ├── auto-resolve.js │ ├── wrap-generator.js │ ├── optional.js │ ├── coerce.js │ └── validate.js ├── parse │ ├── finalize-type.js │ ├── type.js │ ├── pack-verify.js │ ├── parse-type.js │ ├── hydrate.js │ ├── any-object.js │ ├── array.js │ └── parse-object.js ├── index.js ├── utils.js └── store.js ├── .gitignore ├── examples └── todos │ ├── .babelrc │ ├── index.html │ ├── index.js │ ├── components │ ├── Todo.js │ ├── App.js │ ├── TodoList.js │ ├── Link.js │ ├── Footer.js │ └── AddTodo.js │ ├── schema-store.js │ ├── webpack.config.js │ ├── server.js │ └── package.json ├── .travis.yml ├── webpack.config.js ├── LICENSE.md ├── .babelrc ├── .eslintrc ├── package.json └── README.md /test/misc.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/methods.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/virtuals.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/types/collections.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/node_modules/** 3 | **/server.js 4 | **/webpack.config*.js 5 | -------------------------------------------------------------------------------- /src/types/nil.js: -------------------------------------------------------------------------------- 1 | import union from './union'; 2 | const Nil = union(null, undefined); 3 | export default Nil; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | dist 5 | lib 6 | es 7 | coverage 8 | _book 9 | .idea 10 | -------------------------------------------------------------------------------- /src/modifiers/bare.js: -------------------------------------------------------------------------------- 1 | export default function bare(func) { 2 | func.noWrap = true; 3 | return func; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/modifiers/reducer.js: -------------------------------------------------------------------------------- 1 | export default function reducer(func) { 2 | func.reducer = true; 3 | return func; 4 | } 5 | -------------------------------------------------------------------------------- /src/modifiers/auto-resolve.js: -------------------------------------------------------------------------------- 1 | export default function autoResolve(func) { 2 | func.autoResolve = true; 3 | return func; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/any.js: -------------------------------------------------------------------------------- 1 | import union from './union'; 2 | const Any = union(Object, Array, null, undefined, Number, Boolean, String); 3 | export default Any; 4 | -------------------------------------------------------------------------------- /examples/todos/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/object-id.js: -------------------------------------------------------------------------------- 1 | import parseType from '../parse/parse-type'; 2 | 3 | export default function ObjectId(options) { 4 | let base = parseType(options, String); 5 | base.name = 'objectid'; 6 | return base; 7 | } 8 | ObjectId.isType = true; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | - "node" 7 | script: 8 | - npm run coveralls 9 | - npm run build 10 | branches: 11 | only: 12 | - master 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | -------------------------------------------------------------------------------- /examples/todos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux Schema Todos example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/parse/finalize-type.js: -------------------------------------------------------------------------------- 1 | import packVerify from './pack-verify'; 2 | 3 | export default function finalizeType(type) { 4 | let options = type.options; 5 | if (options.validate || options.freeze) { 6 | packVerify(type); 7 | } 8 | return type; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /examples/todos/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import store from './schema-store'; 5 | import App from './components/App'; 6 | 7 | render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /src/modifiers/wrap-generator.js: -------------------------------------------------------------------------------- 1 | import { isGeneratorFunction } from '../utils'; 2 | 3 | export default function wrapGenerator(func, wrapper) { 4 | if (!isGeneratorFunction(func)) { 5 | throw new TypeError(`wrapGenerator requires a generator function`); 6 | } 7 | func.wrapGenerator = wrapper || true; 8 | return func; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /examples/todos/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import connect from 'react-redux-schema'; 3 | 4 | let Todo = ({ todo }) => ( 5 |
  • todo.completed = !todo.completed} 7 | style={{ 8 | textDecoration: todo.completed ? 'line-through' : 'none' 9 | }} 10 | > 11 | {todo.text} 12 |
  • 13 | ); 14 | 15 | Todo = connect()(Todo); 16 | 17 | export default Todo; 18 | -------------------------------------------------------------------------------- /src/parse/type.js: -------------------------------------------------------------------------------- 1 | import { namedFunction } from '../utils.js'; 2 | import parseType from './parse-type'; 3 | 4 | export default function type(type) { 5 | if (!type) throw new TypeError('Type expected'); 6 | let name = type.name || (type.constructor && type.constructor.name) || 'Type' 7 | , result = namedFunction(name, function(options) { 8 | return parseType(options, type); 9 | }) 10 | ; 11 | result.isType = true; 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /examples/todos/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AddTodo from './AddTodo'; 3 | import TodoList from './TodoList'; 4 | import Footer from './Footer'; 5 | import connect from 'react-redux-schema'; 6 | 7 | let App = ({root}) => ( 8 |
    9 | 10 | 11 |
    12 |
    13 | ); 14 | 15 | App = connect()(App); 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /examples/todos/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Todo from './Todo'; 3 | import connect from 'react-redux-schema'; 4 | 5 | let TodoList = ({ todos, filter }) => ( 6 | 14 | ); 15 | 16 | TodoList = connect()(TodoList); 17 | 18 | export default TodoList; 19 | -------------------------------------------------------------------------------- /examples/todos/schema-store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reduxSchemaStore, { type, collection, validate, model } from 'redux-schema'; 3 | 4 | const schema = type({ 5 | todos: collection(model('todo', { 6 | text: String, 7 | completed: Boolean 8 | })), 9 | filter: validate(String, /all|active|completed/) 10 | }); 11 | 12 | export default reduxSchemaStore(schema, { debug: true }, createStore, undefined, window.devToolsExtension ? window.devToolsExtension() : f => f); 13 | -------------------------------------------------------------------------------- /examples/todos/components/Link.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Link = ({ active, children, onClick }) => { 4 | if (active) { 5 | return {children}; 6 | } 7 | 8 | return ( 9 | { 11 | e.preventDefault(); 12 | onClick(); 13 | }} 14 | > 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | Link.propTypes = { 21 | active: PropTypes.bool.isRequired, 22 | children: PropTypes.node.isRequired, 23 | onClick: PropTypes.func.isRequired 24 | }; 25 | 26 | export default Link; 27 | -------------------------------------------------------------------------------- /src/parse/pack-verify.js: -------------------------------------------------------------------------------- 1 | const freeze = Object.freeze ? Object.freeze.bind(Object) : () => {}; 2 | 3 | export default function packVerify(type) { 4 | type.origPack = type.pack; 5 | type.pack = function(value) { 6 | let options = type.options 7 | , packed 8 | ; 9 | if (options.validate) { 10 | let message = type.validateAssign(value); 11 | if (message) { 12 | throw new TypeError(message); 13 | } 14 | } 15 | packed = type.origPack(value); 16 | if (options.freeze) { 17 | freeze(packed); 18 | } 19 | return packed; 20 | }; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/todos/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from './Link'; 3 | import connect from 'react-redux-schema'; 4 | 5 | const link = (filter, type) => ( 6 | filter.filter = type.toLowerCase()}> 7 | {type} 8 | 9 | ); 10 | 11 | let Footer = ({ filter }) => ( 12 |

    13 | Show: 14 | {' '} 15 | {link(filter, 'All')} 16 | {', '} 17 | {link(filter, 'Active')} 18 | {', '} 19 | {link(filter, 'Completed')} 20 |

    21 | ); 22 | 23 | Footer = connect()(Footer); 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /src/modifiers/optional.js: -------------------------------------------------------------------------------- 1 | import parseType from '../parse/parse-type'; 2 | import union from '../types/union'; 3 | 4 | export default function optional(baseType) { 5 | function Optional(options) { 6 | const self = { options }; 7 | const base = parseType({ ...options, parent: options.self || self, self: null }, baseType); 8 | const out = union(undefined, baseType)(options); 9 | if (base.storageKinds.indexOf('undefined') !== -1) { 10 | return parseType(options, baseType); 11 | } 12 | 13 | out.name = `optional(${base.name})`; 14 | return Object.assign(self, out); 15 | } 16 | 17 | Optional.isType = true; 18 | return Optional; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /examples/todos/components/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import connect from 'react-redux-schema'; 3 | 4 | let AddTodo = ({ todos }) => { 5 | let input; 6 | return ( 7 |
    8 |
    { 9 | e.preventDefault(); 10 | if (!input.value.trim()) { 11 | return; 12 | } 13 | todos.create({ text: input.value }); 14 | input.value = ''; 15 | }}> 16 | { 17 | input = node; 18 | }} /> 19 | 22 |
    23 |
    24 | ); 25 | }; 26 | 27 | AddTodo = connect()(AddTodo); 28 | 29 | export default AddTodo; 30 | -------------------------------------------------------------------------------- /src/modifiers/coerce.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import parseType from '../parse/parse-type'; 3 | 4 | export default function coerce(baseType, customCoerce) { 5 | function Coerce(options) { 6 | const self = { options }; 7 | const type = { ...parseType({ ...options, self: options.self }, baseType) }; 8 | const origCoerce = type.coerceData; 9 | 10 | type.coerceData = function(value) { 11 | value = customCoerce(value); 12 | if (type.validateData(value)) { 13 | return origCoerce(value); 14 | } 15 | return value; 16 | }; 17 | return Object.assign(self, finalizeType(type)); 18 | } 19 | 20 | Coerce.isType = true; 21 | return Coerce; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/todos/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/types/tuple.js: -------------------------------------------------------------------------------- 1 | import parseObjectType from '../parse/parse-object'; 2 | import { isArray } from '../utils'; 3 | 4 | export default function tuple(...types) { 5 | if (!types.length) throw new TypeError('Tuple requires item types.'); 6 | 7 | if (types.length === 1) { 8 | if (!types[0] || !isArray(types[0]) || types[0].length<2) throw new TypeError('Tuple requires multiple item types.'); 9 | types = types[0]; 10 | } 11 | 12 | function Tuple(options) { 13 | let type = parseObjectType(options, types, true); 14 | 15 | type.name = `tuple(${Object.keys(type.properties).map(prop => type.properties[prop].kind).join(', ')})`; 16 | return type; 17 | } 18 | 19 | Tuple.isType = true; 20 | return Tuple; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/todos/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackDevMiddleware = require('webpack-dev-middleware'); 3 | var webpackHotMiddleware = require('webpack-hot-middleware'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = new (require('express'))(); 7 | var port = 3000; 8 | 9 | var compiler = webpack(config); 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 11 | app.use(webpackHotMiddleware(compiler)); 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }); 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/types/collections.js: -------------------------------------------------------------------------------- 1 | import parseType from '../parse/parse-type'; 2 | import collection from './collection'; 3 | 4 | export default function collections(models) { 5 | function Collections(options) { 6 | let thisType = { options, isCollections: true } 7 | , type 8 | ; 9 | 10 | type = { 11 | get models() { 12 | let result = {}; 13 | models.forEach(model => result[model.modelName] = this[model.collection].model); 14 | return result; 15 | } 16 | }; 17 | 18 | models.forEach(model => type[model.collection] = collection(model)(options)); 19 | 20 | Object.assign(thisType, parseType({ ...options, self: thisType }, type)); 21 | thisType.name = `collections(${models.map(model => model.modelName).join(', ')})`; 22 | thisType.collections = models.map(model => type[model.collection]); 23 | return thisType; 24 | } 25 | 26 | Collections.isType = true; 27 | return Collections; 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var env = process.env.NODE_ENV; 6 | var config = { 7 | module: { 8 | loaders: [ 9 | { test: /\.js$/, loaders: ['babel-loader'] } 10 | ] 11 | }, 12 | output: { 13 | library: 'ReduxSchema', 14 | libraryTarget: 'umd' 15 | }, 16 | plugins: [ 17 | new webpack.optimize.OccurenceOrderPlugin(), 18 | new webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': JSON.stringify(env) 20 | }) 21 | ] 22 | }; 23 | 24 | if (env === 'production') { 25 | config.plugins.push( 26 | new webpack.optimize.UglifyJsPlugin({ 27 | compressor: { 28 | pure_getters: false, 29 | unsafe: false, 30 | unsafe_comps: false, 31 | warnings: false, 32 | screw_ie8: false 33 | }, 34 | mangle: { 35 | screw_ie8: false 36 | }, 37 | output: { 38 | screw_ie8: false 39 | } 40 | }) 41 | ) 42 | } 43 | 44 | module.exports = config; 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present Han de Boer 4 | Portions copyright (c) 2015-present Dan Abramov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/types/utils.js: -------------------------------------------------------------------------------- 1 | import { expect, should } from 'chai'; 2 | 3 | should(); 4 | 5 | export const baseTypeProperties = { 6 | isType: true, 7 | name: String, 8 | kind: String, 9 | storageKinds: Array, 10 | options: Object, 11 | validateData: Function, 12 | coerceData: Function, 13 | validateAssign: Function, 14 | pack: Function, 15 | unpack: Function, 16 | defaultValue: Function 17 | }; 18 | 19 | export function checkProperties(objGetter, properties) { 20 | let locus = new Error(); 21 | Object.keys(properties).forEach(name => it('should have the correct property ' + name, () => { 22 | try { 23 | let obj = objGetter() 24 | , propValue = obj[name] 25 | , propExpectedType = properties[name] 26 | ; 27 | if (typeof propExpectedType === 'function') { 28 | expect(propValue).to.not.be.undefined; 29 | expect(propValue.constructor).to.not.be.undefined; 30 | propValue.constructor.should.equal(propExpectedType); 31 | } else { 32 | expect(propValue).to.deep.equal(propExpectedType); 33 | } 34 | } catch (err) { 35 | err.stack = locus.stack.replace(/^[^\n]*\n/, ''); 36 | throw err; 37 | } 38 | })); 39 | } 40 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-es2015-template-literals", { "loose": true }], 4 | "transform-es2015-literals", 5 | "transform-es2015-function-name", 6 | "transform-es2015-arrow-functions", 7 | "transform-es2015-block-scoped-functions", 8 | ["transform-es2015-classes", { "loose": true }], 9 | "transform-es2015-object-super", 10 | "transform-es2015-shorthand-properties", 11 | ["transform-es2015-computed-properties", { "loose": true }], 12 | ["transform-es2015-for-of", { "loose": true }], 13 | "transform-es2015-sticky-regex", 14 | "transform-es2015-unicode-regex", 15 | "check-es2015-constants", 16 | ["transform-es2015-spread", { "loose": true }], 17 | "transform-es2015-parameters", 18 | ["transform-es2015-destructuring", { "loose": true }], 19 | "transform-es2015-block-scoping", 20 | "transform-object-rest-spread", 21 | "transform-es3-member-expression-literals", 22 | "transform-es3-property-literals", 23 | "transform-object-assign" 24 | ], 25 | "env": { 26 | "commonjs": { 27 | "plugins": [ 28 | ["transform-es2015-modules-commonjs", { "loose": true }] 29 | ] 30 | }, 31 | "debug": { 32 | "plugins": [ 33 | ["transform-es2015-modules-commonjs", { "loose": true }] 34 | ], 35 | "sourceMaps": "both" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-schema-todos-example", 3 | "version": "0.0.0", 4 | "description": "Redux Schema Todos example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ddsol/redux-schema.git" 12 | }, 13 | "keywords": [ 14 | "redux", 15 | "redux schema", 16 | "react", 17 | "example" 18 | ], 19 | "author": "Han de Boer", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ddsol/redux-schema/issues" 23 | }, 24 | "homepage": "https://github.com/ddsol/redux-schema#readme", 25 | "dependencies": { 26 | "babel-polyfill": "^6.3.14", 27 | "react": "^0.14.7", 28 | "react-dom": "^0.14.7", 29 | "react-redux-schema": "^2.0.0", 30 | "redux": "^3.1.2", 31 | "redux-schema": "^4.3.0" 32 | }, 33 | "devDependencies": { 34 | "babel-core": "^6.3.15", 35 | "babel-loader": "^6.2.0", 36 | "babel-preset-es2015": "^6.3.13", 37 | "babel-preset-react": "^6.3.13", 38 | "babel-preset-react-hmre": "^1.1.1", 39 | "babel-register": "^6.3.13", 40 | "cross-env": "^1.0.7", 41 | "expect": "^1.8.0", 42 | "express": "^4.13.3", 43 | "jsdom": "^5.6.1", 44 | "mocha": "^2.2.5", 45 | "node-libs-browser": "^0.5.2", 46 | "react-addons-test-utils": "^0.14.7", 47 | "webpack": "^1.9.11", 48 | "webpack-dev-middleware": "^1.2.0", 49 | "webpack-hot-middleware": "^2.9.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/types/number.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Number', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(Number), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'number', 28 | kind: 'number', 29 | storageKinds: ['number'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be 0 by default ', () => { 35 | store.instance.should.equal(0); 36 | }); 37 | 38 | it('should allow correct state assignment', () => { 39 | store.state = (store.state); 40 | }); 41 | 42 | it('should disallow incorrect state assignment', () => { 43 | expect(() => store.state = true).to.throw(TypeError); 44 | }); 45 | 46 | it('should allow assignment of a number', () => { 47 | store.instance = 17; 48 | store.state.should.equal(17); 49 | }); 50 | 51 | it('should reject non-number assignment', () => { 52 | expect(()=>store.instance = '12').to.throw(TypeError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/types/boolean.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Boolean', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(Boolean), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'boolean', 28 | kind: 'boolean', 29 | storageKinds: ['boolean'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be false by default ', () => { 35 | store.instance.should.equal(false); 36 | }); 37 | 38 | it('should allow correct state assignment', () => { 39 | store.state = (store.state); 40 | }); 41 | 42 | it('should disallow incorrect state assignment', () => { 43 | expect(() => store.state = 0).to.throw(TypeError); 44 | }); 45 | 46 | it('should allow assignment of a boolean value', () => { 47 | store.instance = true; 48 | store.state.should.equal(true); 49 | }); 50 | 51 | it('should reject non-boolean assignment', () => { 52 | expect(()=>store.instance = 7).to.throw(TypeError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/types/string.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('String', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(String), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'string', 28 | kind: 'string', 29 | storageKinds: ['string'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be \'\' by default ', () => { 35 | store.instance.should.equal(''); 36 | }); 37 | 38 | it('should allow correct state assignment', () => { 39 | store.state = (store.state); 40 | }); 41 | 42 | it('should disallow incorrect state assignment', () => { 43 | expect(() => store.state = 0).to.throw(TypeError); 44 | }); 45 | 46 | it('should allow assignment of a string', () => { 47 | store.instance = 'Foo the Bar'; 48 | store.state.should.equal('Foo the Bar'); 49 | }); 50 | 51 | it('should reject non-string assignment', () => { 52 | expect(()=>store.instance = 12).to.throw(TypeError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/types/date.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr } from '../utils'; 3 | 4 | export default function date(options) { 5 | let name = pathToStr(options.typeMoniker) || 'date'; 6 | const thisType = finalizeType({ 7 | isType: true, 8 | name: name, 9 | kind: 'date', 10 | storageKinds: ['string'], 11 | options, 12 | validateData(value, instancePath) { 13 | instancePath = instancePath || options.typeMoniker; 14 | if (value !== '' && (typeof value !== 'string' || (new Date(value)).toJSON() !== value)) { 15 | return `Type of "${pathToStr(instancePath) || name}" data must be Date string`; 16 | } 17 | }, 18 | coerceData(value, instancePath) { 19 | if (!thisType.validateData(value, instancePath)) return value; 20 | return ''; 21 | }, 22 | validateAssign(value, instancePath) { 23 | instancePath = instancePath || options.typeMoniker; 24 | if (!(value instanceof Date)) { 25 | return `Type of "${pathToStr(instancePath) || name}" must be Date`; 26 | } 27 | }, 28 | pack(value) { 29 | return value.toJSON() || ''; 30 | }, 31 | unpack(store, path, instancePath, currentInstance) { 32 | if (currentInstance) throw new Error('Date types cannot modify a data instance'); 33 | return new Date(store.get(path) || 'Invalid Date'); 34 | }, 35 | getTypeFromPath(path) { 36 | if (path.length) throw new Error(`Cannot get type path for properties of Dates`); 37 | return options.typeMoniker; 38 | }, 39 | defaultValue() { 40 | return ''; 41 | } 42 | }); 43 | return thisType; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /test/types/constant.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type, constant } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Constant', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(constant('Test')), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'constant(Test)', 28 | kind: 'string', 29 | storageKinds: ['string'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should have the correct default value', () => { 35 | store.instance.should.equal('Test'); 36 | }); 37 | 38 | it('should allow correct state assignment', () => { 39 | store.state = (store.state); 40 | }); 41 | 42 | it('should disallow incorrect state assignment', () => { 43 | expect(() => store.state = 'wrong').to.throw(TypeError); 44 | }); 45 | 46 | it('should allow assignment of same constant value', () => { 47 | store.instance = 'Test'; 48 | store.state.should.equal('Test'); 49 | }); 50 | 51 | it('should reject different constant assignment', () => { 52 | expect(() => store.instance = 'wrong').to.throw(TypeError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/modifiers/validate.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import parseType from '../parse/parse-type'; 3 | import SimpExp from 'simpexp'; 4 | import { pathToStr } from '../utils'; 5 | 6 | export default function validate(baseType, validation) { 7 | function Validate(options) { 8 | let self = { options } 9 | , type = { ...parseType({ ...options, self: options.self }, baseType) } 10 | , origValidator = type.validateAssign 11 | , typeMoniker = options.typeMoniker 12 | , regExp 13 | , origDefault 14 | , defaultValue 15 | ; 16 | 17 | if (validation instanceof RegExp) { 18 | regExp = validation; 19 | defaultValue = new SimpExp(validation).gen(); 20 | origDefault = type.defaultValue; 21 | type.defaultValue = () => { 22 | let value = origDefault(); 23 | return regExp.test(value) ? value : defaultValue; 24 | }; 25 | validation = value => regExp.test(value) ? null : `Must match ${String(regExp)}`; 26 | } 27 | 28 | type.validateAssign = function(value, instancePath) { 29 | let instanceName = pathToStr(instancePath || typeMoniker) 30 | , message 31 | ; 32 | 33 | try { 34 | message = validation(value); 35 | } catch (err) { 36 | message = err.message; 37 | } 38 | if (message === true) { 39 | message = null; 40 | } 41 | if (message) { 42 | return `Can't assign "${instanceName}": ${message}`; 43 | } else { 44 | return origValidator(value, instancePath); 45 | } 46 | }; 47 | 48 | if (type.origPack) { 49 | type.pack = type.origPack; 50 | type.origPack = undefined; 51 | } 52 | 53 | return Object.assign(self, finalizeType(type)); 54 | } 55 | 56 | Validate.isType = true; 57 | return Validate; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/types/constant.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import parseType from '../parse/parse-type'; 3 | import { pathToStr } from '../utils'; 4 | 5 | export default function constant(constValue) { 6 | 7 | const type = { 8 | number: Number, 9 | string: String, 10 | ['boolean']: Boolean 11 | }[typeof constValue]; 12 | 13 | function Constant(options) { 14 | const moniker = [...options.typeMoniker]; 15 | 16 | if (!type || (constValue !== constValue)) throw new Error(`Currently only number, string and boolean constants are supported for constant "${pathToStr(moniker)}"`); 17 | 18 | const thisType = parseType({ 19 | ...options 20 | }, type); 21 | thisType.name = `constant(${constValue})`; 22 | 23 | thisType.validateData = (value, instancePath) => { 24 | instancePath = instancePath || options.typeMoniker; 25 | if (value !== constValue) { 26 | return `Value of "${pathToStr(instancePath)}" data must be ${value}`; 27 | } 28 | }; 29 | 30 | thisType.coerceData = () => constValue; 31 | 32 | const origValidateAssign = thisType.validateAssign; 33 | thisType.validateAssign = (value, instancePath) => { 34 | instancePath = instancePath || options.typeMoniker; 35 | const message = origValidateAssign(value, instancePath); 36 | if (message) return message; 37 | if (value !== constValue) { 38 | const display = typeof constValue === 'string' ? `"${constValue}"` : String(constValue); 39 | return `Value of "${pathToStr(instancePath)}" must be ${display}`; 40 | } 41 | } ; 42 | 43 | thisType.defaultValue = () => constValue; 44 | 45 | if (thisType.origPack) { 46 | thisType.pack = thisType.origPack; 47 | delete type.origPack; 48 | } 49 | 50 | return finalizeType(thisType); 51 | } 52 | 53 | Constant.isType = true; 54 | return Constant; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /test/types/error.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Error', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(Error), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'error', 28 | kind: 'error', 29 | storageKinds: ['object'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be an empty error by default ', () => { 35 | let v = store.instance; 36 | v.should.be.instanceOf(Error); 37 | String(v).should.match(/\bError\b/i); 38 | }); 39 | 40 | it('should allow correct state assignment', () => { 41 | store.state = (store.state); 42 | }); 43 | 44 | it('should disallow incorrect state assignment', () => { 45 | expect(() => store.state = 0).to.throw(TypeError); 46 | }); 47 | 48 | it('should allow assignment and retrieval of an Error object', () => { 49 | let errorIn = new Error('message here') 50 | , errorOut 51 | ; 52 | 53 | errorIn.custom = 'extra info'; 54 | store.instance = errorIn; 55 | errorOut = store.instance; 56 | errorIn.stack.should.equal(errorOut.stack); 57 | errorIn.custom.should.equal(errorOut.custom); 58 | }); 59 | 60 | it('should reject non-Error assignment', () => { 61 | expect(()=>store.instance = {}).to.throw(TypeError); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/types/date.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Date', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(Date), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'date', 28 | kind: 'date', 29 | storageKinds: ['string'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be invalid by default ', () => { 35 | let v = store.instance; 36 | v.should.be.instanceOf(Date); 37 | String(v).should.match(/\binvalid\b/i); 38 | }); 39 | 40 | it('should allow correct state assignment', () => { 41 | store.state = (store.state); 42 | store.instance = new Date(); 43 | store.state = (store.state); 44 | }); 45 | 46 | it('should disallow incorrect state assignment', () => { 47 | expect(() => store.state = 0).to.throw(TypeError); 48 | }); 49 | 50 | it('should allow assignment and retrieval of a date object', () => { 51 | let dateIn = new Date() 52 | , dateOut 53 | ; 54 | 55 | store.instance = dateIn; 56 | dateOut = store.instance; 57 | dateIn.getTime().should.equal(dateOut.getTime()); 58 | store.state.should.equal(dateIn.toISOString()); 59 | }); 60 | 61 | it('should reject non-Date assignment', () => { 62 | expect(()=>store.instance = (new Date()).toISOString()).to.throw(TypeError); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "browser": true, 6 | "builtin": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "arrowFunctions": true, 11 | "blockBindings": true, 12 | "classes": true, 13 | "defaultParams": true, 14 | "destructuring": true, 15 | "forOf": true, 16 | "generators": true, 17 | "modules": true, 18 | "objectLiteralComputedProperties": true, 19 | "objectLiteralDuplicateProperties": true, 20 | "objectLiteralShorthandMethods": true, 21 | "objectLiteralShorthandProperties": true, 22 | "octalLiterals": true, 23 | "restParams": true, 24 | "spread": true, 25 | "templateStrings": true, 26 | "unicodeCodePointEscapes": true, 27 | "experimentalObjectRestSpread": true, 28 | "jsx": true 29 | }, 30 | "sourceType": "module" 31 | }, 32 | "rules": { 33 | "block-scoped-var": 2, 34 | "camelcase": 2, 35 | "curly": [ 2, "multi-line" ], 36 | "dot-notation": [ 2, { "allowKeywords": true } ], 37 | "eqeqeq": [ 2, "allow-null" ], 38 | "strict": [ 2, "global" ], 39 | "guard-for-in": 0, 40 | "new-cap": [2, { "capIsNewExceptions": ["Model", "View", "TROGDOR"] }], 41 | "no-bitwise": 2, 42 | "no-caller": 2, 43 | "no-cond-assign": [ 2, "except-parens" ], 44 | "no-debugger": 2, 45 | "no-empty": 2, 46 | "no-eval": 2, 47 | "no-extend-native": 2, 48 | "no-irregular-whitespace": 2, 49 | "no-iterator": 2, 50 | "no-loop-func": 2, 51 | "no-multi-str": 2, 52 | "no-new": 2, 53 | "no-proto": 2, 54 | "no-script-url": 2, 55 | "no-sequences": 2, 56 | "no-shadow": 0, 57 | "no-undef": 2, 58 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 59 | "no-with": 2, 60 | "quotes": [ 2, "single", { "allowTemplateLiterals": true } ], 61 | "semi": [ 2, "always" ], 62 | "keyword-spacing": [ "error" ], 63 | "valid-typeof": 2, 64 | "wrap-iife": [ 2, "inside" ], 65 | "react/jsx-uses-vars": 1, 66 | "react/jsx-uses-react": 1, 67 | "react/jsx-no-undef": 2, 68 | }, 69 | "plugins": [ 70 | "react" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /test/types/regexp.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('RegExp', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type(RegExp), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'regexp', 28 | kind: 'regexp', 29 | storageKinds: ['object'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should be empty regexp by default ', () => { 35 | let v = store.instance; 36 | v.should.be.instanceOf(RegExp); 37 | String(v).should.match(/^[^a-z]*$/i); 38 | }); 39 | 40 | it('should allow correct state assignment', () => { 41 | store.state = (store.state); 42 | }); 43 | 44 | it('should disallow incorrect state assignment', () => { 45 | expect(() => store.state = 0).to.throw(TypeError); 46 | }); 47 | 48 | it('should allow assignment and retrieval of a RegExp object', () => { 49 | let str = 'testa12 testb34foo' 50 | , regExpIn = /test[abc](12|34)(:?foo)/ig 51 | , regExpOut 52 | ; 53 | 54 | regExpIn.exec(str); 55 | 56 | regExpIn.custom = 'extra info'; 57 | store.instance = regExpIn; 58 | regExpOut = store.instance; 59 | regExpIn.source.should.equal(regExpOut.source); 60 | regExpIn.global.should.equal(regExpOut.global); 61 | regExpIn.ignoreCase.should.equal(regExpOut.ignoreCase); 62 | regExpIn.multiline.should.equal(regExpOut.multiline); 63 | regExpIn.lastIndex.should.equal(regExpOut.lastIndex); 64 | }); 65 | 66 | it('should reject non-RegExp assignment', () => { 67 | expect(()=>store.instance = {}).to.throw(TypeError); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/parse/parse-type.js: -------------------------------------------------------------------------------- 1 | import parseObjectType from './parse-object'; 2 | import anyObject from './any-object'; 3 | 4 | import regExp from '../types/regexp'; 5 | import date from '../types/date'; 6 | import error from '../types/error'; 7 | import union from '../types/union'; 8 | import { functionIsType, basicTypes } from '../types/basic'; 9 | 10 | import { isArray } from '../utils'; 11 | 12 | import validate from '../modifiers/validate'; 13 | import optional from '../modifiers/optional'; 14 | 15 | export default function parseType(options, type) { 16 | if (typeof type === 'function' && type.isType && !type.storageKinds) return type(options); 17 | if (type && type.isType) return type; 18 | 19 | if (type === Object) return anyObject(options, false); 20 | if (type === Array) return anyObject(options, true); 21 | 22 | if (type === null) return basicTypes.null(options); 23 | if (type === undefined) return basicTypes.undefined(options); 24 | 25 | if (type === Number) return basicTypes.number(options); 26 | if (type === Boolean) return basicTypes['boolean'](options); //eslint-disable-line dot-notation 27 | if (type === String) return basicTypes.string(options); 28 | 29 | if (type === RegExp) return regExp(options); 30 | if (type === Date) return date(options); 31 | if (type === Error) return error(options); 32 | 33 | if (typeof type === 'object') { 34 | if (isArray(type)) { 35 | if (!type.length) { 36 | return anyObject(options, true); 37 | } else if (type.length > 1) { 38 | return union.apply(null, type)(options); 39 | } else { 40 | return parseObjectType(options, type, true); 41 | } 42 | } else { 43 | 44 | if (functionIsType(type.type)) { 45 | try { 46 | let actual = parseType(options, type.type); 47 | if (type.validate) { 48 | actual = validate(actual, type.validate)(options); 49 | } 50 | if (type.optional) { 51 | actual = optional(actual)(options); 52 | } 53 | return actual; 54 | } catch (err) { 55 | //empty 56 | } 57 | } 58 | 59 | return parseObjectType(options, type); 60 | } 61 | } 62 | 63 | throw new TypeError(`Unknown type ${type}`); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //Export types 2 | export { default as union } from './types/union'; 3 | export { default as tuple } from './types/tuple'; 4 | export { default as Any } from './types/any'; 5 | export { default as Nil } from './types/nil'; 6 | export { default as reference } from './types/reference'; 7 | export { default as ObjectId } from './types/object-id'; 8 | export { default as collection } from './types/collection'; 9 | export { default as collections } from './types/collections'; 10 | export { default as model, makeOwner } from './types/model'; 11 | export { default as constant } from './types/constant'; 12 | 13 | //Export modifiers 14 | export { default as optional } from './modifiers/optional'; 15 | export { default as validate } from './modifiers/validate'; 16 | export { default as bare } from './modifiers/bare'; 17 | export { default as reducer } from './modifiers/reducer'; 18 | export { default as wrapGenerator } from './modifiers/wrap-generator'; 19 | export { default as autoResolve } from './modifiers/auto-resolve'; 20 | export { default as coerce } from './modifiers/coerce'; 21 | 22 | //Export generic type parser 23 | export { default as type } from './parse/type'; 24 | 25 | //Helper 26 | export { owner, scratch } from './utils'; 27 | 28 | export function coerceState(state, type) { 29 | return type({ typeMoniker: [] }).coerceData(state); 30 | } 31 | 32 | import Store from './store'; 33 | 34 | export default function reduxSchemaStore(schema, options, createStore, preloadedState, enhancer) { 35 | let result = createStore => (reducer, preloadedState, enhancer) => { 36 | let store = new Store({ schema: schema, ...options }) 37 | , redux 38 | ; 39 | 40 | if (preloadedState !== undefined) { 41 | let message = store.schema.validateData(preloadedState); 42 | if (message) throw new TypeError(`Can't use preloaded state: ${message}`); 43 | } 44 | 45 | if (reducer) { 46 | let customReducer = reducer; 47 | reducer = (state, action) => customReducer(store.reducer(state, action), action); 48 | } else { 49 | reducer = store.reducer; 50 | } 51 | 52 | redux = createStore(reducer, preloadedState, enhancer); 53 | 54 | store.getState = redux.getState; 55 | store.replaceReducer = redux.replaceReducer; 56 | store.subscribe = redux.subscribe; 57 | 58 | store.store = redux; 59 | 60 | return store; 61 | }; 62 | 63 | return createStore ? result(createStore)(null, preloadedState, enhancer): result; 64 | } 65 | -------------------------------------------------------------------------------- /src/types/collection.js: -------------------------------------------------------------------------------- 1 | import { namedFunction, dedent } from '../utils'; 2 | import parseType from '../parse/parse-type'; 3 | import { makeOwner } from './model'; 4 | import bare from '../modifiers/bare'; 5 | 6 | export default function collection(model, extraProperties) { 7 | if (!model.isModel) { 8 | throw new TypeError('Collection items must be Models'); 9 | } 10 | function Collection(options) { 11 | let self = { options, isCollection: true } 12 | , typeMoniker = options.typeMoniker 13 | , parentMoniker = typeMoniker.slice(0, -1) 14 | , modelType = model({ 15 | ...options, 16 | typeMoniker: parentMoniker.concat(model.modelName), 17 | parent: options.self || self, 18 | self: null 19 | }) 20 | , type = { 21 | '*': modelType, 22 | create: bare(namedFunction(`create${modelType.name}`, function() { 23 | return this.model.apply(null, arguments); 24 | }), dedent` 25 | function(){ 26 | return new ${modelType.name}'(...arguments); 27 | }`, !options.namedFunctions), 28 | get all() { 29 | return this.keys.map(id => this.get(id)); 30 | }, 31 | get model() { 32 | let self = this 33 | , BoundModel = namedFunction(modelType.name, function Model(...args) { 34 | return modelType.call(this, makeOwner(self), ...args); 35 | }, modelType, !options.debug); 36 | BoundModel.prototype = modelType.prototype; 37 | Object.assign(BoundModel, modelType); 38 | return BoundModel; 39 | }, 40 | remove(id) { 41 | if (id && id._meta && id._meta.idKey && id[id._meta.idKey]) { 42 | id = id[id._meta.idKey]; 43 | } 44 | var val = this.get(id); 45 | if (!val) throw new Error(`Could not remove ${modelType.name}[${id}]: object not found`); 46 | this.set(id, undefined); 47 | } 48 | } 49 | , thisType 50 | ; 51 | 52 | if (extraProperties) { 53 | type = {...extraProperties, ...type}; 54 | } 55 | 56 | thisType = parseType({ ...options, self }, type); 57 | 58 | thisType.name = `collection(${modelType.name})`; 59 | thisType.collection = model.collection; 60 | thisType.model = modelType; 61 | return Object.assign(self, thisType); 62 | } 63 | 64 | Collection.isType = true; 65 | return Collection; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/types/error.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr, isPlainObject } from '../utils'; 3 | import serializeError from 'serialize-error'; 4 | 5 | export default function error(options) { 6 | let name = pathToStr(options.typeMoniker) || 'error'; 7 | 8 | const thisType = finalizeType({ 9 | isType: true, 10 | name: name, 11 | kind: 'error', 12 | storageKinds: ['object'], 13 | options, 14 | validateData(value, instancePath) { 15 | instancePath = instancePath || options.typeMoniker; 16 | if (!isPlainObject(value) || (typeof value.name !== 'string') || (typeof value.message !== 'string')) { 17 | return `Type of "${pathToStr(instancePath) || name}" data must be and Error object`; 18 | } 19 | }, 20 | coerceData(value, instancePath) { 21 | if (!thisType.validateData(value, instancePath)) return value; 22 | return thisType.defaultValue(); 23 | }, 24 | validateAssign(value, instancePath) { 25 | instancePath = instancePath || options.typeMoniker; 26 | if (!(value instanceof Error)) { 27 | return `Type of "${pathToStr(instancePath) || name}" must be Error`; 28 | } 29 | }, 30 | pack(value) { 31 | let serializable = JSON.parse(JSON.stringify(serializeError(value))); 32 | if (serializable.stack) { 33 | serializable.stack = serializable.stack.split('\n'); 34 | } 35 | return serializable; 36 | }, 37 | unpack(store, path, instancePath, currentInstance) { 38 | if (currentInstance) throw new Error('Error types cannot modify a data instance'); 39 | let value = { ...store.get(path) } 40 | , type = { 41 | EvalError, 42 | RangeError, 43 | ReferenceError, 44 | SyntaxError, 45 | TypeError, 46 | URIError 47 | }[value.name] || Error 48 | ; 49 | 50 | if (value.stack && value.stack.join) { 51 | value.stack = value.stack.join('\n'); 52 | } 53 | if (type.prototype.name === value.name) { 54 | delete value.name; 55 | } 56 | 57 | return Object.assign(Object.create(type.prototype), { 58 | ...value, 59 | toString() { 60 | return this.message ? `${this.name}: ${this.message}` : this.name; 61 | }, 62 | inspect() { 63 | return `[${this.toString()}]`; 64 | } 65 | }); 66 | }, 67 | getTypeFromPath(path) { 68 | if (path.length) throw new Error(`Cannot get type path for properties of Errors`); 69 | return options.typeMoniker; 70 | }, 71 | defaultValue() { 72 | return { 73 | name: 'Error', 74 | message: '' 75 | }; 76 | } 77 | }); 78 | 79 | return thisType; 80 | } 81 | -------------------------------------------------------------------------------- /src/types/regexp.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr } from '../utils'; 3 | 4 | export default function regExp(options) { 5 | let name = pathToStr(options.typeMoniker) || 'regexp'; 6 | 7 | const thisType = finalizeType({ 8 | isType: true, 9 | name: name, 10 | kind: 'regexp', 11 | storageKinds: ['object'], 12 | options, 13 | validateData(value, instancePath) { 14 | let ok = true 15 | , props = 2 16 | ; 17 | 18 | if (!value || typeof value !== 'object') { 19 | ok = false; 20 | } else { 21 | instancePath = instancePath || options.typeMoniker; 22 | if ('lastIndex' in value) { 23 | props++; 24 | } 25 | if ( 26 | !value 27 | || typeof value !== 'object' 28 | || Object.keys(value).length !== props 29 | || typeof value.pattern !== 'string' 30 | || typeof value.flags !== 'string' 31 | ) { 32 | ok = false; 33 | } else { 34 | try { 35 | new RegExp(value.pattern, value.flags); //eslint-disable-line no-new 36 | if (props === 3) { 37 | if (typeof value.lastIndex !== 'number') { 38 | ok = false; 39 | } 40 | } 41 | } catch (err) { 42 | ok = false; 43 | } 44 | } 45 | } 46 | if (!ok) { 47 | return `Type of "${pathToStr(instancePath) || name}" data must be RegExp data object`; 48 | } 49 | }, 50 | coerceData(value, instancePath) { 51 | if (!thisType.validateData(value, instancePath)) return value; 52 | return thisType.defaultValue(); 53 | }, 54 | validateAssign(value, instancePath) { 55 | instancePath = instancePath || options.typeMoniker; 56 | if (!(value instanceof RegExp)) { 57 | return `Type of "${pathToStr(instancePath) || name}" must be RegExp`; 58 | } 59 | }, 60 | pack(value) { 61 | let result = { 62 | pattern: value.source, 63 | flags: String(value).match(/[gimuy]*$/)[0] 64 | }; 65 | if (value.lastIndex) { 66 | result.lastIndex = value.lastIndex; 67 | } 68 | return result; 69 | }, 70 | unpack(store, path, instancePath, currentInstance) { 71 | if (currentInstance) throw new Error('RegExp types cannot modify a data instance'); 72 | let stored = store.get(path) 73 | , regExp = new RegExp(stored.pattern, stored.flags) 74 | ; 75 | if (stored.lastIndex) { 76 | regExp.lastIndex = stored.lastIndex; 77 | } 78 | return regExp; 79 | }, 80 | getTypeFromPath(path) { 81 | if (path.length) throw new Error(`Cannot get type path for properties of RegExps`); 82 | return options.typeMoniker; 83 | }, 84 | defaultValue() { 85 | return { 86 | pattern: '', 87 | flags: '' 88 | }; 89 | } 90 | }); 91 | return thisType; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/types/reference.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr } from '../utils'; 3 | 4 | export default function reference(target) { 5 | function Reference(options) { 6 | const thisType = finalizeType({ 7 | isType: true, 8 | name: pathToStr(options.typeMoniker), 9 | kind: 'reference', 10 | storageKinds: ['string'], 11 | options, 12 | validateData(value, instancePath) { 13 | instancePath = instancePath || options.typeMoniker; 14 | if (typeof value !== 'string') { 15 | return `Reference data for "${pathToStr(instancePath)}" must be a string`; 16 | } 17 | if (!value) return 'Reference cannot be empty'; 18 | }, 19 | coerceData(value, instancePath) { 20 | if (!thisType.validateData(value, instancePath)) return value; 21 | return thisType.defaultValue(); 22 | }, 23 | validateAssign(value, instancePath) { 24 | instancePath = instancePath || options.typeMoniker; 25 | if (!value || !value._meta || !value._meta.idKey) { 26 | return `Reference for "${pathToStr(instancePath)}" must be an object of type "${target}"`; 27 | } 28 | }, 29 | pack(value) { 30 | if (!value || !value._meta) { 31 | throw new Error(`Reference for "${pathToStr(options.typeMoniker)}" must be an object of type "${target}"`); 32 | } 33 | return value[value._meta.idKey]; 34 | }, 35 | unpack(store, storePath, instancePath, currentInstance, owner) { 36 | const findCollection = () => { 37 | let ancestor = owner 38 | , found 39 | , type 40 | , collection 41 | ; 42 | 43 | while (true) { 44 | if (!ancestor || !ancestor._meta) break; 45 | type = ancestor._meta.type; 46 | if (!type) break; 47 | if (type.isCollections && type.properties[target] && type.properties[target].isCollection) { 48 | collection = ancestor[target]; 49 | if (collection && collection._meta && (type = collection._meta.type) && type.model) { 50 | found = collection; 51 | break; 52 | } 53 | } 54 | ancestor = ancestor._meta.owner; 55 | } 56 | return found; 57 | }; 58 | 59 | let id = store.get(storePath); 60 | if (!id || id === '') throw new ReferenceError(`Cannot dereference: No "${target}" id present`); 61 | 62 | let collection = findCollection(); 63 | 64 | if (!collection) throw new TypeError(`Cannot find collection of type "${target}"`); 65 | 66 | let result = collection.get(id); 67 | if (!result) throw new ReferenceError(`Cannot dereference: No "${target}" with id "${id}" exists`); 68 | return result; 69 | }, 70 | getTypeFromPath(path) { 71 | if (path.length) throw new Error('Type paths for references are not supported'); 72 | return options.typeMoniker; 73 | }, 74 | defaultValue() { 75 | return ''; 76 | } 77 | }); 78 | return thisType; 79 | } 80 | 81 | Reference.isType = true; 82 | return Reference; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/types/basic.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr } from '../utils'; 3 | 4 | export function functionIsType(func) { 5 | return func && ( 6 | func === String 7 | || func === Number 8 | || func === Boolean 9 | || func === Error 10 | || func === RegExp 11 | || func === Date 12 | || func === Array 13 | || func === Object 14 | || func.isType === true 15 | ); 16 | } 17 | 18 | const basicTypeHandlers = { 19 | String: { 20 | name: 'string', 21 | is: v => typeof v === 'string', 22 | defaultValue: '', 23 | coerce: v => { 24 | if (v === undefined || v === null) { 25 | return ''; 26 | } 27 | return String(v); 28 | } 29 | }, 30 | Number: { 31 | name: 'number', 32 | is: v => typeof v === 'number', 33 | defaultValue: 0, 34 | coerce: v => { 35 | const n = Number(v); 36 | return isNaN(n) ? 0 : n; 37 | } 38 | }, 39 | Boolean: { 40 | name: 'boolean', 41 | is: v => typeof v === 'boolean', 42 | defaultValue: false, 43 | coerce: v => Boolean(v) 44 | }, 45 | Null: { 46 | name: 'null', 47 | is: v => v === null, 48 | defaultValue: null, 49 | coerce: () => null 50 | }, 51 | Undefined: { 52 | name: 'undefined', 53 | is: v => typeof v === 'undefined', 54 | defaultValue: undefined, 55 | coerce: () => undefined 56 | } 57 | }; 58 | 59 | function basicType(options, type) { 60 | const upName = type.name[0].toUpperCase() + type.name.substr(1); 61 | 62 | const thisType = finalizeType({ 63 | isType: true, 64 | name: pathToStr(options.typeMoniker) || type.name, 65 | kind: type.name, 66 | storageKinds: [type.name], 67 | options, 68 | validateData(value, instancePath) { 69 | instancePath = instancePath || options.typeMoniker; 70 | if (!type.is(value)) { 71 | return `Type of "${pathToStr(instancePath)}" data must be ${type.name}`; 72 | } 73 | }, 74 | coerceData(value) { 75 | if (!thisType.validateData(value)) return value; 76 | return type.coerce(value); 77 | }, 78 | validateAssign(value, instancePath) { 79 | instancePath = instancePath || options.typeMoniker; 80 | if (!type.is(value)) { 81 | return `Type of "${pathToStr(instancePath)}" must be ${type.name}`; 82 | } 83 | }, 84 | pack(value) { 85 | return value; 86 | }, 87 | unpack(store, path, instancePath, currentInstance) { 88 | if (currentInstance) throw new Error(`${upName} types cannot modify a data instance`); 89 | return store.get(path); 90 | }, 91 | getTypeFromPath(path) { 92 | if (path.length) throw new Error(`Cannot get type path for properties of ${type.name}s`); 93 | return options.typeMoniker; 94 | }, 95 | defaultValue() { 96 | return type.defaultValue; 97 | } 98 | }); 99 | return thisType; 100 | } 101 | 102 | let _basicTypes = {}; 103 | 104 | Object.keys(basicTypeHandlers).forEach(name => _basicTypes[name.toLowerCase()] = options => basicType(options, basicTypeHandlers[name])); 105 | 106 | export const basicTypes = _basicTypes; 107 | 108 | -------------------------------------------------------------------------------- /test/types/defined-object.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Object (defined)', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type({ v: Number }), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'object', 28 | kind: 'object', 29 | storageKinds: ['object'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | it('should allow empty object assignment', () => { 35 | store.instance = { v: 1 }; 36 | store.state.should.deep.equal({ v: 1 }); 37 | }); 38 | 39 | it('should allow correct state assignment', () => { 40 | store.state = (store.state); 41 | }); 42 | 43 | it('should disallow incorrect state assignment', () => { 44 | expect(() => store.state = 0).to.throw(TypeError); 45 | }); 46 | 47 | it('should allow single property object assignment', () => { 48 | store.instance = { v: 1 }; 49 | store.state.should.deep.equal({ v: 1 }); 50 | }); 51 | }); 52 | 53 | context('properties', () => { 54 | it('should allow setting a defined property directly', () => { 55 | store.instance.v = 5; 56 | store.instance.v.should.equal(5); 57 | }); 58 | 59 | context('#get', () => { 60 | it('should not mutate the object', () => { 61 | let pre = store.state; 62 | store.instance.get('v'); 63 | actions.should.have.length(0); 64 | store.state.should.equal(pre); 65 | }); 66 | 67 | it('should get properties from the object', () => { 68 | store.state = { v: 7 }; 69 | store.instance.get('v').should.equal(7); 70 | }); 71 | 72 | it('should throw for properties that don\'t exist', () => { 73 | store.state = { v: 1 }; 74 | expect(()=>store.instance.get('d')).to.throw; 75 | expect(()=>store.instance.get(0)).to.throw; 76 | expect(()=>store.instance.get()).to.throw; 77 | expect(()=>store.instance.get('missing')).throw; 78 | }); 79 | }); 80 | 81 | context('#set', () => { 82 | it('should mutate the object', () => { 83 | let pre = store.state; 84 | store.instance.set('v', 3); 85 | actions.should.have.length(1); 86 | store.state.should.not.equal(pre); 87 | }); 88 | 89 | it('should set properties in the object', () => { 90 | store.state = { v: 7 }; 91 | store.instance.set('v', 17); 92 | store.state.should.deep.equal({ v: 17 }); 93 | }); 94 | 95 | it('should fail to extend the object when setting an undefined property', () => { 96 | store.state = { v: 5 }; 97 | expect(() => store.instance.set('x', 8)).to.throw(TypeError); 98 | }); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/types/collection.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { model, collection } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('collection', () => { 9 | 10 | let schema 11 | , store 12 | , instance 13 | ; 14 | 15 | beforeEach(() => { 16 | let type = model('Model', { p:Number }); 17 | store = schemaStore(collection(type), { debug: true }, createStore); 18 | schema = store.schema; 19 | instance = store.instance; 20 | }); 21 | 22 | context('type', () => { 23 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 24 | name: 'collection(Model)', 25 | kind: 'object', 26 | storageKinds: ['object'] 27 | })); 28 | }); 29 | 30 | context('instance', () => { 31 | it('should accept anything that is a model instance', () => { 32 | instance.set('someid', { id: 'someid', p:1 }); 33 | }); 34 | 35 | it('#model should return the child model', () => { 36 | var model = instance.model; 37 | model.should.be.an.instanceof(Function); 38 | model.name.should.equal('Model'); 39 | model.kind.should.equal('object'); 40 | }); 41 | 42 | it('should reject anything that isn\'t a model instance', () => { 43 | expect(() => { 44 | instance.set('someid', {}); 45 | }).to.throw(); 46 | }); 47 | 48 | it('.all should return all children', () => { 49 | new instance.model(); //eslint-disable-line no-new, new-cap 50 | new instance.model(); //eslint-disable-line no-new, new-cap 51 | let all = instance.all; 52 | all.should.be.an.instanceof(Array); 53 | all.should.have.length(2); 54 | all[0].p.should.equal(0); 55 | }); 56 | 57 | it('#create should create model instances', () => { 58 | instance.create(); 59 | instance.create(); 60 | instance.create(); 61 | let all = instance.all; 62 | all.should.be.an.instanceof(Array); 63 | all.should.have.length(3); 64 | all[0].p.should.equal(0); 65 | }); 66 | 67 | context('#remove', () => { 68 | 69 | it('should delete model instances by id', () => { 70 | let all 71 | , second 72 | ; 73 | new instance.model(); //eslint-disable-line no-new, new-cap 74 | second = new instance.model(); //eslint-disable-line no-new, new-cap 75 | new instance.model(); //eslint-disable-line no-new, new-cap 76 | 77 | instance.remove(second.id); 78 | all = instance.all; 79 | 80 | all.should.be.an.instanceof(Array); 81 | all.should.have.length(2); 82 | }); 83 | 84 | it('should delete model instances by reference', () => { 85 | let all 86 | , second 87 | ; 88 | new instance.model(); //eslint-disable-line no-new, new-cap 89 | second = new instance.model(); //eslint-disable-line no-new, new-cap 90 | new instance.model(); //eslint-disable-line no-new, new-cap 91 | 92 | instance.remove(second); 93 | all = instance.all; 94 | 95 | all.should.be.an.instanceof(Array); 96 | all.should.have.length(2); 97 | }); 98 | 99 | it('should throw if object not found', () => { 100 | expect(() => { 101 | new instance.model(); //eslint-disable-line no-new, new-cap 102 | new instance.model(); //eslint-disable-line no-new, new-cap 103 | 104 | instance.remove('notpresent'); 105 | }).to.throw(); 106 | }); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/types/model.js: -------------------------------------------------------------------------------- 1 | import { namedFunction, guid } from '../utils'; 2 | import parseType from '../parse/parse-type'; 3 | import ObjectId from './object-id'; 4 | 5 | const isOwner = {}; 6 | 7 | export default function model(name, model) { 8 | let collection = name[0].toLowerCase() + name.substr(1); 9 | 10 | function Model(options = { typeMoniker: [] }) { 11 | if (typeof model !== 'object') throw new TypeError('model definitions must be objects'); 12 | 13 | let rebuild = false 14 | , ResultModel 15 | , resultType 16 | , idKey 17 | ; 18 | 19 | ResultModel = namedFunction(name, function(...args) { 20 | 21 | if (!(this instanceof ResultModel)) { 22 | return new ResultModel(...args); 23 | } 24 | 25 | let owner = args[0] 26 | , store = options.store 27 | , id = guid(24) 28 | , storePath 29 | , instancePath 30 | ; 31 | if (owner && (owner.isOwner === isOwner) && owner.owner && owner.owner._meta && owner.owner._meta.store) { 32 | args.shift(); 33 | if (owner.id) { 34 | id = owner.id; 35 | } 36 | owner = owner.owner; 37 | } else { 38 | owner = null; 39 | } 40 | if (!store || !store.isStore) { 41 | throw new Error(`Cannot create new ${name}. No store assigned`); 42 | } 43 | if (!owner) { 44 | throw new Error(`Cannot create new ${name}. New instances only allowed for models in a collection.`); 45 | } 46 | 47 | storePath = owner._meta.storePath.concat(id); 48 | instancePath = owner._meta.instancePath.concat(id); 49 | store.unpack(resultType, storePath, instancePath, this, owner); 50 | this.constructor.apply(this, args); 51 | }, model.constructor, !options.namedFunctions); 52 | 53 | resultType = parseType({ ...options, self: ResultModel }, model); 54 | 55 | Object.keys(resultType.properties || {}).forEach((name) => { 56 | if (resultType.properties[name].name === 'objectid') { 57 | idKey = name; 58 | } 59 | }); 60 | 61 | if (!resultType.methods.hasOwnProperty('constructor')) { 62 | model.constructor = function(values) { 63 | if (values) { 64 | Object.keys(values).forEach(prop => this.set(prop, values[prop])); 65 | } 66 | }; 67 | rebuild = true; 68 | } 69 | 70 | let origConstructor = model.constructor; 71 | 72 | model.constructor = namedFunction(name, function() { 73 | let storeInstance 74 | , path = this._meta.storePath 75 | ; 76 | 77 | storeInstance = resultType.defaultValue(); 78 | storeInstance[idKey] = path[path.length - 1]; 79 | this._meta.store.put(path, storeInstance); 80 | 81 | if (origConstructor) { 82 | origConstructor.apply(this, arguments); 83 | } 84 | }, origConstructor, !options.namedFunctions); 85 | 86 | if (!idKey) { 87 | if (resultType.properties.id) { 88 | throw new TypeError(`The "id" property of "${name}" must be an ObjectId`); 89 | } 90 | model.id = ObjectId; 91 | idKey = 'id'; 92 | rebuild = true; 93 | } 94 | if (rebuild) { 95 | resultType = parseType({ ...options, self: ResultModel }, model); 96 | } 97 | 98 | const origCoerceData = resultType.coerceData; 99 | resultType.coerceData = function(value, instancePath) { 100 | const coerced = origCoerceData(value, instancePath); 101 | if (!instancePath || !instancePath.length) return coerced; 102 | coerced.id = instancePath[instancePath.length - 1]; 103 | return coerced; 104 | }; 105 | 106 | Object.assign(ResultModel, resultType, { 107 | prototype: resultType.prototype, 108 | collection, 109 | idKey, 110 | model, 111 | isModel: true 112 | }); 113 | 114 | return ResultModel; 115 | } 116 | 117 | Model.collection = collection; 118 | Model.modelName = name; 119 | Model.isType = true; 120 | Model.isModel = true; 121 | return Model; 122 | } 123 | 124 | export const makeOwner = (owner, id) => ({ 125 | isOwner, 126 | owner, 127 | id 128 | }); 129 | -------------------------------------------------------------------------------- /test/types/union.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Union', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | context('type', () => { 15 | beforeEach(() => { 16 | store = schemaStore(type([Number, String]), { debug: true }, createStore); 17 | schema = store.schema; 18 | actions = []; 19 | let origDispatch = store.dispatch; 20 | store.dispatch = function(action) { 21 | actions.push(action); 22 | return origDispatch(action); 23 | }; 24 | }); 25 | 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'union(number, string)', 28 | kind: 'union', 29 | storageKinds: ['number', 'string'] 30 | })); 31 | }); 32 | 33 | context('instance', () => { 34 | context('non-clashing types', () => { 35 | beforeEach(() => { 36 | store = schemaStore(type([Number, String]), { debug: true }, createStore); 37 | schema = store.schema; 38 | actions = []; 39 | let origDispatch = store.dispatch; 40 | store.dispatch = function(action) { 41 | actions.push(action); 42 | return origDispatch(action); 43 | }; 44 | }); 45 | 46 | it('should be the first type\'s default by default ', () => { 47 | store.instance.should.equal(0); 48 | }); 49 | 50 | it('should allow correct state assignment', () => { 51 | store.state = ''; 52 | }); 53 | 54 | it('should disallow incorrect state assignment', () => { 55 | expect(() => store.state = true).to.throw(TypeError); 56 | }); 57 | 58 | it('should allow assignment of each constituent type', () => { 59 | store.instance = 8675309; 60 | store.state.should.equal(8675309); 61 | store.instance = 'Foo the Bar'; 62 | store.state.should.equal('Foo the Bar'); 63 | }); 64 | 65 | it('should reject incorrect type assignment', () => { 66 | expect(()=>store.instance = true).to.throw(TypeError); 67 | }); 68 | }); 69 | 70 | context('clashing types', () => { 71 | beforeEach(() => { 72 | store = schemaStore(type([{ prop: Number }, { prop: String} ]), { debug: true }, createStore); 73 | schema = store.schema; 74 | actions = []; 75 | let origDispatch = store.dispatch; 76 | store.dispatch = function(action) { 77 | actions.push(action); 78 | return origDispatch(action); 79 | }; 80 | }); 81 | 82 | it('should be the first type\'s default by default ', () => { 83 | store.instance.toObject().should.deep.equal({ prop: 0 }); 84 | }); 85 | 86 | it('should allow correct state assignment', () => { 87 | store.state = { 88 | object2: { 89 | prop: 'Foo' 90 | } 91 | }; 92 | }); 93 | 94 | it('should disallow incorrect state assignment', () => { 95 | expect(() => store.state = 0).to.throw(TypeError); 96 | expect(() => store.state = { prop:0 }).to.throw(TypeError); 97 | expect(() => store.state = { 98 | object1: { 99 | prop: 'Foo' 100 | } 101 | }).to.throw(TypeError); 102 | expect(() => store.state = { 103 | object2: { 104 | prop: 42 105 | } 106 | }).to.throw(TypeError); 107 | }); 108 | 109 | it('should allow assignment of each constituent type', () => { 110 | store.instance = { prop: 8675309 }; 111 | store.state.should.deep.equal({ 112 | object1: { 113 | prop: 8675309 114 | } 115 | }); 116 | store.instance = { prop: 'Foo the Bar' }; 117 | store.state.should.deep.equal({ 118 | object2: { 119 | prop: 'Foo the Bar' 120 | } 121 | }); 122 | }); 123 | 124 | it('should reject incorrect type assignment', () => { 125 | expect(()=>store.instance = true).to.throw(TypeError); 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/types/object.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Object (plain)', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type({}), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'object', 28 | kind: 'object', 29 | storageKinds: ['object'] 30 | })); 31 | 32 | it('should treat {} and Object as equivalent', () => { 33 | let type1 = schemaStore(type({}), {}, createStore).schema 34 | , type2 = schemaStore(type(Object), {}, createStore).schema 35 | , prop1 36 | , prop2 37 | ; 38 | 39 | for (let prop in baseTypeProperties) { 40 | if (prop === 'options') return; 41 | prop1 = type1[prop]; 42 | prop2 = type2[prop]; 43 | if (typeof prop1 === 'function') prop1 = String(prop1); 44 | if (typeof prop2 === 'function') prop2 = String(prop2); 45 | expect(prop1).to.deep.equal(prop2); 46 | } 47 | }); 48 | }); 49 | 50 | context('instance', () => { 51 | it('should allow empty object assignment', () => { 52 | store.instance = {}; 53 | store.state.should.deep.equal({}); 54 | }); 55 | 56 | it('should allow correct state assignment', () => { 57 | store.state = (store.state); 58 | }); 59 | 60 | it('should disallow incorrect state assignment', () => { 61 | expect(() => store.state = 0).to.throw(TypeError); 62 | }); 63 | 64 | it('should allow single property object assignment', () => { 65 | store.instance = { p: 1 }; 66 | store.state.should.deep.equal({ p: 1 }); 67 | }); 68 | 69 | it('should allow multiple complex item object assignment', () => { 70 | const test = { 71 | prop1: { 72 | prop: { 73 | prop: 1 74 | } 75 | }, 76 | prop2: { 77 | prop1: 1, 78 | prop2: [1, 2, 3] 79 | } 80 | }; 81 | store.instance = test; 82 | store.state.should.deep.equal(test); 83 | store.state.should.not.equal(test); 84 | }); 85 | }); 86 | 87 | context('properties', () => { 88 | context('#get', () => { 89 | it('should not mutate the object', () => { 90 | let pre = store.state; 91 | store.instance.get(0); 92 | actions.should.have.length(0); 93 | store.state.should.equal(pre); 94 | }); 95 | 96 | it('should get properties from the object', () => { 97 | store.state = { a: 1, b: 2, c: 3 }; 98 | store.instance.get('b').should.equal(2); 99 | }); 100 | 101 | it('should return undefined for properties that don\'t exist', () => { 102 | store.state = { a: 1, b: 2, c: 3 }; 103 | expect(store.instance.get('d')).to.be.undefined; 104 | expect(store.instance.get(0)).to.be.undefined; 105 | expect(store.instance.get()).to.be.undefined; 106 | expect(store.instance.get('missing')).to.be.undefined; 107 | }); 108 | }); 109 | 110 | context('#set', () => { 111 | it('should mutate the object', () => { 112 | let pre = store.state; 113 | store.instance.set(0, 3); 114 | actions.should.have.length(1); 115 | store.state.should.not.equal(pre); 116 | }); 117 | 118 | it('should set properties in the object', () => { 119 | store.state = { a: 1, b: 2, c: 3 }; 120 | store.instance.set('c', 8); 121 | store.state.should.deep.equal({ a: 1, b: 2, c: 8 }); 122 | }); 123 | 124 | it('should extend the object when setting an undefined property', () => { 125 | store.state = { a: 1, b: 2, c: 3 }; 126 | store.instance.set('x', 8); 127 | store.state.should.deep.equal({ a: 1, b: 2, c: 3, x: 8 }); 128 | }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/types/model.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { model, ObjectId, collection } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('model', () => { 9 | context('type', () => { 10 | let schema 11 | , store 12 | ; 13 | 14 | store = schemaStore(model('Model', { p:Number }),{ debug: true }, createStore); 15 | schema = store.schema; 16 | 17 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 18 | name: 'Model', 19 | kind: 'object', 20 | storageKinds: ['object'] 21 | })); 22 | 23 | context('Should throw on attempts to create a model from a type that isn\'t an object', () => { 24 | it('with undefined', () => { 25 | expect(() => { 26 | schemaStore(model('Model', undefined), { debug: true }, createStore); //eslint-disable-line no-new 27 | }).to.throw(); 28 | }); 29 | 30 | it('with null', () => { 31 | expect(() => { 32 | schemaStore(model('Model', null), { debug: true }, createStore); //eslint-disable-line no-new 33 | }).to.throw(); 34 | }); 35 | 36 | it('with Boolean', () => { 37 | expect(() => { 38 | schemaStore(model('Model', Boolean), { debug: true }, createStore); //eslint-disable-line no-new 39 | }).to.throw(); 40 | }); 41 | 42 | it('with String', () => { 43 | expect(() => { 44 | schemaStore(model('Model', String), { debug: true }, createStore); //eslint-disable-line no-new 45 | }).to.throw(); 46 | }); 47 | }); 48 | 49 | it('should create an id property if none passed', () => { 50 | let store = schemaStore(model('Model', {}), { debug: true }, createStore); 51 | store.schema.properties.should.have.property('id'); 52 | store.schema.properties.id.name.should.equal('objectid'); 53 | }); 54 | 55 | it('should allow creation of a differently named id property', () => { 56 | let store = schemaStore(model('Model', { ident: ObjectId }), { debug: true }, createStore); 57 | store.schema.properties.should.not.have.property('id'); 58 | store.schema.properties.should.have.property('ident'); 59 | store.schema.properties.ident.name.should.equal('objectid'); 60 | }); 61 | 62 | it('should throw when the id property is set to a different type thasn ObjectId', () => { 63 | expect(() => { 64 | schemaStore(model('Model', { id: String }), { debug: true }, createStore); //eslint-disable-line no-new 65 | }).to.throw; 66 | }); 67 | }); 68 | 69 | context('instance', () => { 70 | it('should throw on attempt to create new instance without a collection', () => { 71 | expect(() => { 72 | let store = schemaStore(model('Model', {}), { debug: true }, createStore) 73 | , Model = store.schema 74 | ; 75 | 76 | new Model(); //eslint-disable-line no-new 77 | }).to.throw(); 78 | }); 79 | 80 | it('should create new instance when in a collection', () => { 81 | let store = schemaStore(collection(model('Model', {})), { debug: true }, createStore) 82 | , Model = store.instance.model 83 | , instance 84 | ; 85 | 86 | instance = new Model(); 87 | instance.id.should.be.ok; 88 | 89 | store.instance.get(instance.id).should.equal(instance); 90 | }); 91 | 92 | it('should create an instance when called without new', () => { 93 | let store = schemaStore(collection(model('Model', {})), { debug: true }, createStore) 94 | , Model = store.instance.model 95 | , instance 96 | ; 97 | 98 | instance = Model(); 99 | instance.id.should.be.ok; 100 | 101 | store.instance.get(instance.id).should.equal(instance); 102 | }); 103 | 104 | it('should set properties when passed into the default constructor', () => { 105 | let store = schemaStore(collection(model('Model', { foo: String, bar: Number, '*': Boolean })), { debug: true }, createStore) 106 | , Model = store.instance.model 107 | , instance 108 | ; 109 | 110 | instance = Model({ foo: 'baz', bar: 42, other: true }); 111 | instance.foo.should.equal('baz'); 112 | instance.bar.should.equal(42); 113 | instance.get('other').should.equal(true); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-schema", 3 | "version": "4.8.0", 4 | "description": "Automatic actions, reducers and validation for Redux. Use State like mutable objects without violating any of the redux principles.", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es/index.js", 7 | "files": [ 8 | "dist", 9 | "lib", 10 | "es", 11 | "src" 12 | ], 13 | "scripts": { 14 | "clean": "rimraf lib dist es coverage", 15 | "lint": "eslint src test examples build", 16 | "test": "cross-env BABEL_ENV=commonjs mocha --compilers js:babel-register --recursive", 17 | "test:push": "cross-env BABEL_ENV=commonjs mocha --compilers js:babel-register --recursive -R dot", 18 | "check:src": "npm run lint && npm run test", 19 | "check:push": "npm run lint && npm run test:push", 20 | "check:pushwin": "hidecon && npm run check:push", 21 | "check:es3-syntax": "check-es3-syntax lib/ dist/ --kill", 22 | "check:es3-syntax-print": "check-es3-syntax lib/ dist/ -p", 23 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 24 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 25 | "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js dist/redux-schema.js", 26 | "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack src/index.js dist/redux-schema.min.js", 27 | "build:examples": "cross-env BABEL_ENV=commonjs babel-node examples/buildAll.js", 28 | "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", 29 | "prepublish": "npm run clean && npm run check:src && npm run build && npm run check:es3-syntax", 30 | "coverage": "cross-env BABEL_ENV=commonjs babel-node node_modules/isparta/bin/isparta cover node_modules/mocha/bin/_mocha -- --recursive -R dot", 31 | "coveralls": "npm run lint && cross-env BABEL_ENV=commonjs babel-node node_modules/isparta/bin/isparta cover node_modules/mocha/bin/_mocha --report lcovonly -- --recursive -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/ddsol/redux-schema.git" 36 | }, 37 | "keywords": [ 38 | "redux", 39 | "schema", 40 | "redux-schema", 41 | "validation", 42 | "mutable" 43 | ], 44 | "author": "Han de Boer", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/ddsol/redux-schema/issues" 48 | }, 49 | "dependencies": { 50 | "co": "^4.6.0", 51 | "deepmerge": "^1.3.0", 52 | "serialize-error": "^2.0.0", 53 | "simpexp": "^0.1.0", 54 | "uuid": "^3.0.0" 55 | }, 56 | "devDependencies": { 57 | "babel-cli": "^6.10.1", 58 | "babel-core": "^6.10.4", 59 | "babel-eslint": "^7.1.1", 60 | "babel-loader": "^6.2.0", 61 | "babel-plugin-check-es2015-constants": "^6.3.13", 62 | "babel-plugin-transform-es2015-arrow-functions": "^6.3.13", 63 | "babel-plugin-transform-es2015-block-scoped-functions": "^6.3.13", 64 | "babel-plugin-transform-es2015-block-scoping": "^6.10.1", 65 | "babel-plugin-transform-es2015-classes": "^6.3.13", 66 | "babel-plugin-transform-es2015-computed-properties": "^6.3.13", 67 | "babel-plugin-transform-es2015-destructuring": "^6.3.13", 68 | "babel-plugin-transform-es2015-for-of": "^6.3.13", 69 | "babel-plugin-transform-es2015-function-name": "^6.3.13", 70 | "babel-plugin-transform-es2015-literals": "^6.3.13", 71 | "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3", 72 | "babel-plugin-transform-es2015-object-super": "^6.3.13", 73 | "babel-plugin-transform-es2015-parameters": "^6.3.13", 74 | "babel-plugin-transform-es2015-shorthand-properties": "^6.3.13", 75 | "babel-plugin-transform-es2015-spread": "^6.3.13", 76 | "babel-plugin-transform-es2015-sticky-regex": "^6.3.13", 77 | "babel-plugin-transform-es2015-template-literals": "^6.3.13", 78 | "babel-plugin-transform-es2015-unicode-regex": "^6.3.13", 79 | "babel-plugin-transform-es3-member-expression-literals": "^6.8.0", 80 | "babel-plugin-transform-es3-property-literals": "^6.8.0", 81 | "babel-plugin-transform-object-assign": "^6.5.0", 82 | "babel-plugin-transform-object-rest-spread": "^6.3.13", 83 | "babel-register": "^6.3.13", 84 | "chai": "^3.5.0", 85 | "check-es3-syntax-cli": "^0.1.0", 86 | "coveralls": "^2.11.9", 87 | "cross-env": "^3.1.3", 88 | "eslint": "^3.10.2", 89 | "eslint-plugin-react": "^6.7.1", 90 | "isparta": "^4.0.0", 91 | "mocha": "^3.1.2", 92 | "redux": "^3.4.0", 93 | "rimraf": "^2.3.4", 94 | "webpack": "^1.9.6", 95 | "istanbul": "0.4.4" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | 3 | export function isArray(arr) { 4 | //Adapted from extend, Copyright (c) 2014 Stefan Thomas, MIT license 5 | if (typeof Array.isArray === 'function') { 6 | return Array.isArray(arr); 7 | } 8 | return Object.prototype.toString.call(arr) === '[object Array]'; 9 | } 10 | 11 | export function isPlainObject(obj) { 12 | //Adapted from extend, Copyright (c) 2014 Stefan Thomas, MIT license 13 | if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') { 14 | return false; 15 | } 16 | 17 | let hasOwnConstructor = Object.prototype.hasOwnProperty.call(obj, 'constructor') 18 | , hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && Object.prototype.hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf') 19 | , key 20 | ; 21 | 22 | if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { 23 | return false; 24 | } 25 | for (key in obj) {/**/} 26 | return typeof key === 'undefined' || Object.prototype.hasOwnProperty.call(obj, key); 27 | } 28 | 29 | export function guid(len) { 30 | let id = ''; 31 | len = len || 24; 32 | 33 | while (id.length < len) { 34 | id += uuid.v1().replace(/-/g, ''); 35 | } 36 | return id.substr(0, len); 37 | } 38 | 39 | export let snakeCase = camelCase => String(camelCase).replace(/[A-Z]/g, v=>'_' + v).toUpperCase().replace(/^_/, ''); 40 | 41 | export function pathToStr(path, delParams) { 42 | let result = '' 43 | , item 44 | ; 45 | for (let i = 0; i < path.length; i++) { 46 | item = path[i]; 47 | if (delParams) { 48 | item = item.replace(/\(.*/, ''); 49 | } 50 | if (i === 0 || /^[a-z$_][0-9a-z$_]*$/i.test(item)) { 51 | result += '.' + path[i]; 52 | } else { 53 | if (String(Math.round(Number(item))) === item) { 54 | result += `[${item}]`; 55 | } else { 56 | result += `[${JSON.stringify(String(item))}]`; 57 | } 58 | } 59 | } 60 | return result.replace(/^\./, '').replace(/\[\"\*\"\]/g, '[]'); 61 | } 62 | 63 | export function namedFunction(name, actualFunc, templateFunc, passThrough) { 64 | if (passThrough) { 65 | return actualFunc; 66 | } 67 | if (!templateFunc) { 68 | templateFunc = actualFunc; 69 | } 70 | 71 | let f = actualFunc // eslint-disable-line 72 | , funcText 73 | , signature 74 | , func 75 | ; 76 | 77 | if (typeof actualFunc !== 'function') throw new TypeError('Parameter to namedFunction must be a function'); 78 | 79 | funcText = templateFunc.toString(); 80 | 81 | signature = /\([^\)]*\)/.exec(funcText)[0]; 82 | 83 | funcText = funcText.replace(/^[^{]+\{|}$/g, ''); 84 | 85 | func = eval(`(function(){function ${name + signature} {/*${funcText.replace(/\*\//g, '* /')}*/return f.apply(this,arguments);}return ${name}}())`); // eslint-disable-line 86 | func.toString = function() { 87 | return `function ${name + signature}{${funcText}}`; 88 | }; 89 | return func; 90 | } 91 | 92 | export function toObject(value) { 93 | 94 | let seenValues = [] 95 | , seenObjects = [] 96 | ; 97 | 98 | function internal(value) { 99 | if ((value instanceof Date) || (value instanceof RegExp) || (value instanceof Error)) return value; 100 | if (typeof value !== 'object' || !value) return value; 101 | if (!('keys' in value) || !('_meta' in value)) return value; 102 | if (value._meta.type.kind === 'array') return value.slice().map(v => internal(v)); 103 | let result = {} 104 | , ix = seenValues.indexOf(value) 105 | ; 106 | 107 | if (ix !== -1) { 108 | return seenObjects[ix]; 109 | } 110 | 111 | seenValues.push(value); 112 | seenObjects.push(result); 113 | 114 | value.keys.forEach(key => { 115 | result[key] = internal(value.get(key)); 116 | }); 117 | return result; 118 | } 119 | 120 | return internal(value); 121 | } 122 | 123 | export function dedent(strings, ...args) { 124 | let string = strings 125 | .map((str, i) => (i === 0 ? '' : args[i - 1]) + str) 126 | .join('') 127 | , match = /^\n( *)/.exec(string) 128 | , len 129 | , replace 130 | ; 131 | 132 | if (!match) return string; 133 | 134 | len = match[1].length; 135 | 136 | replace = new RegExp(`\\n {${len}}`,'g'); 137 | 138 | return string.replace(replace, '\n').substr(1); 139 | } 140 | 141 | export function isGenerator(obj) { 142 | return typeof obj.next === 'function' && typeof obj.throw === 'function'; 143 | } 144 | 145 | export function isGeneratorFunction(obj) { 146 | const constructor = obj.constructor; 147 | if (!constructor) return false; 148 | if (constructor.name === 'GeneratorFunction' || constructor.displayName === 'GeneratorFunction') return true; 149 | return isGenerator(constructor.prototype); 150 | } 151 | 152 | export function owner(obj) { 153 | return obj && obj._meta && obj._meta.owner; 154 | } 155 | 156 | export function scratch(obj) { 157 | return obj && obj._meta && obj._meta.scratch; 158 | } 159 | -------------------------------------------------------------------------------- /src/parse/hydrate.js: -------------------------------------------------------------------------------- 1 | import { snakeCase, pathToStr, namedFunction, toObject } from '../utils'; 2 | import co from 'co'; 3 | import { isGeneratorFunction } from '../utils'; 4 | 5 | function freezeObject(obj) { 6 | if (Object.freeze) { 7 | Object.freeze(obj); 8 | } 9 | } 10 | 11 | export function hydratePrototype({ type, typePath, getter, setter, keys, properties = {}, methods = {}, virtuals = {}, meta = {}, freeze, namedFunctions, wrapGenerators = false, generatorWrapper }) { 12 | let prototype = Object.create(type.kind === 'array' ? Array.prototype : Object.prototype) 13 | , define = {} 14 | , typeSnake = snakeCase(pathToStr(typePath)).replace('.', '_') 15 | ; 16 | 17 | meta = { 18 | ...meta, 19 | type, 20 | typePath, 21 | recordRead() { 22 | this.store.recordRead(this.storePath); 23 | }, 24 | scratch: {} 25 | }; 26 | 27 | Object.defineProperties(meta, { 28 | state: { 29 | get: function() { 30 | return this.store.get(this.storePath); 31 | } 32 | , 33 | set: function(value) { 34 | this.store.put(this.storePath, value); 35 | } 36 | }, 37 | options: { 38 | get: function() { 39 | return this.type && this.type.options; 40 | } 41 | } 42 | }); 43 | 44 | define.get = { 45 | enumerable: true, 46 | value: function(propName) { 47 | if (virtuals[propName]) return this[propName]; 48 | let meta = this._meta; 49 | return getter.call(this, propName, meta.store, meta.storePath, meta.instancePath, this); 50 | } 51 | }; 52 | 53 | define.set = { 54 | enumerable: true, 55 | value: function(propName, value) { 56 | if (virtuals[propName]) { 57 | this[propName] = value; 58 | return; 59 | } 60 | let meta = this._meta; 61 | return setter.call(this, propName, value, meta.store, meta.storePath, meta.instancePath, this); 62 | } 63 | }; 64 | 65 | define.keys = { 66 | enumerable: false, 67 | get: keys 68 | }; 69 | 70 | if (freeze) { 71 | freezeObject(meta); 72 | } 73 | 74 | define._meta = { 75 | enumerable: false, 76 | value: meta 77 | }; 78 | 79 | define.toObject = { 80 | enumerable: false, 81 | value: function() { 82 | return toObject(this); 83 | } 84 | }; 85 | 86 | define.inspect = { 87 | enumerable: false, 88 | value: function() { 89 | return toObject(this); 90 | } 91 | }; 92 | 93 | Object.keys(properties).forEach((propName) => { 94 | define[propName] = { 95 | enumerable: true, 96 | get: function() { 97 | let meta = this._meta; 98 | return getter.call(this, propName, meta.store, meta.storePath, meta.instancePath, meta.instance); 99 | }, 100 | set: function(value) { 101 | let meta = this._meta; 102 | return setter.call(this, propName, value, meta.store, meta.storePath, meta.instancePath, meta.instance); 103 | } 104 | }; 105 | }); 106 | 107 | Object.keys(methods).forEach((methodName) => { 108 | let invokeName = methodName 109 | , method = methods[methodName] 110 | , actionType = (typeSnake ? typeSnake + '_' : '') + snakeCase(methodName) 111 | , func = method 112 | , wrap 113 | , wrapper 114 | ; 115 | 116 | wrapper = generatorWrapper || co.wrap; 117 | wrap = ('wrapGenerator' in method) ? method.wrapGenerator : wrapGenerators; 118 | 119 | if (typeof wrap === 'function') { 120 | wrapper = wrap; 121 | } 122 | 123 | if (wrap && isGeneratorFunction(func)) { 124 | func = wrapper(func); 125 | } 126 | 127 | if (method.noWrap) { 128 | define[methodName] = { 129 | enumerable: true, 130 | value: func 131 | }; 132 | } else { 133 | if (methodName === 'constructor') { 134 | invokeName = typePath[0]; 135 | } 136 | define[methodName] = { 137 | enumerable: true, 138 | value: namedFunction(invokeName, function invokeMethod(...args) { 139 | let meta = this._meta 140 | , path = meta.instancePath.concat(methodName) 141 | ; 142 | return meta.store.invoke(this, actionType, path, func, args); 143 | }, method, !namedFunctions) 144 | }; 145 | } 146 | }); 147 | 148 | Object.keys(virtuals).forEach((virtualName) => { 149 | let actionType = `SET_${typeSnake}_${snakeCase(virtualName)}` 150 | , prop = virtuals[virtualName] 151 | ; 152 | 153 | define[virtualName] = { 154 | enumerable: true, 155 | get: prop.get, 156 | set: function(value) { 157 | let meta = this._meta 158 | , propInstancePath = meta.instancePath.concat(virtualName) 159 | ; 160 | meta.store.setVirtual(this, actionType, propInstancePath, prop.set, value); 161 | } 162 | }; 163 | }); 164 | 165 | Object.defineProperties(prototype, define); 166 | if (freeze) { 167 | freezeObject(prototype); 168 | } 169 | return prototype; 170 | } 171 | 172 | export function hydrateInstance({ prototype, store, storePath, instancePath, currentInstance, meta, freeze }) { 173 | let instance = currentInstance || Object.create(prototype); 174 | 175 | meta = Object.assign(Object.create(instance._meta), meta); 176 | 177 | meta.store = store; 178 | meta.storePath = storePath; 179 | meta.instancePath = instancePath; 180 | 181 | if (freeze) { 182 | freezeObject(meta); 183 | } 184 | 185 | Object.defineProperty(instance, '_meta', { 186 | enumerable: false, 187 | value: meta 188 | }); 189 | 190 | if (freeze) { 191 | freezeObject(instance); 192 | } 193 | 194 | return instance; 195 | } 196 | -------------------------------------------------------------------------------- /src/types/union.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import parseType from '../parse/parse-type'; 3 | import { pathToStr } from '../utils'; 4 | 5 | export default function union(...types) { 6 | if (!types.length) throw new TypeError('Union requires subtypes.'); 7 | 8 | function Union(options) { 9 | let typeMoniker = options.typeMoniker 10 | , simple = true 11 | , storageKinds = {} 12 | , kinds = {} 13 | , handlersById = {} 14 | , self = { options } 15 | , handlerIds 16 | , handlers 17 | ; 18 | 19 | function parse(type, name) { 20 | var moniker = [...options.typeMoniker]; 21 | if (name) { 22 | moniker.push(name); 23 | } 24 | return parseType({ 25 | ...options, 26 | typeMoniker: moniker, 27 | parent: options.self || self, 28 | self: null 29 | }, type); 30 | } 31 | 32 | //Flatten all unions: union(union(null, undefined), union(Number, String)) => union(null, undefined, Number, String) 33 | types = Array.prototype.concat.apply([], types.map(type => { 34 | let handler = parse(type); 35 | if (handler.kind !== 'union') { 36 | return type; 37 | } 38 | return handler.types; 39 | })); 40 | 41 | handlers = types.map(type => parse(type)).map((handler, ix) => { 42 | handler.storageKinds.forEach((kind) => { 43 | if (storageKinds[kind]) { 44 | simple = false; 45 | } else { 46 | storageKinds[kind] = true; 47 | } 48 | }); 49 | if (!kinds[handler.kind]) { 50 | kinds[handler.kind] = 0; 51 | } 52 | kinds[handler.kind]++; 53 | let id = handler.kind + kinds[handler.kind]; 54 | handler = parse(types[ix], id); 55 | handlersById[id] = handler; 56 | return handler; 57 | }); 58 | 59 | handlerIds = Object.keys(handlersById); 60 | 61 | function idOfHandler(handler) { 62 | for (let i = 0; i < handlerIds.length; i++) { 63 | if (handlersById[handlerIds[i]] === handler) return handlerIds[i]; 64 | } 65 | } 66 | 67 | let thisType = { 68 | isType: true, 69 | name: `union(${handlers.map(handler => handler.kind).join(', ')})`, 70 | kind: 'union', 71 | storageKinds: Object.keys(storageKinds), 72 | options, 73 | validateData(value, instancePath) { 74 | instancePath = instancePath || typeMoniker; 75 | if (!simple) { 76 | let keys = Object.keys(value||{}) 77 | , type = keys[0] 78 | ; 79 | if (!value || keys.length !== 1) { 80 | return `Missing union type for union "${pathToStr(instancePath)}"`; 81 | } 82 | if (!handlersById[type]) { 83 | return `Unexpected type "${type}" for union "${pathToStr(instancePath)}"`; 84 | } 85 | return handlersById[type].validateData(value[type], instancePath); 86 | } 87 | return ( 88 | handlers.every(handler => handler.validateData(value, instancePath)) 89 | && `No matching data type for union "${pathToStr(instancePath)}"` 90 | ); 91 | }, 92 | coerceData(value, instancePath) { 93 | if (!thisType.validateData(value, instancePath)) return value; 94 | return thisType.defaultValue(); 95 | }, 96 | validateAssign(value, instancePath) { 97 | instancePath = instancePath || typeMoniker; 98 | let messages = [] 99 | , handler 100 | , message 101 | ; 102 | 103 | for (let i = 0; i < handlers.length; i++) { 104 | message = handlers[i].validateAssign(value); 105 | if (!message) { 106 | handler = handlers[i]; 107 | break; 108 | } 109 | messages.push(message); 110 | } 111 | if (!handler) { 112 | return `Incompatible value ${value} for ${pathToStr(instancePath)}. ${messages.join(' -or- ')}.`; 113 | } 114 | }, 115 | pack(value) { 116 | let messages = [] 117 | , handler 118 | , packed 119 | , message 120 | ; 121 | for (let i = 0; i < handlers.length; i++) { 122 | message = handlers[i].validateAssign(value); 123 | if (!message) { 124 | handler = handlers[i]; 125 | break; 126 | } 127 | messages.push(message); 128 | } 129 | if (!handler) { 130 | throw new TypeError(`Incompatible value ${value} for ${thisType.name}. ${messages.join(' -or- ')}.`); 131 | } 132 | packed = handler.pack(value); 133 | if (simple) { 134 | return packed; 135 | } 136 | return { 137 | [idOfHandler(handler)]: packed 138 | }; 139 | }, 140 | unpack(store, path, instancePath, currentInstance, owner) { 141 | if (currentInstance) throw new Error('Union types cannot modify a data instance'); 142 | let value = store.get(path); 143 | if (simple) { 144 | for (let i = 0; i < handlers.length; i++) { 145 | if (!handlers[i].validateData(value, instancePath)) { 146 | return store.unpack(handlers[i], path, instancePath, null, owner); 147 | } 148 | } 149 | throw new TypeError(`No matching data type for union "${pathToStr(instancePath)}"`); 150 | } else { 151 | let type = Object.keys(value||{})[0]; 152 | if (!handlersById[type]) { 153 | if (type) { 154 | throw new Error(`Unexpected type "${type}" for union "${pathToStr(instancePath)}"`); 155 | } else { 156 | throw new Error(`Missing union type for union "${pathToStr(instancePath)}"`); 157 | } 158 | } 159 | return store.unpack(handlersById[type], path.concat(type), instancePath, null, owner); 160 | } 161 | }, 162 | getTypeFromPath(path) { 163 | if (!path.length) { 164 | return options.typeMoniker; 165 | } 166 | let first = path[0]; 167 | if (!handlersById[first]) throw new Error(`Can't find union subtype ${first}, ${path}, ${handlers[0].options.typeMoniker}`); 168 | return handlersById[first].getTypeFromPath(path.slice(1)); 169 | }, 170 | defaultValue() { 171 | return ( 172 | simple 173 | ? handlers[0].defaultValue() 174 | : { 175 | [idOfHandler(handlers[0])]: handlers[0].defaultValue() 176 | } 177 | ); 178 | }, 179 | handlers, 180 | types 181 | }; 182 | 183 | return Object.assign(self, finalizeType(thisType)); 184 | } 185 | 186 | Union.isType = true; 187 | return Union; 188 | } 189 | 190 | -------------------------------------------------------------------------------- /src/parse/any-object.js: -------------------------------------------------------------------------------- 1 | import finalizeType from '../parse/finalize-type'; 2 | import { pathToStr, isArray, isPlainObject } from '../utils'; 3 | import { arrayMethods, arrayVirtuals } from './array'; 4 | import { hydratePrototype, hydrateInstance } from './hydrate'; 5 | 6 | export default function anyObject(options, arrayType) { 7 | 8 | arrayType = Boolean(arrayType); 9 | 10 | let self = { options } 11 | , kind = arrayType ? 'array' : 'object' 12 | , storedKeys = [] 13 | , storedState 14 | , prototype 15 | , thisType 16 | ; 17 | 18 | if (options.self) { 19 | self = Object.assign(options.self, self); 20 | } 21 | 22 | function isValidObject(value, forceArray) { 23 | if (!isPlainObject(value) && !isArray(value)) { 24 | return false; 25 | } 26 | 27 | if (typeof forceArray !== 'undefined') { 28 | if (isArray(value) !== forceArray) return false; 29 | } 30 | 31 | const keys = Object.keys(value); 32 | for (let i = 0; i < keys.length; i++) { 33 | const propVal = value[keys[i]]; 34 | if (typeof propVal === 'object' && propVal !== null && !isValidObject(propVal)) { 35 | return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | function coerceObject(value, forceArray) { 42 | if (isValidObject(value, forceArray)) return value; 43 | if (!isPlainObject(value) && !isArray(value)) { 44 | return forceArray ? [] : {}; 45 | } 46 | if (typeof forceArray !== 'undefined') { 47 | if (isArray(value) !== forceArray) return forceArray ? [] : {}; 48 | } 49 | 50 | const keys = Object.keys(value); 51 | const result = forceArray ? [] : {}; 52 | for (let i = 0; i < keys.length; i++) { 53 | const key = keys[i]; 54 | const propVal = value[key]; 55 | if (typeof propVal === 'object' && propVal !== null) { 56 | result[key] = coerceObject(value); 57 | } else { 58 | result[key] = value; 59 | } 60 | } 61 | return result; 62 | } 63 | 64 | function clone(obj) { 65 | if (typeof obj !== 'object' || obj === null) { 66 | return obj; 67 | } 68 | let out = isArray(obj) ? [] : {} 69 | , isSchemaObject = obj && obj._meta && obj._meta.type && (obj._meta.type.kind === 'object' || obj._meta.type.kind === 'array') 70 | , keys 71 | , key 72 | , value 73 | ; 74 | 75 | function getProp(prop) { 76 | if (isSchemaObject) { 77 | return obj.get(prop); 78 | } 79 | return obj[prop]; 80 | } 81 | 82 | if (isSchemaObject) { 83 | keys = obj.keys; 84 | } else { 85 | keys = Object.keys(obj); 86 | } 87 | 88 | for (let i = 0; i < keys.length; i++) { 89 | key = keys[i]; 90 | value = getProp(key); 91 | if (typeof value === 'object' && value !== null) { 92 | value = clone(value); 93 | } 94 | out[key] = value; 95 | } 96 | return out; 97 | } 98 | 99 | thisType = { 100 | isType: true, 101 | name: pathToStr(options.typeMoniker) || arrayType ? 'array' : 'object', 102 | kind, 103 | storageKinds: [kind], 104 | options, 105 | validateData(value, instancePath) { 106 | instancePath = instancePath || options.typeMoniker; 107 | if (!isValidObject(value, arrayType)) { 108 | return `Type of "${pathToStr(instancePath)}" data must be ${kind}`; 109 | } 110 | }, 111 | coerceData(value) { 112 | return coerceObject(value, arrayType); 113 | }, 114 | validateAssign(value, instancePath) { 115 | instancePath = instancePath || options.typeMoniker; 116 | if (!isValidObject(value, arrayType)) { 117 | return `Type of "${pathToStr(instancePath)}" data must be ${kind}`; 118 | } 119 | }, 120 | pack(value) { 121 | if (!isValidObject(value, arrayType)) { 122 | throw new TypeError(`${pathToStr(options.typeMoniker)} only accepts simple ${kind}s`); 123 | } 124 | return clone(value); 125 | }, 126 | unpack(store, storePath, instancePath, currentInstance, owner) { 127 | return hydrateInstance({ 128 | ...options, 129 | prototype, 130 | store, 131 | storePath, 132 | instancePath, 133 | currentInstance, 134 | meta: { owner } 135 | }); 136 | }, 137 | getTypeFromPath(path) { 138 | return options.typeMoniker.concat(path); 139 | }, 140 | defaultValue(){ 141 | return arrayType ? [] : {}; 142 | }, 143 | properties: {}, 144 | methods: {}, 145 | virtuals: {}, 146 | defaultRestProp(){}, 147 | packProp(name, value) { 148 | if (typeof value === 'object' && value !== null && !isValidObject(value)) { 149 | throw new TypeError(`${pathToStr(options.typeMoniker.concat(name))} only accepts simple types`); 150 | } 151 | return clone(value); 152 | } 153 | }; 154 | 155 | if ('name' in self) { 156 | delete thisType.name; 157 | } 158 | self = Object.assign(self, finalizeType(thisType)); 159 | 160 | prototype = hydratePrototype({ 161 | type: self, 162 | typePath: options.typeMoniker, 163 | getter(name) { 164 | let meta = this._meta 165 | , storeValue = meta.store.get(meta.storePath) 166 | , propValue = storeValue[name] 167 | , array = isArray(propValue) 168 | , ix = Number(name) 169 | , type 170 | ; 171 | 172 | if (arrayType) { 173 | if (isNaN(name) || ((ix % 1) !== 0) || ix < 0 || String(ix) !== String(name) || ix >= this.length) { 174 | return undefined; 175 | } 176 | } 177 | 178 | if (typeof propValue === 'object' && propValue !== null) { 179 | type = anyObject({ 180 | ...options, 181 | parent: self, 182 | self: null, 183 | typeMoniker: options.typeMoniker.concat(name) 184 | }, array); 185 | return meta.store.unpack(type, meta.storePath.concat(name), meta.instancePath.concat(name), null, this); 186 | } else { 187 | return propValue; 188 | } 189 | }, setter(name, value) { 190 | let meta = this._meta 191 | , ix = Number(name) 192 | , newState 193 | ; 194 | if (typeof value === 'object' && value === null && !isValidObject(value)) { 195 | throw new TypeError(`${pathToStr(options.typeMoniker.concat(name))} only accepts simple types`); 196 | } 197 | if (arrayType) { 198 | if (isNaN(name) || ((ix % 1) !== 0) || ix < 0 || String(ix) !== String(name)) { 199 | throw new TypeError(`Cannot set "${pathToStr(options.typeMoniker.concat(name))}" property on array`); 200 | } 201 | newState = meta.store.get(meta.storePath); 202 | if (ix > this.length) { 203 | newState = newState.slice(); 204 | while (ix > newState.length) { 205 | newState.push(undefined); 206 | } 207 | newState.push(clone(value)); 208 | return meta.store.put(meta.storePath, newState); 209 | } 210 | } 211 | meta.store.put(meta.storePath.concat(name), clone(value)); 212 | }, keys() { 213 | this._meta.store.recordRead(this._meta.storePath); 214 | let state = this._meta.state; 215 | if (storedState !== state) { 216 | storedKeys = Object.keys(state); 217 | storedState = state; 218 | } 219 | return storedKeys; 220 | }, 221 | methods: arrayType ? arrayMethods : {}, 222 | virtuals: arrayType ? arrayVirtuals : {} 223 | }); 224 | 225 | self.prototype = prototype; 226 | 227 | return self; 228 | } 229 | 230 | -------------------------------------------------------------------------------- /src/parse/array.js: -------------------------------------------------------------------------------- 1 | import reducer from '../modifiers/reducer'; 2 | import bare from '../modifiers/bare'; 3 | 4 | function recordLength(array) { 5 | if (!array._meta.storePath) { 6 | console.log(Object.keys(array._meta)); 7 | } 8 | array._meta.recordRead(array._meta.storePath.concat('length')); 9 | } 10 | 11 | export const arrayMethods = { 12 | concat: bare(function() { 13 | var plain = this.slice(); 14 | return plain.concat.apply(plain, arguments); 15 | }), 16 | 17 | copyWithin: reducer(function(state, args) { 18 | let plain = state.slice(); 19 | return plain.copyWithin.apply(plain, args); 20 | }), 21 | 22 | every: bare(function(func, thisArg) { 23 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 24 | 25 | for (let i = 0, l = this.length; i < l; i++) { 26 | if (!func.call(thisArg, this.get(i), i, this)) return false; 27 | } 28 | return true; 29 | }), 30 | 31 | fill: function(value, start, end) { 32 | start = start || 0; 33 | 34 | let length = this.length; 35 | 36 | if (start < 0) { 37 | start = Math.max(0, length + start); 38 | } else { 39 | start = Math.min(length, start); 40 | } 41 | 42 | if (end === undefined) { 43 | end = length; 44 | } 45 | if (end < 0) { 46 | end = Math.max(0, length + end); 47 | } else { 48 | end = Math.min(length, end); 49 | } 50 | 51 | for (let i = start; i < end; i++) { 52 | this.set(i, value); 53 | } 54 | return this; 55 | }, 56 | 57 | filter: bare(function(func, thisArg) { 58 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 59 | 60 | let result = [] 61 | , item; 62 | 63 | for (let i = 0, l = this.length; i < l; i++) { 64 | item = this.get(i); 65 | if (func.call(thisArg, item, i, this)) { 66 | result.push(item); 67 | } 68 | } 69 | 70 | return result; 71 | }), 72 | 73 | find: bare(function(func, thisArg) { 74 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 75 | 76 | let item; 77 | for (let i = 0, l = this.length; i < l; i++) { 78 | item = this.get(i); 79 | if (func.call(thisArg, item, i, this)) { 80 | return item; 81 | } 82 | } 83 | }), 84 | 85 | findIndex: bare(function(func, thisArg) { 86 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 87 | 88 | let item; 89 | for (let i = 0, l = this.length; i < l; i++) { 90 | item = this.get(i); 91 | if (func.call(thisArg, item, i, this)) { 92 | return i; 93 | } 94 | } 95 | }), 96 | 97 | forEach: bare(function(func, thisArg) { 98 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 99 | 100 | for (let i = 0, l = this.length; i < l; i++) { 101 | func.call(thisArg, this.get(i), i, this); 102 | } 103 | }), 104 | 105 | includes: bare(function(item, start) { 106 | start = start || 0; 107 | 108 | let length = this.length; 109 | 110 | if (start < 0) { 111 | start = Math.max(0, start + length); 112 | } 113 | 114 | if (item !== item) { //NaN check 115 | for (let i = start; i < length; i++) { 116 | item = this.get(i); 117 | if (item !== item) { 118 | return true; 119 | } 120 | } 121 | return false; 122 | } 123 | 124 | for (let i = start; i < length; i++) { 125 | if (item === this.get(i)) { 126 | return true; 127 | } 128 | } 129 | 130 | return false; 131 | }), 132 | 133 | indexOf: bare(function(item, start) { 134 | start = start || 0; 135 | 136 | let length = this.length; 137 | 138 | if (start < 0) { 139 | start = Math.max(0, length + start); 140 | } 141 | 142 | for (let i = start; i < length; i++) { 143 | if (item === this.get(i)) { 144 | return i; 145 | } 146 | } 147 | return -1; 148 | }), 149 | 150 | join: bare(function(separator) { 151 | let result = []; 152 | 153 | for (let i = 0, l = this.length; i < l; i++) { 154 | result.push(this.get(i)); 155 | } 156 | return result.join(separator); 157 | }), 158 | 159 | lastIndexOf: bare(function(item, start) { 160 | let length = this.length; 161 | start = start || length - 1; 162 | 163 | if (start < 0) { 164 | start = Math.max(0, start + length); 165 | } 166 | 167 | for (let i = start; i >= 0; i--) { 168 | if (item === this.get(i)) { 169 | return i; 170 | } 171 | } 172 | return -1; 173 | }), 174 | 175 | map: bare(function(func, thisArg) { 176 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 177 | 178 | let result = []; 179 | 180 | for (let i = 0, l = this.length; i < l; i++) { 181 | result.push(func.call(thisArg, this.get(i), i, this)); 182 | } 183 | 184 | return result; 185 | }), 186 | 187 | pop: reducer(function(state, args, result) { 188 | let newState = state.slice(); 189 | 190 | result.result = this.get(newState.length - 1); 191 | 192 | let isRef = false; 193 | 194 | if (this._meta.type.getPropType) { 195 | isRef = this._meta.type.getPropType(newState.length - 1).kind === 'reference'; 196 | } 197 | 198 | if (!isRef && result.result && typeof result.result.toObject === 'function') { 199 | result.result = result.result.toObject(); 200 | } 201 | newState.pop(); 202 | return newState; 203 | }), 204 | 205 | push: reducer(function(state, args, result) { 206 | let newState = state.slice() 207 | , type = this._meta.type 208 | ; 209 | 210 | if (type.length > 1) throw new TypeError('Tuples cannot be extended'); 211 | result.result = newState.push.apply(newState, args.map((item, ix) => type.packProp(ix, item))); 212 | return newState; 213 | }), 214 | 215 | reduce: bare(function(func, initialValue) { 216 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 217 | 218 | let length = this.length 219 | , reduced 220 | , start 221 | ; 222 | 223 | if (arguments.length < 2) { 224 | if (!length) throw new TypeError('Reduce of empty array with no initial value'); 225 | reduced = this.get(0); 226 | start = 1; 227 | } else { 228 | reduced = initialValue; 229 | start = 0; 230 | } 231 | 232 | for (let i = start; i < length; i++) { 233 | reduced = func(reduced, this.get(i), i, this); 234 | } 235 | 236 | return reduced; 237 | }), 238 | 239 | reduceRight: bare(function(func, initialValue) { 240 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 241 | let length = this.length 242 | , reduced 243 | , start 244 | ; 245 | 246 | if (arguments.length < 2) { 247 | if (!length) throw new TypeError('Reduce of empty array with no initial value'); 248 | reduced = this.get(length - 1); 249 | start = length - 2; 250 | } else { 251 | reduced = initialValue; 252 | start = length - 1; 253 | } 254 | 255 | for (let i = start; i >= 0; i--) { 256 | reduced = func(reduced, this.get(i), i, this); 257 | } 258 | 259 | return reduced; 260 | }), 261 | 262 | reverse: reducer(function(state) { 263 | if (!state.length) return state; 264 | return state.slice().reverse(); 265 | }), 266 | 267 | shift: reducer(function(state, args, result) { 268 | let newState = state.slice(); 269 | result.result = this.get(0); 270 | if (result.result && typeof result.result.toObject === 'function') { 271 | result.result = result.result.toObject(); 272 | } 273 | newState.shift(); 274 | return newState; 275 | }), 276 | 277 | slice: bare(function(start, end) { 278 | start = start || 0; 279 | 280 | let length = this.length; 281 | 282 | if (start < 0) start += length; 283 | end = end || length; 284 | if (end < 0) end += length; 285 | 286 | let result = []; 287 | 288 | for (let i = start; i < end; i++) { 289 | result.push(this.get(i)); 290 | } 291 | return result; 292 | }), 293 | 294 | some: bare(function(func, thisArg) { 295 | if (typeof func !== 'function') throw new TypeError(`${func} is not a function`); 296 | 297 | for (let i = 0, l = this.length; i < l; i++) { 298 | if (func.call(thisArg, this.get(i), i, this)) return true; 299 | } 300 | return false; 301 | }), 302 | 303 | sort: reducer(function(state, args) { 304 | let order = [] 305 | , func = args[0] 306 | ; 307 | 308 | for (let i = 0; i < state.length; i++) order.push(i); 309 | 310 | if (typeof func !== 'function') { 311 | func = function(a, b) { 312 | a = String(a); 313 | b = String(b); 314 | if (a < b) return -1; 315 | if (a > b) return 1; 316 | return 0; 317 | }; 318 | } 319 | 320 | order.sort((a, b) => { 321 | a = this.get(a); 322 | b = this.get(b); 323 | if (a === undefined && b === undefined) return 0; 324 | if (a === undefined) return 1; 325 | if (b === undefined) return -1; 326 | return func(a, b); 327 | }); 328 | 329 | return order.map(i => state[i]); 330 | }), 331 | 332 | splice: reducer(function(state, args, result) { 333 | let start = args[0] || 0 334 | , delCount = args[1] || 0 335 | , rest = args.slice(2) 336 | ; 337 | 338 | if (this._meta.type.length > 1) { 339 | if (delCount !== rest.count || (start + delCount) > this._meta.type.length) { 340 | throw new TypeError('Cannot change the length of a tuple array'); 341 | } 342 | rest = rest.map((item, ix) => this._meta.type.properties[ix].pack(item)); 343 | } else { 344 | rest = rest.map((item, ix) => this._meta.type.packProp(ix, item)); 345 | } 346 | 347 | let newState = state.slice(); 348 | result.result = newState.splice.apply(newState, [start, delCount].concat(rest)); 349 | return newState; 350 | }), 351 | 352 | toLocaleString: bare(function() { 353 | let plain = this.slice(); 354 | return plain.toLocaleString ? plain.toLocaleString.apply(plain, arguments) : plain.toString(); 355 | }), 356 | 357 | toString: bare(function() { 358 | return this.join(); 359 | }), 360 | 361 | unshift: reducer(function(state, args, result) { 362 | let newState = state.slice() 363 | , type = this._meta.type 364 | ; 365 | 366 | if (type.length > 1) throw new TypeError('Tuples cannot be extended'); 367 | result.result = newState.unshift.apply(newState, args.map((item, ix) => type.packProp(ix, item))); 368 | return newState; 369 | }), 370 | 371 | valueOf: bare(function() { 372 | return this.toObject(); 373 | }) 374 | }; 375 | 376 | export const arrayVirtuals = { 377 | length: { 378 | get: function() { 379 | recordLength(this); 380 | return this._meta.state.length; 381 | }, 382 | set: function(value) { 383 | if (this._meta.type.length > 1) throw new TypeError('Cannot change the length of a tuple array'); 384 | let newState = this._meta.state.slice(0, value) 385 | , defaultRestProp = this._meta.type.defaultRestProp.bind(this._meta.type) || (v => undefined) 386 | ; 387 | 388 | while (newState.length < value) { 389 | newState.push(defaultRestProp()); 390 | } 391 | this._meta.state = newState; 392 | } 393 | } 394 | }; 395 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux-Schema 2 | 3 | [![build status](https://img.shields.io/travis/ddsol/redux-schema.svg?style=flat-square)](https://travis-ci.org/ddsol/redux-schema) 4 | [![Coveralls](https://img.shields.io/coveralls/ddsol/redux-schema.svg?style=flat-square)](https://coveralls.io/github/ddsol/redux-schema) 5 | [![npm version](https://img.shields.io/npm/v/redux-schema.svg?style=flat-square)](https://www.npmjs.com/package/redux-schema) 6 | [![npm downloads](https://img.shields.io/npm/dm/redux-schema.svg?style=flat-square)](https://www.npmjs.com/package/redux-schema) 7 | 8 | ### Introduction 9 | 10 | Redux Schema is a system to use [Redux](https://github.com/reactjs/redux) without needing to write any action creators or reducers. If you don't know what Redux is all about, you should spend some time looking at it. There's a whole community for you to connect with. 11 | 12 | Redux is based on 3 principles: 13 | - Single source of truth 14 | - State is read-only 15 | - Changes are made with pure functions 16 | 17 | Also, the state is best kept serializable so it can be packed up and shipped easily. 18 | 19 | The above principles create applications that are easy to manage as they grow from tiny tests into large complex applications. 20 | 21 | ### Why Schema? 22 | 23 | Redux is a very small library. It's designed to help without getting in the way. It only covers a very small area, namely managing the state. Even there it doesn't touch the state. It leaves this to the reducers, which copy-and-modify the state. 24 | 25 | The code to copy-and-modify the state is fairly simple in each case, and using rest spread and such, it's even fairly clean. The matching action creators are also tiny and quick to write. 26 | 27 | The trouble comes when you require many actions with matching reducers. The reducers usually live in a separate file. Nonetheless, most often each action creator is paired with a single reducer case. Moreover, the action creators are extremely similar from one to the next and writing them quickly feels like boilerplate coding. The reducers, due to their pure-functional nature, aren't always easily readable. The intent of simply setting a property is easily lost in code like `return { ...state, myProp: action.value }`. Also, this code is embedded in a case statement that can grow to unwieldly proportions. 28 | 29 | And when you have these reducers and action creators, you have to make sure they are being tested. Each has to be matched with a test or 2 to make sure it does its job. 30 | 31 | Less obvious when you start coding this way is that you lose out on something we're very much used to when we write JavaScript, and that is Object Oriented Programming. By turning every mutation into an action and sending this to a central processing plant (the reducer), the code to act on our data is no longer attached to the data. `user.friend(otherUser)` becomes `dispatch(friendUser(requester, invitee))` and the actual code that does the work is found elsewhere and can't reference `this`. 32 | 33 | Redux-Schema is designed to overcome these issues. It allows you to use Redux without needing to write any reducers, actionTypes, actionCreators or dispatch calls. 34 | 35 | ### Example 36 | 37 | To whet your appetite, check out the todo example in `examples/todos`. Compare it to the redux version of the same example. You'll see that it contains a lot less code. 38 | 39 | In particular: 40 | - No action types 41 | - No action creators 42 | - No reducers 43 | - No selectors (`mapStateToProps`) 44 | - No action selecting / binding (`mapDispatchToProps`) 45 | 46 | ### What does it look like? 47 | 48 | A picture is worth 1000 words. Unfortunately, I'm no artist. So here's some code: 49 | 50 | ```js 51 | import schemaStore, { model, optional, Nil, bare, reference, collections, union } from 'redux-schema'; 52 | import { createStore } from 'redux'; 53 | 54 | 55 | let userModel = schema('User', { 56 | first: optional(String), 57 | last: optional(String), 58 | 59 | address: union(schema.Nil, { 60 | street: String, 61 | town: String 62 | }, { 63 | POBox: String, 64 | town: String 65 | }), 66 | 67 | constructor(foo, bar) { 68 | if (foo === bar) { 69 | console.log('The foo is the bar!') 70 | } else { 71 | console.log('The foo and the bar are not the same.') 72 | } 73 | }, 74 | 75 | get full() { 76 | return this.first + ' ' + this.last; 77 | }, 78 | 79 | set full(full) { 80 | let split = full.split(' '); 81 | this.first = split.shift(); 82 | this.last = split.join(' '); 83 | }, 84 | 85 | getGreeting() { 86 | if (this.full === 'Foo Bar') { 87 | return 'Baz Quux!'; 88 | } else { 89 | return 'Hello ' + this.first + ' from ' + this.address.town; 90 | } 91 | }, 92 | 93 | friend: optional(schema.reference('user')), 94 | 95 | makeFoo() { 96 | this.full = 'Foo Bar'; 97 | } 98 | }); 99 | 100 | let root = collections([userModel]); 101 | 102 | let store = schemaStore(root, { debug: true })(redux.createStore)(); 103 | 104 | let { User } = store.models; 105 | 106 | let user = new User('foo', 'bar'); 107 | /* 108 | generates: 109 | 110 | dispatch({ 111 | type: 'USER_CONSTRUCTOR', 112 | path: [ 'user', 'fc6e4b60004c11e6963a4dd9', 'constructor' ], 113 | args: [ 'foo', 'bar' ] 114 | }); 115 | 116 | new state: 117 | { 118 | user: { 119 | '9b66b7d0005111e68f23a7ab': { 120 | first: undefined, 121 | last: undefined, 122 | address: { type: '1:union', value: null }, 123 | friend: undefined, 124 | id: '9b66b7d0005111e68f23a7ab' 125 | } 126 | } 127 | } 128 | */ 129 | 130 | console.log(user.full); //"undefined undefined" 131 | /* This doesn't generate any action */ 132 | 133 | user.full = 'First Last'; 134 | /* 135 | generates: 136 | 137 | dispatch({ 138 | type: 'USER_SET_FULL', 139 | path: [ 'user', 'fc6e4b60004c11e6963a4dd9', 'full' ], 140 | value: 'First Last' 141 | }); 142 | 143 | new state: 144 | { 145 | user: { 146 | '9b66b7d0005111e68f23a7ab': { 147 | address: { type: '1:union', value: null }, 148 | id: '9b66b7d0005111e68f23a7ab', 149 | friend: undefined, 150 | first: 'First', 151 | last: 'Last' 152 | } 153 | } 154 | } 155 | */ 156 | 157 | console.log(user.full); //First Last 158 | 159 | user.makeFoo(); 160 | /* 161 | generates: 162 | dispatch({ 163 | type: 'USER_MAKE_FOO', 164 | path: [ 'user', '9b66b7d0005111e68f23a7ab', 'makeFoo' ], 165 | args: [] 166 | }); 167 | 168 | new state: 169 | { 170 | user: { 171 | '9b66b7d0005111e68f23a7ab': { 172 | address: { type: '1:union', value: null }, 173 | id: '9b66b7d0005111e68f23a7ab', 174 | friend: undefined, 175 | first: 'Foo', 176 | last: 'Bar' 177 | } 178 | } 179 | } 180 | */ 181 | 182 | console.log(user.getGreeting()); 183 | /* 184 | Because it's wrapped in schema.bare, it doesn't generate any action. 185 | If it did set any properties, it would result in the same actions as if 186 | when those properties were set from outside a method. 187 | */ 188 | 189 | user.address = { street: '123 west somewhere', town: 'Wiggletown' }; 190 | /* 191 | 192 | generates: 193 | dispatch({ 194 | type: 'SET_USER_ADDRESS', 195 | prop: true, 196 | path: [ 'user', '9b66b7d0005111e68f23a7ab', 'address' ], 197 | value: { 198 | type: '1:object', 199 | value: { street: '123 west somewhere', town: 'Wiggletown' } 200 | } 201 | }); 202 | 203 | new state: 204 | { 205 | user: { 206 | '9b66b7d0005111e68f23a7ab': { 207 | address: { 208 | type: '1:object', 209 | value: { street: '123 west somewhere', town: 'Wiggletown' } 210 | }, 211 | id: '9b66b7d0005111e68f23a7ab', 212 | friend: undefined, 213 | first: 'Foo', 214 | last: 'Bar' 215 | } 216 | } 217 | } 218 | 219 | Note that the storage of the union of 2 different objects results in the 220 | store having extra information about the data type. This doesn't interfere 221 | with the usage of this data. The store is simply the backend representation. 222 | */ 223 | 224 | let ref1 = user.address; 225 | user.address = { POBox : '101', town: '12' }; 226 | /* 227 | 228 | generates: 229 | { 230 | type: 'SET_USER_ADDRESS', 231 | prop: true, 232 | path: [ 'user', '9b66b7d0005111e68f23a7ab', 'address' ], 233 | value: { 234 | type: '2:object', value: { POBox: '101', town: '12' } 235 | } 236 | } 237 | 238 | new state: 239 | { 240 | user: { 241 | '9b66b7d0005111e68f23a7ab': { 242 | address: { 243 | type: '2:object', 244 | value: { POBox: '101', town: '12' } 245 | }, 246 | id: '9b66b7d0005111e68f23a7ab', 247 | friend: undefined, 248 | first: 'Foo', 249 | last: 'Bar' 250 | } 251 | } 252 | } 253 | 254 | The type of the object is automatically inferred. 255 | */ 256 | 257 | user.address = { street: '123 west somewhere', town: 'Wiggletown' }; 258 | /* 259 | 260 | generates: 261 | dispatch({ 262 | type: 'SET_USER_ADDRESS', 263 | prop: true, 264 | path: [ 'user', '9b66b7d0005111e68f23a7ab', 'address' ], 265 | value: { 266 | type: '1:object', 267 | value: { street: '123 west somewhere', town: 'Wiggletown' } 268 | } 269 | }); 270 | 271 | new state: 272 | { 273 | user: { 274 | '9b66b7d0005111e68f23a7ab': { 275 | address: { 276 | type: '1:object', 277 | value: { street: '123 west somewhere', town: 'Wiggletown' } 278 | }, 279 | id: '9b66b7d0005111e68f23a7ab', 280 | friend: undefined, 281 | first: 'Foo', 282 | last: 'Bar' 283 | } 284 | } 285 | } 286 | */ 287 | let ref2 = user.address; 288 | 289 | console.log(ref1 === ref2, ref1.street, ref2.street)); //true, '123 west somewhere', '123 west somewhere'; 290 | 291 | /* 292 | 293 | A cache ensures that references to the same object of the same type are 294 | actually the same instance. Even if they wouldn't be the same instance, 295 | however, the property values would be sourced from the same store. Thus, 296 | the only way to know they are different is to compare the instances with 297 | strict equal. 298 | 299 | */ 300 | 301 | user.friend = user; 302 | 303 | /* 304 | 305 | generates: 306 | dispatch({ 307 | type: 'SET_USER_FRIEND', 308 | prop: true, 309 | path: [ 'user', '9b66b7d0005111e68f23a7ab', 'friend' ], 310 | value: '9b66b7d0005111e68f23a7ab' 311 | }); 312 | 313 | new state: 314 | { 315 | user: { 316 | '9b66b7d0005111e68f23a7ab': { 317 | address: { 318 | type: '1:object', 319 | value: { street: '123 west somewhere', town: 'Wiggletown' } 320 | }, 321 | id: '9b66b7d0005111e68f23a7ab', 322 | first: 'Foo', 323 | last: 'Bar', 324 | friend: '9b66b7d0005111e68f23a7ab' 325 | } 326 | } 327 | } 328 | */ 329 | 330 | console.log(store.rootInstance.user.keys); //[ '9b66b7d0005111e68f23a7ab' ] 331 | 332 | new User(); 333 | new User(); 334 | /* 335 | 336 | new state: 337 | { 338 | user: { 339 | '9b66b7d0005111e68f23a7ab': { 340 | address: { 341 | type: '1:object', 342 | value: { street: '123 west somewhere', town: 'Wiggletown' } 343 | }, 344 | id: '6a879770005511e68e3269d9', 345 | first: 'Foo', 346 | last: 'Bar', 347 | friend: '6a879770005511e68e3269d9' 348 | }, 349 | '6a8b6800005511e68e3269d9': { 350 | first: undefined, 351 | last: undefined, 352 | address: { type: '1:union', value: null }, 353 | friend: undefined, 354 | id: '6a8b6800005511e68e3269d9' 355 | }, 356 | '6a8b8f10005511e68e3269d9': { 357 | first: undefined, 358 | last: undefined, 359 | address: { type: '1:union', value: null }, 360 | friend: undefined, 361 | id: '6a8b8f10005511e68e3269d9' 362 | } 363 | } 364 | } 365 | 366 | */ 367 | console.log(store.rootInstance.user.keys); 368 | //[ '9b66b7d0005111e68f23a7ab', '6a8b6800005511e68e3269d9', '6a8b8f10005511e68e3269d9' ] 369 | 370 | ``` 371 | 372 | ### Work in progress 373 | 374 | There's still a lot of work to be done: 375 | - More Tests 376 | - Better documentation 377 | - Allow to set defaults 378 | - Code cleanup 379 | - Add method parameter type descriptions and automatic serialization and deserialization of arguments 380 | - Automatic Promise resolution for properties 381 | 382 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { snakeCase, pathToStr } from './utils'; 2 | import deepMerge from 'deepmerge'; 3 | 4 | const freeze = Object.freeze ? Object.freeze.bind(Object) : () => {}; 5 | 6 | export default class Store { 7 | constructor(options) { 8 | function error() { 9 | throw new Error('Schema Store has no Redux Store assigned'); 10 | } 11 | 12 | let { schema, ... newOptions } = { 13 | typeMoniker: [], 14 | skipWriteSame: false, 15 | ...options, 16 | store: this 17 | }; 18 | 19 | options = newOptions; 20 | 21 | if (options.debug) { 22 | options.freeze = true; 23 | options.validate = true; 24 | options.namedFunctions = true; 25 | } 26 | 27 | if (!schema || !schema.isType) { 28 | throw new Error('Missing schema in Store options'); 29 | } 30 | 31 | this.result = undefined; 32 | this.internalState = undefined; 33 | this.reducer = this.reducer.bind(this); 34 | this.store = { 35 | dispatch: error, 36 | getState: error 37 | }; 38 | this.options = options; 39 | this.schema = schema(options); 40 | this.maxCache = 1024; 41 | this.cache = {}; 42 | this.cachePaths = []; 43 | this.record = null; 44 | this.recordStack = []; 45 | this.traceSuspended = 0; 46 | } 47 | 48 | setVirtual(obj, actionType, instancePath, setter, value) { 49 | let action = { 50 | type: actionType, 51 | path: instancePath, 52 | arg: value 53 | }; 54 | 55 | if (this.internalState === undefined) { 56 | this.store.dispatch(action); 57 | let result = this.result; 58 | this.result = undefined; 59 | return result; 60 | } else { 61 | if (this.verifyAction && this.verifyAction.type !== actionType) return; 62 | this.verifyAction = null; 63 | return setter.apply(obj, [value]); 64 | } 65 | } 66 | 67 | invoke(obj, actionType, instancePath, method, args) { 68 | let self = this 69 | , action = { 70 | type: actionType, 71 | path: instancePath, 72 | args: args 73 | } 74 | , currentState 75 | , newState 76 | , result 77 | , arg 78 | , ix 79 | ; 80 | 81 | function argResolved(value) { 82 | args = args.slice(); 83 | args[ix] = value; 84 | return self.invoke(obj, actionType, instancePath, method, args); 85 | } 86 | 87 | if (method.autoResolve) { 88 | for (ix = 0; ix < args.length; ix++) { 89 | arg = args[ix]; 90 | if (arg && typeof arg === 'object' && typeof arg.then === 'function') { 91 | return arg.then(argResolved); 92 | } 93 | } 94 | } 95 | 96 | if (this.internalState === undefined) { 97 | this.store.dispatch(action); 98 | result = this.result; 99 | this.result = undefined; 100 | return result; 101 | } else { 102 | if (this.verifyAction && this.verifyAction.type !== actionType) return; 103 | this.verifyAction = null; 104 | if (method.reducer) { 105 | result = {}; 106 | 107 | currentState = this.get(obj._meta.storePath); 108 | 109 | newState = method.call(obj, currentState, args, result); 110 | 111 | if (currentState !== newState) { 112 | this.put(obj._meta.storePath, newState); 113 | } 114 | if ('result' in result) return result.result; 115 | return obj; 116 | } 117 | return method.apply(obj, args); 118 | } 119 | } 120 | 121 | put(path, value) { 122 | this.checkRecord(); 123 | 124 | if (this.options.skipWriteSame) { 125 | if (this.get(path) === value) return; 126 | } 127 | 128 | let action = { 129 | type: `SET_${this.propActionFromPath(path) || 'ROOT'}`, 130 | path: path, 131 | value: value 132 | }; 133 | 134 | if (this.internalState === undefined) { 135 | this.store.dispatch(action); 136 | let result = this.result; 137 | this.result = undefined; 138 | return result; 139 | } else { 140 | return this.executeAction(action); 141 | } 142 | } 143 | 144 | get(path) { 145 | this.recordRead(path); 146 | let toGo = path.slice() 147 | , current = this.getActiveState() 148 | ; 149 | while (current && toGo.length) { 150 | current = current[toGo.shift()]; 151 | } 152 | return current; 153 | } 154 | 155 | reducer(state, action) { 156 | if (state === undefined) { 157 | state = this.schema.defaultValue(); 158 | } 159 | if (!action.path) return state; 160 | this.internalState = state; 161 | try { 162 | this.result = this.executeAction(action); 163 | state = this.internalState; 164 | if (this.options.freeze) { 165 | freeze(state); 166 | } 167 | } finally { 168 | this.internalState = undefined; 169 | } 170 | return state; 171 | } 172 | 173 | getActiveState() { 174 | if (this.internalState !== undefined) { 175 | return this.internalState; 176 | } 177 | return this.store.getState(); 178 | } 179 | 180 | executeAction(action) { 181 | 182 | function updateProperty(state, path, value) { 183 | if (!path.length) return value; 184 | let name = path[0] 185 | , prop = state[name] 186 | , updated = updateProperty(prop, path.slice(1), value) 187 | , newState 188 | ; 189 | 190 | if (updated === prop) return state; 191 | 192 | if (Array.isArray(state) && /^\-?\d+$/.test(name)) { 193 | newState = state.slice(); 194 | newState[Number(name)] = updated; 195 | return newState; 196 | } else { 197 | if (Array.isArray(state)) { 198 | if (name === 'length') { 199 | newState = state.slice(); 200 | newState.length = updated; 201 | return newState; 202 | } 203 | throw new Error('Property put does not support extra properties on Arrays'); 204 | } 205 | newState = { ...state }; 206 | if (updated === undefined) { 207 | delete newState[name]; 208 | } else { 209 | newState[name] = updated; 210 | } 211 | return newState; 212 | } 213 | } 214 | 215 | let path = action.path.slice() 216 | , methodOrPropName 217 | , instance 218 | , result 219 | ; 220 | 221 | if ('value' in action && action.type === `SET_${this.propActionFromPath(path) || 'ROOT'}`) { 222 | this.checkRecord(); 223 | this.internalState = updateProperty(this.internalState, path, action.value); 224 | } else { 225 | methodOrPropName = path.pop(); 226 | try { 227 | instance = this.traversePath(path); 228 | } catch (err) { 229 | if (/_CONSTRUCTOR$/.test(action.type) && /not found in state/.test(err.message) && action.args) { 230 | this.put(action.path.slice(0, -1), {}); 231 | instance = this.traversePath(path); 232 | } else { 233 | throw err; 234 | } 235 | } 236 | 237 | this.verifyAction = action; 238 | if (action.args) { 239 | result = instance[methodOrPropName].apply(instance, action.args); 240 | } else { 241 | result = instance[methodOrPropName] = action.arg; 242 | } 243 | this.verifyAction = null; 244 | } 245 | return result; 246 | } 247 | 248 | traversePath(path) { 249 | let toGo = path.slice() 250 | , current = this.instance 251 | ; 252 | while (current && toGo.length) { 253 | current = current.get(toGo.shift()); 254 | } 255 | if (!current) { 256 | throw new Error(`Path "${path.join('.')}" not found in state.`); 257 | } 258 | return current; 259 | } 260 | 261 | unpack(type, storePath, instancePath, currentInstance, owner) { 262 | let path 263 | , cached 264 | , result 265 | , ix 266 | ; 267 | result = type.unpack(this, storePath, instancePath, currentInstance, owner); 268 | if (!result || typeof result !== 'object') return result; 269 | 270 | path = pathToStr(instancePath); 271 | cached = this.cache[path]; 272 | if (cached && !currentInstance && result._meta && result._meta.options && result._meta.options.typeMoniker.join('.') === cached._meta.options.typeMoniker.join('.')) { 273 | ix = this.cachePaths.indexOf(path); 274 | if (ix !== -1) { 275 | this.cachePaths.splice(ix, 1); 276 | } 277 | this.cachePaths.push(path); 278 | return cached; 279 | } 280 | 281 | this.cache[path] = result; 282 | this.cachePaths.push(path); 283 | if (this.cachePaths.length > this.maxCache) { 284 | delete this.cache[this.cachePaths.shift()]; 285 | } 286 | return result; 287 | } 288 | 289 | recordRead(path) { 290 | if (!this.record) return; 291 | let reg = this.record 292 | ; 293 | 294 | for (let i = 0; i < path.length; i++) { 295 | let prop = path[i]; 296 | 297 | if (!reg.children[prop]) { 298 | reg.children[prop] = { children: {} }; 299 | } 300 | reg = reg.children[prop]; 301 | } 302 | 303 | reg.check = true; 304 | } 305 | 306 | startRecord() { 307 | if (this.record && !this.traceSuspended) { 308 | this.recordStack.push(this.record); 309 | } 310 | this.record = { children: {} }; 311 | var current = this.record; 312 | if (this.options.debug) { 313 | var traceError = new Error('startRecord called without a matching stopRecord'); 314 | process.nextTick(() => { 315 | if (this.record === current) { 316 | throw traceError; 317 | } 318 | }); 319 | } 320 | } 321 | 322 | stopRecord(remove) { 323 | let result = this.record 324 | , parent = this.recordStack.pop() 325 | ; 326 | 327 | if (!remove && parent) { 328 | deepMerge(parent, result); 329 | } 330 | 331 | if (parent) { 332 | this.record = parent; 333 | } else { 334 | this.record = null; 335 | } 336 | 337 | function readState(record, state) { 338 | let result = { 339 | state, 340 | children: {}, 341 | check: record.check 342 | }; 343 | 344 | Object.keys(record.children).forEach(prop => result.children[prop] = readState(record.children[prop], state[prop])); 345 | 346 | return result; 347 | } 348 | 349 | return readState(result, this.state); 350 | } 351 | 352 | sameRecordedState(snapshot) { 353 | function compareState(snapshot, snapShotState, state) { 354 | if (state === snapShotState) return true; 355 | if (snapshot.check) { 356 | if (!state || !snapShotState || typeof state !== 'object' || typeof snapShotState !== 'object') return false; 357 | //Only compare this object itself, which only includes the names of the properties. Their values have their own trackers. 358 | for (let prop in state) if (!(prop in snapShotState)) return false; 359 | for (let prop in snapShotState) if (!(prop in state)) return false; 360 | } 361 | return Object.keys(snapshot.children).every(prop => snapShotState && state && compareState(snapshot.children[prop], snapShotState[prop], state[prop])); 362 | } 363 | 364 | return compareState(snapshot, snapshot.state, this.state); 365 | } 366 | 367 | trace(func, remove) { 368 | let result; 369 | this.startRecord(); 370 | try { 371 | func(); 372 | } finally { 373 | result = this.stopRecord(remove); 374 | } 375 | return result; 376 | } 377 | 378 | suspendTrace(func) { 379 | this.traceSuspended++; 380 | try { 381 | func(); 382 | } finally { 383 | this.traceSuspended--; 384 | } 385 | } 386 | 387 | checkRecord() { 388 | if (this.record) { 389 | throw new Error('Cannot write state while in read recording mode'); 390 | } 391 | } 392 | 393 | propActionFromPath(path) { 394 | return snakeCase(pathToStr(this.schema.getTypeFromPath(path)).replace(/\./g,'_')); 395 | } 396 | 397 | get instance() { 398 | if (!this._instance) { 399 | this._instance = this.unpack(this.schema, [], []); 400 | } 401 | return this._instance; 402 | } 403 | 404 | set instance(value) { 405 | this.put([], this.schema.pack(value)); 406 | } 407 | 408 | get state() { 409 | return this.getActiveState(); 410 | } 411 | 412 | set state(value) { 413 | let message = this.schema.validateData(value); 414 | if (message) throw new TypeError(`Can't assign state: ${message}`); 415 | this.put([], value); 416 | } 417 | 418 | get dispatch() { 419 | return this.store.dispatch; 420 | } 421 | 422 | set dispatch(value) { 423 | this.store.dispatch = value; 424 | } 425 | 426 | get models() { 427 | return this.instance.models; 428 | } 429 | 430 | get isStore() { 431 | return true; 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/parse/parse-object.js: -------------------------------------------------------------------------------- 1 | import Any from '../types/any'; 2 | import finalizeType from '../parse/finalize-type'; 3 | import parseType from '../parse/parse-type'; 4 | import { pathToStr } from '../utils'; 5 | import { functionIsType} from '../types/basic'; 6 | import { arrayMethods, arrayVirtuals } from './array'; 7 | import { hydratePrototype, hydrateInstance } from './hydrate'; 8 | 9 | const circularTypes = []; 10 | 11 | export default function parseObjectType(options, type, arrayType) { 12 | if (type === Object) return parseType(options, {}); 13 | if (type === Array) return parseType(options, []); 14 | 15 | if (typeof type !== 'object') throw new TypeError(`${pathToStr(options.typeMoniker)} type must be an object`); 16 | 17 | arrayType = Boolean(arrayType); 18 | 19 | let typeMoniker = options.typeMoniker 20 | , kind = arrayType ? ( type.length === 1 ? 'array' : 'tuple' ) : 'object' 21 | , self = { 22 | isType: true, 23 | name: pathToStr(typeMoniker) || arrayType ? 'array' : 'object', 24 | kind, 25 | storageKinds: [kind], 26 | options 27 | } 28 | , propNames = Object.keys(type) 29 | , properties = {} 30 | , virtuals = {} 31 | , methods = {} 32 | , meta = {} 33 | , storedKeys = [] 34 | , storedState 35 | , prototype 36 | , thisType 37 | , restType 38 | ; 39 | 40 | if (options.self) { 41 | delete self.name; 42 | self = Object.assign(options.self, self); 43 | } 44 | 45 | for (let i = 0; i< circularTypes.length; i++) { 46 | const circularType = circularTypes[i]; 47 | if (circularType.type === type) { 48 | return circularType.self; 49 | } 50 | } 51 | 52 | circularTypes.push({ 53 | type, 54 | self 55 | }); 56 | 57 | try { 58 | if (arrayType) { 59 | if (!type.length) { 60 | return parseType(options, Array); 61 | } 62 | 63 | if (type.length === 1) { 64 | restType = parseType({ ...options, parent: self, self: null, typeMoniker: typeMoniker.concat('*') }, type[0]); 65 | propNames = []; 66 | methods = { ...arrayMethods }; 67 | virtuals = { ...arrayVirtuals }; 68 | } 69 | } else { 70 | if (!propNames.length) { 71 | return parseType(options, Object); 72 | } 73 | 74 | if (propNames.indexOf('*') !== -1) { 75 | propNames.splice(propNames.indexOf('*'), 1); 76 | if (!propNames.length && type['*'] === Any) return parseType(options, Object); 77 | restType = parseType({ ...options, parent: self, self: null, typeMoniker: typeMoniker.concat('*') }, type['*']); 78 | } 79 | } 80 | 81 | propNames.forEach((prop) => { 82 | let descriptor = Object.getOwnPropertyDescriptor(type, prop); 83 | if (descriptor.get 84 | || descriptor.set 85 | || ((typeof descriptor.value === 'function' || (descriptor.value && typeof descriptor.value.method === 'function' ) ) && !functionIsType(descriptor.value)) 86 | ) { 87 | if (descriptor.value) { 88 | methods[prop] = descriptor.value; 89 | } else { 90 | let virtual = {}; 91 | if (descriptor.get) { 92 | virtual.get = descriptor.get; 93 | } 94 | if (descriptor.set) { 95 | virtual.set = descriptor.set; 96 | } 97 | virtuals[prop] = virtual; 98 | } 99 | } else { 100 | properties[prop] = parseType({ 101 | ...options, 102 | parent: self, 103 | self: null, 104 | typeMoniker: typeMoniker.concat(prop) 105 | }, type[prop]); 106 | if (properties[prop].name === 'objectid' && !meta.idKey) { 107 | meta.idKey = prop; 108 | } 109 | } 110 | }); 111 | 112 | propNames = Object.keys(properties); 113 | 114 | thisType = { 115 | isType: true, 116 | name: pathToStr(typeMoniker) || arrayType ? 'array' : 'object', 117 | kind, 118 | storageKinds: [kind], 119 | options, 120 | validateData(value, instancePath) { 121 | instancePath = instancePath || typeMoniker; 122 | const objType = arrayType ? 'array' : 'object'; 123 | if (typeof value !== 'object' || Array.isArray(value) !== arrayType) { 124 | return `Type of "${pathToStr(instancePath)}" data must be ${objType}`; 125 | } 126 | return ( 127 | propNames.reduce((message, name) => message || properties[name].validateData(value[name], instancePath.concat(name)), null) 128 | || Object.keys(value).reduce((message, name) => { 129 | if (message) return message; 130 | if (restType) { 131 | if (propNames.indexOf(name) !== -1) return null; 132 | return restType.validateData(value[name], instancePath.concat(name)); 133 | } else { 134 | if (propNames.indexOf(name) === -1) { 135 | return `Unknown data property "${pathToStr(instancePath.concat(name))}"`; 136 | } 137 | } 138 | }, null) 139 | ); 140 | }, 141 | coerceData(value, instancePath) { 142 | instancePath = instancePath || typeMoniker; 143 | if (!thisType.validateData(value, instancePath)) return value; 144 | const result = arrayType ? [] : {}; 145 | if (typeof value !== 'object') { 146 | value = {}; 147 | } 148 | propNames.forEach(name => result[name] = properties[name].coerceData(value[name], instancePath.concat(name))); 149 | if (restType) { 150 | Object.keys(value).forEach(name => { 151 | if (propNames.indexOf(name) !== -1) return; 152 | result[name] = restType.coerceData(value[name], instancePath.concat(name)); 153 | }); 154 | } 155 | return result; 156 | }, 157 | validateAssign(value, instancePath) { 158 | instancePath = instancePath || typeMoniker; 159 | 160 | if (typeof value !== 'object') { 161 | return `Type of "${pathToStr(instancePath)}" must be object`; 162 | } 163 | 164 | let isSchemaObject = value && value._meta && value._meta.type && (value._meta.type.kind === 'object' || value._meta.type.kind === 'array'); 165 | 166 | function getProp(prop) { 167 | if (isSchemaObject) { 168 | return value.get(prop); 169 | } 170 | return value[prop]; 171 | } 172 | 173 | function getKeys(value) { 174 | if (isSchemaObject) { 175 | return value.keys; 176 | } 177 | return Object.keys(value); 178 | } 179 | 180 | instancePath = instancePath || typeMoniker; 181 | return ( 182 | propNames.reduce((message, name) => message || properties[name].validateAssign(getProp(name), instancePath.concat(name)), null) 183 | || getKeys(value).reduce((message, name) => { 184 | if (message) return message; 185 | if (propNames.indexOf(name) !== -1) return null; 186 | if (restType) { 187 | return restType.validateAssign(getProp(name), instancePath.concat(name)); 188 | } else { 189 | return `Unknown property "${pathToStr(instancePath.concat(name))}"`; 190 | } 191 | }, null) 192 | ); 193 | }, 194 | pack(value) { 195 | const isSchemaObject = value && value._meta && value._meta.type && (value._meta.type.kind === 'object' || value._meta.type.kind === 'array'); 196 | 197 | function getProp(prop) { 198 | if (isSchemaObject) { 199 | return value.get(prop); 200 | } 201 | return value[prop]; 202 | } 203 | 204 | function getKeys(value) { 205 | if (isSchemaObject) { 206 | return value.keys; 207 | } 208 | return Object.keys(value); 209 | } 210 | 211 | let out = arrayType ? [] : {}; 212 | propNames.forEach(name => out[name] = properties[name].pack(getProp(name))); 213 | 214 | if (restType) { 215 | getKeys(value).forEach((name) => { 216 | if (propNames.indexOf(name) === -1) { 217 | if (arrayType) { 218 | if ((isNaN(name) || ((Number(name) % 1) !== 0) || Number(name) < 0 || String(Number(name)) !== String(name))) { 219 | throw new TypeError(`Cannot set "${pathToStr(options.typeMoniker.concat(name))}" property on array`); 220 | } 221 | } 222 | out[name] = restType.pack(getProp(name)); 223 | } 224 | }); 225 | } 226 | return out; 227 | }, 228 | unpack(store, storePath, instancePath, currentInstance, owner) { 229 | return hydrateInstance({ 230 | ...options, 231 | prototype, 232 | store, 233 | storePath, 234 | instancePath, 235 | currentInstance, 236 | meta: { owner } 237 | }); 238 | }, 239 | getTypeFromPath(path) { 240 | if (!path.length) return typeMoniker; 241 | 242 | let first = path[0] 243 | , type 244 | ; 245 | 246 | if (propNames.indexOf(first) !== -1) { 247 | type = properties[first]; 248 | } else { 249 | if (!restType) { 250 | if (this.virtuals[first] && path.length === 1) { 251 | return options.typeMoniker.concat(first); 252 | } 253 | throw new Error(`Path not found: ${pathToStr(typeMoniker.concat(path))}`); 254 | } 255 | type = restType; 256 | } 257 | return type.getTypeFromPath(path.slice(1)); 258 | }, 259 | defaultValue() { 260 | let defaultValue = arrayType ? [] : {}; 261 | Object.keys(properties).forEach(name => defaultValue[name] = properties[name].defaultValue()); 262 | return defaultValue; 263 | }, 264 | properties, 265 | restType, 266 | methods, 267 | virtuals, 268 | getPropType(name) { 269 | if (propNames.indexOf(name) !== -1) { 270 | return properties[name]; 271 | } else { 272 | if (!restType) throw new TypeError(`Unknown property ${pathToStr(typeMoniker.concat(name))}`); 273 | return restType; 274 | } 275 | }, 276 | defaultRestProp() { 277 | if (restType) return restType.defaultValue(); 278 | }, 279 | packProp(name, value) { 280 | let type; 281 | if (propNames.indexOf(name) !== -1) { 282 | type = properties[name]; 283 | } else { 284 | if (!restType) throw new TypeError(`Unknown property ${pathToStr(typeMoniker.concat(name))}`); 285 | type = restType; 286 | } 287 | return type.pack(value); 288 | } 289 | }; 290 | 291 | if (arrayType) { 292 | thisType.length = type.length; 293 | } 294 | 295 | if ('name' in self) { 296 | delete thisType.name; 297 | } 298 | Object.assign(self, finalizeType(thisType)); 299 | 300 | prototype = hydratePrototype({ 301 | ...options, 302 | type: self, 303 | typePath: typeMoniker, 304 | getter(name) { 305 | let meta = this._meta 306 | , ix = Number(name) 307 | , type 308 | ; 309 | 310 | if (arrayType) { 311 | if (isNaN(name) || ((ix % 1) !== 0) || ix < 0 || String(ix) !== String(name) || ix >= this.length) { 312 | return undefined; 313 | } 314 | } 315 | 316 | if (propNames.indexOf(name) !== -1) { 317 | type = properties[name]; 318 | } else { 319 | if (!restType) { 320 | throw new TypeError(`Unknown property ${pathToStr(typeMoniker.concat(name))}`); 321 | } 322 | type = restType; 323 | let hasKey; 324 | this._meta.store.suspendTrace(() => hasKey = this.keys.indexOf(String(name)) !== -1); 325 | if (!hasKey) return; 326 | } 327 | return meta.store.unpack(type, meta.storePath.concat(name), meta.instancePath.concat(name), null, this); 328 | }, 329 | setter(name, value) { 330 | let meta = this._meta 331 | , ix = Number(name) 332 | , newState 333 | , packed 334 | ; 335 | 336 | name = String(name); 337 | 338 | if (propNames.indexOf(name) !== -1) { 339 | type = properties[name]; 340 | } else { 341 | if (!restType) throw new TypeError(`Unknown property ${pathToStr(meta.instancePath.concat(name))}`); 342 | type = restType; 343 | } 344 | 345 | if (arrayType) { 346 | if (isNaN(name) || ((ix % 1) !== 0) || ix < 0 || String(ix) !== String(name)) { 347 | throw new TypeError(`Cannot set "${pathToStr(options.typeMoniker.concat(name))}" property on array`); 348 | } 349 | newState = meta.store.get(meta.storePath); 350 | if (ix > this.length) { 351 | newState = newState.slice(); 352 | while (ix > newState.length) { 353 | newState.push(type.defaultValue()); 354 | } 355 | newState.push(type.pack(value)); 356 | return meta.store.put(meta.storePath, newState); 357 | } 358 | } 359 | 360 | if (!arrayType && value === undefined && type === restType) { 361 | packed = undefined; 362 | } else { 363 | packed = type.pack(value); 364 | } 365 | 366 | meta.store.put(meta.storePath.concat(name), packed); 367 | }, keys() { 368 | this._meta.store.recordRead(this._meta.storePath); 369 | let state = this._meta.state; 370 | if (storedState !== state) { 371 | storedKeys = Object.keys(state); 372 | storedState = state; 373 | } 374 | return storedKeys; 375 | }, 376 | properties, 377 | methods, 378 | virtuals, 379 | meta 380 | }); 381 | 382 | self.prototype = prototype; 383 | 384 | return self; 385 | } finally { 386 | circularTypes.pop(); 387 | } 388 | } 389 | 390 | -------------------------------------------------------------------------------- /test/types/array.js: -------------------------------------------------------------------------------- 1 | import schemaStore, { type } from '../../src'; 2 | import { createStore } from 'redux'; 3 | import { expect, should } from 'chai'; 4 | import { baseTypeProperties, checkProperties } from './utils'; 5 | 6 | should(); 7 | 8 | describe('Array (plain)', () => { 9 | let schema 10 | , store 11 | , actions 12 | ; 13 | 14 | beforeEach(() => { 15 | store = schemaStore(type([]), { debug: true }, createStore); 16 | schema = store.schema; 17 | actions = []; 18 | let origDispatch = store.dispatch; 19 | store.dispatch = function(action) { 20 | actions.push(action); 21 | return origDispatch(action); 22 | }; 23 | }); 24 | 25 | context('type', () => { 26 | checkProperties(() => schema, Object.assign({}, baseTypeProperties, { 27 | name: 'array', 28 | kind: 'array', 29 | storageKinds: ['array'] 30 | })); 31 | 32 | it('should treat [] and Array as equivalent', () => { 33 | let type1 = schemaStore(type([]), {}, createStore).schema 34 | , type2 = schemaStore(type(Array), {}, createStore).schema 35 | , prop1 36 | , prop2 37 | ; 38 | 39 | for (let prop in baseTypeProperties) { 40 | if (prop === 'options') return; 41 | prop1 = type1[prop]; 42 | prop2 = type2[prop]; 43 | if (typeof prop1 === 'function') prop1 = String(prop1); 44 | if (typeof prop2 === 'function') prop2 = String(prop2); 45 | expect(prop1).to.deep.equal(prop2); 46 | } 47 | }); 48 | }); 49 | 50 | context('instance', () => { 51 | it('should allow empty array assignment', () => { 52 | store.instance = []; 53 | store.state.should.deep.equal([]); 54 | }); 55 | 56 | it('should allow correct state assignment', () => { 57 | store.state = (store.state); 58 | }); 59 | 60 | it('should disallow incorrect state assignment', () => { 61 | expect(() => store.state = 0).to.throw(TypeError); 62 | }); 63 | 64 | it('should allow single item array assignment', () => { 65 | store.instance = [1]; 66 | store.state.should.deep.equal([1]); 67 | }); 68 | 69 | it('should allow multiple complex item array assignment', () => { 70 | const test = [ 71 | { 72 | prop: { 73 | prop: 1 74 | } 75 | }, 76 | { 77 | prop1: 1, 78 | prop2: [1, 2, 3] 79 | } 80 | ]; 81 | store.instance = test; 82 | store.state.should.deep.equal(test); 83 | store.state.should.not.equal(test); 84 | }); 85 | }); 86 | 87 | context('methods', () => { 88 | context('#concat', () => { 89 | it('should return a new array', () => { 90 | store.instance.should.not.equal(store.instance.concat()); 91 | actions.should.have.length(0); 92 | }); 93 | 94 | it('should add all arguments when they are not arrays', () => { 95 | store.state = [1, 2, 3]; 96 | store.instance.concat(4, 5).should.deep.equal([1, 2, 3, 4, 5]); 97 | }); 98 | 99 | it('should concatenate all arguments when they are arrays', () => { 100 | store.state = [1, 2, 3]; 101 | store.instance.concat([4, 5], [6, 7]).should.deep.equal([1, 2, 3, 4, 5, 6, 7]); 102 | }); 103 | }); 104 | 105 | context('#copyWithin', () => { 106 | it('should return the same array instance', () => { 107 | let before = store.instance 108 | , result = store.instance.copyWithin(4, 5) 109 | ; 110 | before.should.equal(result); 111 | before.should.equal(store.instance); 112 | actions.should.deep.equal([{ type: 'COPY_WITHIN', args: [4, 5], path: ['copyWithin'] }]); 113 | }); 114 | 115 | it('should copy internal items forward', () => { 116 | store.state = [1, 2, 3, 4, 5, 6, 7]; 117 | store.instance.copyWithin(3, 0, 2); 118 | store.state.should.deep.equal([1, 2, 3, 1, 2, 6, 7]); 119 | }); 120 | 121 | it('should not copy past the end', () => { 122 | store.state = [1, 2, 3, 4, 5, 6, 7]; 123 | store.instance.copyWithin(5, 0, 3); 124 | store.state.should.deep.equal([1, 2, 3, 4, 5, 1, 2]); 125 | }); 126 | 127 | it('should count negative indices from the end', () => { 128 | store.state = [1, 2, 3, 4, 5, 6, 7]; 129 | store.instance.copyWithin(-4, -2, -1); 130 | store.state.should.deep.equal([1, 2, 3, 6, 5, 6, 7]); 131 | }); 132 | }); 133 | 134 | context('#every', () => { 135 | it('should not mutate the array', () => { 136 | let pre = store.state; 137 | store.instance.every(()=> { 138 | // 139 | }); 140 | actions.should.have.length(0); 141 | store.state.should.equal(pre); 142 | }); 143 | 144 | it('should break when the condition fails', () => { 145 | let count = 0; 146 | store.state = [true, true, false, false]; 147 | store.instance.every((item) => { 148 | count++; 149 | return item; 150 | }); 151 | count.should.equal(3); 152 | }); 153 | 154 | it('should return true when the condition is truthy for all items', () => { 155 | let count = 0; 156 | store.state = [true, 1, 'yes', {}, []]; 157 | store.instance.every((item) => { 158 | count++; 159 | return item; 160 | }).should.equal(true); 161 | count.should.equal(5); 162 | }); 163 | 164 | it('should invoke the callback with 3 arguments', () => { 165 | let count = 0; 166 | store.state = [1]; 167 | store.instance.every((first, second, third, ...rest) => { 168 | count++; 169 | first.should.equal(1); 170 | second.should.equal(0); 171 | third.should.equal(store.instance); 172 | rest.should.have.length(0); 173 | }); 174 | count.should.equal(1); 175 | }); 176 | 177 | it('should pass along the thisArg parameter', () => { 178 | let count = 0 179 | , thisArg = {} 180 | ; 181 | store.state = [1]; 182 | store.instance.every(function() { 183 | count++; 184 | this.should.equal(thisArg); 185 | }, thisArg); 186 | count.should.equal(1); 187 | }); 188 | 189 | it('should not enumerate items in the array added during processing', () => { 190 | let count = 0; 191 | store.state = [1, 2, 3, 4, 5]; 192 | store.instance.every(function() { 193 | if (count < 2) { 194 | store.instance.push(1); 195 | } 196 | count++; 197 | return true; 198 | }); 199 | count.should.equal(5); 200 | store.state.should.deep.equal([1, 2, 3, 4, 5, 1, 1]); 201 | actions.should.have.length(3); 202 | }); 203 | }); 204 | 205 | context('#fill', () => { 206 | it('should return the same array instance', () => { 207 | let before = store.instance 208 | , result = store.instance.fill(4, 5, 6) 209 | ; 210 | before.should.equal(result); 211 | before.should.equal(store.instance); 212 | actions.should.deep.equal([{ type: 'FILL', args: [4, 5, 6], path: ['fill'] }]); 213 | }); 214 | 215 | it('should fill an entire array when no indices passed', () => { 216 | store.state = [1, 2, 3, 4]; 217 | store.instance.fill(7); 218 | store.state.should.deep.equal([7, 7, 7, 7]); 219 | actions.should.have.length(2); 220 | }); 221 | 222 | it('should fill from start when a start is passed', () => { 223 | store.state = [1, 2, 3, 4]; 224 | store.instance.fill(7, 2); 225 | store.state.should.deep.equal([1, 2, 7, 7]); 226 | actions.should.have.length(2); 227 | }); 228 | 229 | it('should fill from start to end when both are passed', () => { 230 | store.state = [1, 2, 3, 4, 5, 6]; 231 | store.instance.fill(7, 2, 4); 232 | store.state.should.deep.equal([1, 2, 7, 7, 5, 6]); 233 | actions.should.have.length(2); 234 | }); 235 | 236 | it('should count negative indices from the end', () => { 237 | store.state = [1, 2, 3, 4, 5, 6]; 238 | store.instance.fill(7, -5, -3); 239 | store.state.should.deep.equal([1, 7, 7, 4, 5, 6]); 240 | actions.should.have.length(2); 241 | }); 242 | }); 243 | 244 | context('#filter', () => { 245 | it('should not mutate the array', () => { 246 | let pre = store.state; 247 | store.instance.filter(()=> { 248 | // 249 | }); 250 | actions.should.have.length(0); 251 | store.state.should.equal(pre); 252 | }); 253 | 254 | it('should not include items for which the condition fails', () => { 255 | let count = 0; 256 | store.state = [0, 2, 1, 3, 4, 5, 7, 9]; 257 | store.instance.filter((item) => { 258 | count++; 259 | return item % 2; 260 | }).should.deep.equal([1, 3, 5, 7, 9]); 261 | count.should.equal(8); 262 | }); 263 | 264 | it('should invoke the callback with 3 arguments', () => { 265 | let count = 0; 266 | store.state = [1]; 267 | store.instance.filter((first, second, third, ...rest) => { 268 | count++; 269 | first.should.equal(1); 270 | second.should.equal(0); 271 | third.should.equal(store.instance); 272 | rest.should.have.length(0); 273 | }); 274 | count.should.equal(1); 275 | }); 276 | 277 | it('should pass along the thisArg parameter', () => { 278 | let count = 0 279 | , thisArg = {} 280 | ; 281 | store.state = [1]; 282 | store.instance.filter(function() { 283 | count++; 284 | this.should.equal(thisArg); 285 | }, thisArg); 286 | count.should.equal(1); 287 | }); 288 | 289 | it('should not enumerate items in the array added during processing', () => { 290 | let count = 0; 291 | store.state = [1, 2, 3, 4, 5]; 292 | store.instance.filter(function() { 293 | if (count < 2) { 294 | store.instance.push(7); 295 | } 296 | count++; 297 | return true; 298 | }).should.deep.equal([1, 2, 3, 4, 5]); 299 | count.should.equal(5); 300 | store.state.should.deep.equal([1, 2, 3, 4, 5, 7, 7]); 301 | actions.should.have.length(3); 302 | }); 303 | }); 304 | 305 | context('#find', () => { 306 | it('should not mutate the array', () => { 307 | let pre = store.state; 308 | store.instance.find(()=> { 309 | // 310 | }); 311 | actions.should.have.length(0); 312 | store.state.should.equal(pre); 313 | }); 314 | 315 | it('should find the item for which the condition holds', () => { 316 | let count = 0; 317 | 318 | store.state = [{ v: 1 }, { v: 2 }, { v: 3 }, { v: 4 }]; 319 | store.instance.find((item) => { 320 | count++; 321 | return item.get('v') === 3; 322 | }).should.equal(store.instance.get(2)); 323 | count.should.equal(3); 324 | }); 325 | 326 | it('should invoke the callback with 3 arguments', () => { 327 | let count = 0; 328 | store.state = [1]; 329 | store.instance.find((first, second, third, ...rest) => { 330 | count++; 331 | first.should.equal(1); 332 | second.should.equal(0); 333 | third.should.equal(store.instance); 334 | rest.should.have.length(0); 335 | }); 336 | count.should.equal(1); 337 | }); 338 | 339 | it('should pass along the thisArg parameter', () => { 340 | let count = 0 341 | , thisArg = {} 342 | ; 343 | store.state = [1]; 344 | store.instance.find(function() { 345 | count++; 346 | this.should.equal(thisArg); 347 | }, thisArg); 348 | count.should.equal(1); 349 | }); 350 | 351 | it('should not enumerate items in the array added during processing', () => { 352 | let count = 0; 353 | store.state = [1, 2, 3, 4, 5]; 354 | expect(store.instance.find(function(item) { 355 | if (count < 2) { 356 | store.instance.push(7); 357 | } 358 | count++; 359 | return item === 7; 360 | })).to.be.undefined; 361 | count.should.equal(5); 362 | store.state.should.deep.equal([1, 2, 3, 4, 5, 7, 7]); 363 | actions.should.have.length(3); 364 | }); 365 | }); 366 | 367 | context('#findIndex', () => { 368 | it('should not mutate the array', () => { 369 | let pre = store.state; 370 | store.instance.findIndex(()=> { 371 | // 372 | }); 373 | actions.should.have.length(0); 374 | store.state.should.equal(pre); 375 | }); 376 | 377 | it('should find the item for which the condition holds', () => { 378 | let count = 0; 379 | 380 | store.state = [{ v: 1 }, { v: 2 }, { v: 3 }, { v: 4 }]; 381 | store.instance.findIndex((item) => { 382 | count++; 383 | return item.get('v') === 3; 384 | }).should.equal(2); 385 | count.should.equal(3); 386 | }); 387 | 388 | it('should invoke the callback with 3 arguments', () => { 389 | let count = 0; 390 | store.state = [1]; 391 | store.instance.findIndex((first, second, third, ...rest) => { 392 | count++; 393 | first.should.equal(1); 394 | second.should.equal(0); 395 | third.should.equal(store.instance); 396 | rest.should.have.length(0); 397 | }); 398 | count.should.equal(1); 399 | }); 400 | 401 | it('should pass along the thisArg parameter', () => { 402 | let count = 0 403 | , thisArg = {} 404 | ; 405 | store.state = [1]; 406 | store.instance.findIndex(function() { 407 | count++; 408 | this.should.equal(thisArg); 409 | }, thisArg); 410 | count.should.equal(1); 411 | }); 412 | 413 | it('should not enumerate items in the array added during processing', () => { 414 | let count = 0; 415 | store.state = [1, 2, 3, 4, 5]; 416 | expect(store.instance.findIndex(function(item) { 417 | if (count < 2) { 418 | store.instance.push(7); 419 | } 420 | count++; 421 | return item === 7; 422 | })).to.be.undefined; 423 | count.should.equal(5); 424 | store.state.should.deep.equal([1, 2, 3, 4, 5, 7, 7]); 425 | actions.should.have.length(3); 426 | }); 427 | }); 428 | 429 | context('#forEach', () => { 430 | it('should not mutate the array', () => { 431 | let pre = store.state; 432 | expect(store.instance.forEach(()=> { 433 | // 434 | })).to.be.undefined; 435 | actions.should.have.length(0); 436 | store.state.should.equal(pre); 437 | }); 438 | 439 | it('should call for each item', () => { 440 | let count = 0 441 | , input = [true, 1, 'yes', false, 0, ''] 442 | ; 443 | store.state = input; 444 | store.instance.forEach((item) => { 445 | count++; 446 | item.should.equal(input[count - 1]); 447 | }); 448 | count.should.equal(6); 449 | }); 450 | 451 | it('should invoke the callback with 3 arguments', () => { 452 | let count = 0; 453 | store.state = [1]; 454 | store.instance.forEach((first, second, third, ...rest) => { 455 | count++; 456 | first.should.equal(1); 457 | second.should.equal(0); 458 | third.should.equal(store.instance); 459 | rest.should.have.length(0); 460 | }); 461 | count.should.equal(1); 462 | }); 463 | 464 | it('should pass along the thisArg parameter', () => { 465 | let count = 0 466 | , thisArg = {} 467 | ; 468 | store.state = [1]; 469 | store.instance.forEach(function() { 470 | count++; 471 | this.should.equal(thisArg); 472 | }, thisArg); 473 | count.should.equal(1); 474 | }); 475 | 476 | it('should not enumerate items in the array added during processing', () => { 477 | let count = 0; 478 | store.state = [1, 2, 3, 4, 5]; 479 | store.instance.forEach(function() { 480 | if (count < 2) { 481 | store.instance.push(1); 482 | } 483 | count++; 484 | }); 485 | count.should.equal(5); 486 | store.state.should.deep.equal([1, 2, 3, 4, 5, 1, 1]); 487 | actions.should.have.length(3); 488 | }); 489 | }); 490 | 491 | context('#includes', () => { 492 | it('should not mutate the array', () => { 493 | let pre = store.state; 494 | store.instance.includes(12).should.be.false; 495 | actions.should.have.length(0); 496 | store.state.should.equal(pre); 497 | }); 498 | 499 | it('should not find a missing simple item', () => { 500 | store.state = [1, 2, 3, 4, 5]; 501 | store.instance.includes(7).should.be.false; 502 | }); 503 | 504 | it('should not find an undefined when only a null present', () => { 505 | store.state = [1, 2, null, 4, 5]; 506 | store.instance.includes(undefined).should.be.false; 507 | }); 508 | 509 | it('should not find a null when only an undefined present', () => { 510 | store.state = [1, 2, undefined, 4, 5]; 511 | store.instance.includes(null).should.be.false; 512 | }); 513 | 514 | it('should find a simple item when present', () => { 515 | store.state = [1, 2, 3, 4, 5]; 516 | store.instance.includes(4).should.be.true; 517 | }); 518 | 519 | it('should find an undefined when present', () => { 520 | store.state = [1, 2, undefined, 4, 5]; 521 | store.instance.includes(undefined).should.be.true; 522 | }); 523 | 524 | it('should find a null when present', () => { 525 | store.state = [1, 2, 3, 4, null]; 526 | store.instance.includes(null).should.be.true; 527 | }); 528 | 529 | it('should find a NaN', () => { 530 | store.state = [1, 2, 3, NaN, 5]; 531 | store.instance.includes(NaN).should.be.true; 532 | }); 533 | 534 | it('should find a complex type', () => { 535 | store.state = [{}, {}, {}, {}, {}]; 536 | let toFind = store.instance.get(3); 537 | store.instance.includes(toFind).should.be.true; 538 | }); 539 | 540 | it('should begin searching from start when start is positive', () => { 541 | store.state = [1, 2, 3]; 542 | store.instance.includes(2, 2).should.be.false; 543 | }); 544 | 545 | it('should begin searching relative to end when start is negative', () => { 546 | store.state = [1, 2, 3]; 547 | store.instance.includes(2, -1).should.be.false; 548 | }); 549 | }); 550 | 551 | context('#indexOf', () => { 552 | it('should not mutate the array', () => { 553 | let pre = store.state; 554 | store.instance.indexOf(12).should.equal(-1); 555 | actions.should.have.length(0); 556 | store.state.should.equal(pre); 557 | }); 558 | 559 | it('should not find a missing simple item', () => { 560 | store.state = [1, 2, 3, 4, 5]; 561 | store.instance.indexOf(7).should.equal(-1); 562 | }); 563 | 564 | it('should not find an undefined when only a null present', () => { 565 | store.state = [1, 2, null, 4, 5]; 566 | store.instance.indexOf(undefined).should.equal(-1); 567 | }); 568 | 569 | it('should not find a null when only an undefined present', () => { 570 | store.state = [1, 2, undefined, 4, 5]; 571 | store.instance.indexOf(null).should.equal(-1); 572 | }); 573 | 574 | it('should not find any NaN', () => { 575 | store.state = [1, 2, 3, NaN, 5]; 576 | store.instance.indexOf(NaN).should.equal(-1); 577 | }); 578 | 579 | it('should find a simple item when present', () => { 580 | store.state = [1, 2, 3, 4, 5]; 581 | store.instance.indexOf(4).should.equal(3); 582 | }); 583 | 584 | it('should find an undefined when present', () => { 585 | store.state = [1, 2, undefined, 4, 5]; 586 | store.instance.indexOf(undefined).should.equal(2); 587 | }); 588 | 589 | it('should find a null when present', () => { 590 | store.state = [1, 2, 3, 4, null]; 591 | store.instance.indexOf(null).should.equal(4); 592 | }); 593 | 594 | it('should find a complex type', () => { 595 | store.state = [{}, {}, {}, {}, {}]; 596 | let toFind = store.instance.get(3); 597 | store.instance.indexOf(toFind).should.equal(3); 598 | }); 599 | 600 | it('should begin searching from start when start is positive', () => { 601 | store.state = [1, 2, 3, 1, 2, 3]; 602 | store.instance.indexOf(2, 2).should.equal(4); 603 | }); 604 | 605 | it('should begin searching relative to end when start is negative', () => { 606 | store.state = [1, 2, 3, 1, 2, 3]; 607 | store.instance.indexOf(2, -4).should.equal(4); 608 | }); 609 | }); 610 | 611 | context('#join', () => { 612 | it('should not mutate the array', () => { 613 | let pre = store.state; 614 | store.instance.join().should.equal(''); 615 | actions.should.have.length(0); 616 | store.state.should.equal(pre); 617 | }); 618 | 619 | it('should default to \',\' separator', () => { 620 | store.state = [1, 2, 3]; 621 | store.instance.join().should.equal('1,2,3'); 622 | }); 623 | 624 | it('should accept a separator', () => { 625 | store.state = [1, 2, 3]; 626 | store.instance.join(7).should.equal('17273'); 627 | }); 628 | 629 | it('should turn null and undefined into the empty string', () => { 630 | store.state = [1, null, 2, undefined, 3]; 631 | store.instance.join().should.equal('1,,2,,3'); 632 | }); 633 | 634 | it('should allow an empty separator', () => { 635 | store.state = [1, 2, 3]; 636 | store.instance.join('').should.equal('123'); 637 | }); 638 | }); 639 | 640 | context('#lastIndexOf', () => { 641 | it('should not mutate the array', () => { 642 | let pre = store.state; 643 | store.instance.lastIndexOf(12).should.equal(-1); 644 | actions.should.have.length(0); 645 | store.state.should.equal(pre); 646 | }); 647 | 648 | it('should not find a missing simple item', () => { 649 | store.state = [1, 2, 3, 4, 5]; 650 | store.instance.lastIndexOf(7).should.equal(-1); 651 | }); 652 | 653 | it('should not find an undefined when only a null present', () => { 654 | store.state = [1, 2, null, 4, 5]; 655 | store.instance.lastIndexOf(undefined).should.equal(-1); 656 | }); 657 | 658 | it('should not find a null when only an undefined present', () => { 659 | store.state = [1, 2, undefined, 4, 5]; 660 | store.instance.lastIndexOf(null).should.equal(-1); 661 | }); 662 | 663 | it('should not find any NaN', () => { 664 | store.state = [1, 2, 3, NaN, 5]; 665 | store.instance.lastIndexOf(NaN).should.equal(-1); 666 | }); 667 | 668 | it('should find a simple item when present', () => { 669 | store.state = [1, 2, 3, 4, 5]; 670 | store.instance.lastIndexOf(4).should.equal(3); 671 | }); 672 | 673 | it('should find an undefined when present', () => { 674 | store.state = [1, 2, undefined, 4, 5]; 675 | store.instance.lastIndexOf(undefined).should.equal(2); 676 | }); 677 | 678 | it('should find a null when present', () => { 679 | store.state = [1, 2, 3, 4, null]; 680 | store.instance.lastIndexOf(null).should.equal(4); 681 | }); 682 | 683 | it('should find a complex type', () => { 684 | store.state = [{}, {}, {}, {}, {}]; 685 | let toFind = store.instance.get(3); 686 | store.instance.lastIndexOf(toFind).should.equal(3); 687 | }); 688 | 689 | it('should begin searching from start when start is positive', () => { 690 | store.state = [1, 2, 3, 1, 2, 3]; 691 | store.instance.lastIndexOf(2, 3).should.equal(1); 692 | }); 693 | 694 | it('should begin searching relative to end when start is negative', () => { 695 | store.state = [1, 2, 3, 1, 2, 3]; 696 | store.instance.lastIndexOf(2, -4).should.equal(1); 697 | }); 698 | }); 699 | 700 | context('#map', () => { 701 | it('should not mutate the array', () => { 702 | let pre = store.state; 703 | expect(store.instance.map(()=> { 704 | // 705 | })).to.deep.equal([]); 706 | actions.should.have.length(0); 707 | store.state.should.equal(pre); 708 | }); 709 | 710 | it('should map each item', () => { 711 | let input = [true, 1, 'yes', false, 0, ''] 712 | , mapper = (item, ix) => { 713 | item.should.equal(input[ix]); 714 | return { v: item }; 715 | } 716 | , mapped = input.map(mapper) 717 | ; 718 | store.state = input; 719 | store.instance.map(mapper).should.deep.equal(mapped); 720 | }); 721 | 722 | it('should invoke the callback with 3 arguments', () => { 723 | let count = 0; 724 | store.state = [1]; 725 | store.instance.map((first, second, third, ...rest) => { 726 | count++; 727 | first.should.equal(1); 728 | second.should.equal(0); 729 | third.should.equal(store.instance); 730 | rest.should.have.length(0); 731 | }); 732 | count.should.equal(1); 733 | }); 734 | 735 | it('should pass along the thisArg parameter', () => { 736 | let count = 0 737 | , thisArg = {} 738 | ; 739 | store.state = [1]; 740 | store.instance.map(function() { 741 | count++; 742 | this.should.equal(thisArg); 743 | }, thisArg); 744 | count.should.equal(1); 745 | }); 746 | 747 | it('should not enumerate items in the array added during processing', () => { 748 | let count = 0; 749 | store.state = [1, 2, 3, 4, 5]; 750 | store.instance.map(function() { 751 | if (count < 2) { 752 | store.instance.push(1); 753 | } 754 | count++; 755 | }); 756 | count.should.equal(5); 757 | store.state.should.deep.equal([1, 2, 3, 4, 5, 1, 1]); 758 | actions.should.have.length(3); 759 | }); 760 | }); 761 | 762 | context('#pop', () => { 763 | it('should mutate the array', () => { 764 | store.state = [1, 2, 3]; 765 | let pre = store.state; 766 | store.instance.pop().should.equal(3); 767 | actions.should.have.length(2); 768 | store.state.should.not.equal(pre); 769 | }); 770 | 771 | it('should return the last element and modify the original array', () => { 772 | store.state = [17, 18, 4, 7]; 773 | store.instance.pop().should.equal(7); 774 | store.instance.should.have.length(3); 775 | store.instance.pop().should.equal(4); 776 | store.instance.should.have.length(2); 777 | store.state.should.deep.equal([17, 18]); 778 | }); 779 | 780 | it('should return undefined when there are no elements', () => { 781 | store.state = []; 782 | expect(store.instance.pop()).to.be.undefined; 783 | store.state.should.deep.equal([]); 784 | }); 785 | }); 786 | 787 | context('#push', () => { 788 | it('should mutate the array', () => { 789 | let pre = store.state; 790 | store.instance.push(1).should.equal(1); 791 | actions.should.have.length(1); 792 | store.state.should.not.equal(pre); 793 | }); 794 | 795 | it('should add all passed elements to the array', () => { 796 | store.state = ['a', 'b']; 797 | store.instance.push(3, 4, 5, 6); 798 | store.state.should.deep.equal(['a', 'b', 3, 4, 5, 6]); 799 | }); 800 | 801 | it('should not flatten passed arrays', () => { 802 | store.state = ['a', 'b']; 803 | store.instance.push([3, 4], [5, 6]); 804 | store.state.should.deep.equal(['a', 'b', [3, 4], [5, 6]]); 805 | }); 806 | 807 | it('should return the new length of the array', () => { 808 | store.state = ['a', 'b']; 809 | store.instance.push(3, 4, 5, 6).should.equal(6); 810 | }); 811 | }); 812 | 813 | context('#reduce', () => { 814 | it('should not mutate the array', () => { 815 | let pre = store.state; 816 | expect(store.instance.reduce(()=> { 817 | // 818 | }, 0)).to.equal(0); 819 | actions.should.have.length(0); 820 | store.state.should.equal(pre); 821 | }); 822 | 823 | it('should reduce each item without initial value', () => { 824 | store.state = [2, 5, 3, 6, 9]; 825 | store.instance.reduce((prev, cur) => prev * 10 + cur).should.equal(25369); 826 | }); 827 | 828 | it('should reduce each item with initial value', () => { 829 | store.state = [2, 5, 3, 6, 9]; 830 | store.instance.reduce((prev, cur) => prev * 10 + cur, 77).should.equal(7725369); 831 | }); 832 | 833 | it('should invoke the callback with 4 arguments', () => { 834 | let count = 0; 835 | store.state = [1]; 836 | store.instance.reduce((first, second, third, fourth, ...rest) => { 837 | count++; 838 | first.should.equal(7); 839 | second.should.equal(1); 840 | third.should.equal(0); 841 | fourth.should.equal(store.instance); 842 | rest.should.have.length(0); 843 | }, 7); 844 | count.should.equal(1); 845 | }); 846 | 847 | it('should not enumerate items in the array added during processing', () => { 848 | let count = 0; 849 | store.state = [0, 0, 0, 0, 0]; 850 | store.instance.reduce(function(prev, cur) { 851 | if (count < 2) { 852 | store.instance.push(1); 853 | } 854 | count++; 855 | return prev + cur; 856 | }).should.equal(0); 857 | count.should.equal(4); 858 | store.state.should.deep.equal([0, 0, 0, 0, 0, 1, 1]); 859 | actions.should.have.length(3); 860 | }); 861 | }); 862 | 863 | context('#reduceRight', () => { 864 | it('should not mutate the array', () => { 865 | let pre = store.state; 866 | expect(store.instance.reduceRight(()=> { 867 | // 868 | }, 0)).to.equal(0); 869 | actions.should.have.length(0); 870 | store.state.should.equal(pre); 871 | }); 872 | 873 | it('should reduce each item without initial value', () => { 874 | store.state = [2, 5, 3, 6, 9]; 875 | store.instance.reduceRight((prev, cur) => prev * 10 + cur).should.equal(96352); 876 | }); 877 | 878 | it('should reduce each item with initial value', () => { 879 | store.state = [2, 5, 3, 6, 9]; 880 | store.instance.reduceRight((prev, cur) => prev * 10 + cur, 77).should.equal(7796352); 881 | }); 882 | 883 | it('should invoke the callback with 4 arguments', () => { 884 | let count = 0; 885 | store.state = [1]; 886 | store.instance.reduceRight((first, second, third, fourth, ...rest) => { 887 | count++; 888 | first.should.equal(7); 889 | second.should.equal(1); 890 | third.should.equal(0); 891 | fourth.should.equal(store.instance); 892 | rest.should.have.length(0); 893 | }, 7); 894 | count.should.equal(1); 895 | }); 896 | 897 | it('should not enumerate items in the array added during processing', () => { 898 | let count = 0; 899 | store.state = [0, 0, 0, 0, 0]; 900 | store.instance.reduceRight(function(prev, cur) { 901 | if (count < 2) { 902 | store.instance.push(1); 903 | } 904 | count++; 905 | return prev + cur; 906 | }).should.equal(0); 907 | count.should.equal(4); 908 | store.state.should.deep.equal([0, 0, 0, 0, 0, 1, 1]); 909 | actions.should.have.length(3); 910 | }); 911 | }); 912 | 913 | context('#reverse', () => { 914 | it('should not mutate the array if it has no items, but should dispatch', () => { 915 | let pre = store.state; 916 | store.instance.reverse(); 917 | actions.should.have.length(1); 918 | store.state.should.equal(pre); 919 | }); 920 | 921 | it('should mutate the array if it has items', () => { 922 | store.state = [1, 2, 3]; 923 | let pre = store.state; 924 | store.instance.reverse(); 925 | actions.should.have.length(2); 926 | store.state.should.not.equal(pre); 927 | }); 928 | 929 | it('should reverse the array in place and return the array', () => { 930 | store.state = [1, 2, 3, 8, 7]; 931 | store.instance.reverse().should.equal(store.instance); 932 | store.state.should.deep.equal([7, 8, 3, 2, 1]); 933 | }); 934 | }); 935 | 936 | context('#shift', () => { 937 | it('should mutate the array', () => { 938 | store.state = [1, 2, 3]; 939 | let pre = store.state; 940 | store.instance.shift().should.equal(1); 941 | actions.should.have.length(2); 942 | store.state.should.not.equal(pre); 943 | }); 944 | 945 | it('should return the last element and modify the original array', () => { 946 | store.state = [17, 18, 4, 7]; 947 | store.instance.shift().should.equal(17); 948 | store.instance.should.have.length(3); 949 | store.instance.shift().should.equal(18); 950 | store.instance.should.have.length(2); 951 | store.state.should.deep.equal([4, 7]); 952 | }); 953 | 954 | it('should return undefined when there are no elements', () => { 955 | store.state = []; 956 | expect(store.instance.shift()).to.be.undefined; 957 | store.state.should.deep.equal([]); 958 | }); 959 | }); 960 | 961 | context('#slice', () => { 962 | it('should not mutate the array', () => { 963 | store.state = [1, 2, 3]; 964 | let pre = store.state; 965 | store.instance.slice(); 966 | actions.should.have.length(1); 967 | store.state.should.equal(pre); 968 | }); 969 | 970 | it('should return a shallow copy of the array with no params', () => { 971 | store.state = [1, 2, 3]; 972 | store.instance.slice().should.deep.equal([1, 2, 3]); 973 | }); 974 | 975 | it('should return a slice from start to the end of the array when 1 param passed', () => { 976 | store.state = [1, 2, 3, 4, 5, 6]; 977 | store.instance.slice(2).should.deep.equal([3, 4, 5, 6]); 978 | }); 979 | 980 | it('should return a slice from start to end with 2 params', () => { 981 | store.state = [1, 2, 3, 4, 5, 6]; 982 | store.instance.slice(2, 5).should.deep.equal([3, 4, 5]); 983 | }); 984 | 985 | it('should accept negative parameters to mean from the end', () => { 986 | store.state = [1, 2, 3, 4, 5, 6]; 987 | store.instance.slice(-4, -2).should.deep.equal([3, 4]); 988 | }); 989 | 990 | it('should return references to objects', () => { 991 | store.state = [{}, {}, {}, {}, {}, {}]; 992 | store.instance.slice(1, 4)[1].should.equal(store.instance.get(2)); 993 | }); 994 | }); 995 | 996 | context('#some', () => { 997 | it('should not mutate the array', () => { 998 | let pre = store.state; 999 | store.instance.some(()=> { 1000 | // 1001 | }); 1002 | actions.should.have.length(0); 1003 | store.state.should.equal(pre); 1004 | }); 1005 | 1006 | it('should break when the condition passes', () => { 1007 | let count = 0; 1008 | store.state = [false, true, false, false]; 1009 | store.instance.some((item) => { 1010 | count++; 1011 | return item; 1012 | }); 1013 | count.should.equal(2); 1014 | }); 1015 | 1016 | it('should return false when the condition is falsy for all items', () => { 1017 | let count = 0; 1018 | store.state = [false, 0, '', null, undefined]; 1019 | store.instance.some((item) => { 1020 | count++; 1021 | return item; 1022 | }).should.equal(false); 1023 | count.should.equal(5); 1024 | }); 1025 | 1026 | it('should invoke the callback with 3 arguments', () => { 1027 | let count = 0; 1028 | store.state = [1]; 1029 | store.instance.some((first, second, third, ...rest) => { 1030 | count++; 1031 | first.should.equal(1); 1032 | second.should.equal(0); 1033 | third.should.equal(store.instance); 1034 | rest.should.have.length(0); 1035 | }); 1036 | count.should.equal(1); 1037 | }); 1038 | 1039 | it('should pass along the thisArg parameter', () => { 1040 | let count = 0 1041 | , thisArg = {} 1042 | ; 1043 | store.state = [1]; 1044 | store.instance.some(function() { 1045 | count++; 1046 | this.should.equal(thisArg); 1047 | }, thisArg); 1048 | count.should.equal(1); 1049 | }); 1050 | 1051 | it('should not enumerate items in the array added during processing', () => { 1052 | let count = 0; 1053 | store.state = [1, 2, 3, 4, 5]; 1054 | store.instance.some(function() { 1055 | if (count < 2) { 1056 | store.instance.push(1); 1057 | } 1058 | count++; 1059 | return false; 1060 | }); 1061 | count.should.equal(5); 1062 | store.state.should.deep.equal([1, 2, 3, 4, 5, 1, 1]); 1063 | actions.should.have.length(3); 1064 | }); 1065 | }); 1066 | 1067 | context('#sort', () => { 1068 | it('should mutate the array', () => { 1069 | store.state = [7, 2, 4]; 1070 | let pre = store.state; 1071 | store.instance.sort(); 1072 | actions.should.have.length(2); 1073 | store.state.should.not.equal(pre); 1074 | }); 1075 | 1076 | it('should by default put undefined at the end', () => { 1077 | store.state = [7, 2, undefined, 'z', 4]; 1078 | store.instance.sort(); 1079 | store.state.should.deep.equal([2, 4, 7, 'z', undefined]); 1080 | }); 1081 | 1082 | it('should by default sort in string order', () => { 1083 | store.state = [7, null, 'm', 2, undefined, 'z', 4]; 1084 | store.instance.sort(); 1085 | store.state.should.deep.equal([2, 4, 7, 'm', null, 'z', undefined]); 1086 | }); 1087 | 1088 | it('should sort using a custom compare function', () => { 1089 | store.state = [7, null, 'm', 2, undefined, 'z', 4]; 1090 | store.instance.sort((a, b) => { 1091 | a = String(a).split('').reverse().join(''); 1092 | b = String(b).split('').reverse().join(''); 1093 | if (a < b) return 1; 1094 | if (a > b) return -1; 1095 | return 0; 1096 | }); 1097 | store.state.should.deep.equal(['z', 'm', null, 7, 4, 2, undefined]); 1098 | }); 1099 | }); 1100 | 1101 | context('#splice', () => { 1102 | it('should mutate the array', () => { 1103 | store.state = [7, 2, 4]; 1104 | let pre = store.state; 1105 | store.instance.splice(1, 1, 8); 1106 | actions.should.have.length(2); 1107 | store.state.should.not.equal(pre); 1108 | }); 1109 | 1110 | it('should delete items using a positive start', () => { 1111 | store.state = [7, 2, 4, 1, 5, 3]; 1112 | store.instance.splice(1, 2); 1113 | store.state.should.deep.equal([7, 1, 5, 3]); 1114 | }); 1115 | 1116 | it('should delete items offset from the end using a negative start', () => { 1117 | store.state = [7, 2, 4, 1, 5, 3]; 1118 | store.instance.splice(-4, 2); 1119 | store.state.should.deep.equal([7, 2, 5, 3]); 1120 | }); 1121 | 1122 | it('should insert items using a positive start', () => { 1123 | store.state = [7, 2, 4, 1, 5, 3]; 1124 | store.instance.splice(3, 0, 8, 9, 10); 1125 | store.state.should.deep.equal([7, 2, 4, 8, 9, 10, 1, 5, 3]); 1126 | }); 1127 | 1128 | it('should insert items offset from the end using a negative start', () => { 1129 | store.state = [7, 2, 4, 1, 5, 3]; 1130 | store.instance.splice(-2, 0, 8, 9, 10); 1131 | store.state.should.deep.equal([7, 2, 4, 1, 8, 9, 10, 5, 3]); 1132 | }); 1133 | 1134 | it('should not flatten arrays when inserting', () => { 1135 | store.state = [7, 2, 4, 1, 5, 3]; 1136 | store.instance.splice(3, 0, [8, 9], [10]); 1137 | store.state.should.deep.equal([7, 2, 4, [8, 9], [10], 1, 5, 3]); 1138 | }); 1139 | 1140 | it('should delete and add items in the same call', () => { 1141 | store.state = [7, 2, 4, 1, 5, 3]; 1142 | store.instance.splice(3, 2, 8, 9, 10); 1143 | store.state.should.deep.equal([7, 2, 4, 8, 9, 10, 3]); 1144 | }); 1145 | 1146 | it('should not save items directly to the state', () => { 1147 | let obj = { v: 1 }; 1148 | store.state = [7, 2, 4, 1, 5, 3]; 1149 | store.instance.splice(3, 0, obj); 1150 | store.state.should.deep.equal([7, 2, 4, obj, 1, 5, 3]); 1151 | store.state[3].should.deep.equal(obj); 1152 | store.state[3].should.not.equal(obj); 1153 | }); 1154 | }); 1155 | 1156 | context('#toLocaleString', () => { 1157 | it('should not mutate the array', () => { 1158 | let pre = store.state; 1159 | store.instance.toLocaleString(); 1160 | actions.should.have.length(0); 1161 | store.state.should.equal(pre); 1162 | }); 1163 | 1164 | it('should return the locale string version of the array', () => { 1165 | let input = [1337.1337, {}, [], 'Hello world', true]; 1166 | store.state = input; 1167 | store.instance.toLocaleString().should.equal(input.toLocaleString()); 1168 | }); 1169 | }); 1170 | 1171 | context('#toString', () => { 1172 | it('should not mutate the array', () => { 1173 | let pre = store.state; 1174 | store.instance.toString(); 1175 | actions.should.have.length(0); 1176 | store.state.should.equal(pre); 1177 | }); 1178 | 1179 | it('should return the stringified version of the array', () => { 1180 | let input = [1337.1337, {}, [], 'Hello world', true]; 1181 | store.state = input; 1182 | store.instance.toString().should.equal(input.toString()); 1183 | }); 1184 | }); 1185 | 1186 | context('#unshift', () => { 1187 | it('should mutate the array', () => { 1188 | let pre = store.state; 1189 | store.instance.unshift(1).should.equal(1); 1190 | actions.should.have.length(1); 1191 | store.state.should.not.equal(pre); 1192 | }); 1193 | 1194 | it('should add all passed elements to the array', () => { 1195 | store.state = ['a', 'b']; 1196 | store.instance.unshift(3, 4, 5, 6); 1197 | store.state.should.deep.equal([3, 4, 5, 6, 'a', 'b']); 1198 | }); 1199 | 1200 | it('should not flatten passed arrays', () => { 1201 | store.state = ['a', 'b']; 1202 | store.instance.unshift([3, 4], [5, 6]); 1203 | store.state.should.deep.equal([[3, 4], [5, 6], 'a', 'b']); 1204 | }); 1205 | 1206 | it('should return the new length of the array', () => { 1207 | store.state = ['a', 'b']; 1208 | store.instance.unshift(3, 4, 5, 6).should.equal(6); 1209 | }); 1210 | }); 1211 | 1212 | context('#valueOf', () => { 1213 | it('should not mutate the array', () => { 1214 | let pre = store.state; 1215 | store.instance.valueOf(); 1216 | actions.should.have.length(0); 1217 | store.state.should.equal(pre); 1218 | }); 1219 | 1220 | it('should return the stringified version of the array', () => { 1221 | let input = [1337.1337, {}, [], 'Hello world', true]; 1222 | store.state = input; 1223 | store.instance.valueOf().should.deep.equal(input.valueOf()); 1224 | }); 1225 | }); 1226 | }); 1227 | 1228 | context('properties', () => { 1229 | context('.length', () => { 1230 | it('should retrieve the length of the array', () => { 1231 | store.state = [1, 2, 3]; 1232 | store.instance.should.have.length(3); 1233 | store.state = [1, 2, 3, 4, 5, 6, 7]; 1234 | store.instance.should.have.length(7); 1235 | }); 1236 | 1237 | it('should get length through get function', () => { 1238 | store.state = [1, 2, 3]; 1239 | store.instance.get('length').should.equal(3); 1240 | }); 1241 | 1242 | it('should mutate the array when set', () => { 1243 | store.state = [1, 2, 3]; 1244 | let pre = store.state; 1245 | store.instance.length = 3; 1246 | store.state.should.not.equal(pre); 1247 | actions.should.have.length(2); 1248 | }); 1249 | 1250 | it('should set length through set function', () => { 1251 | store.state = [1, 2, 3]; 1252 | store.instance.set('length', 2); 1253 | store.state.should.deep.equal([1, 2]); 1254 | }); 1255 | 1256 | it('should trim the array when set to a lower value', () => { 1257 | store.state = [1, 2, 3]; 1258 | store.instance.length = 2; 1259 | store.state.should.deep.equal([1, 2]); 1260 | store.state = [1, 2, 3, 4, 5, 6, 7]; 1261 | store.instance.length = 5; 1262 | store.state.should.deep.equal([1, 2, 3, 4, 5]); 1263 | }); 1264 | 1265 | it('should extend the array when set to a higher value', () => { 1266 | store.state = [1, 2, 3]; 1267 | store.instance.length = 5; 1268 | store.state.should.deep.equal([1, 2, 3, undefined, undefined]); 1269 | store.state = [1, 2, 3, 4, 5, 6, 7]; 1270 | store.instance.length = 11; 1271 | store.state.should.deep.equal([1, 2, 3, 4, 5, 6, 7, undefined, undefined, undefined, undefined]); 1272 | }); 1273 | }); 1274 | 1275 | context('#get', () => { 1276 | it('should not mutate the array', () => { 1277 | let pre = store.state; 1278 | store.instance.get(0); 1279 | actions.should.have.length(0); 1280 | store.state.should.equal(pre); 1281 | }); 1282 | 1283 | it('should get elements from the array', () => { 1284 | store.state = [1, 2, 3, 4]; 1285 | store.instance.get(2).should.equal(3); 1286 | }); 1287 | 1288 | it('should return undefined for items that don\'t exist', () => { 1289 | store.state = [1, 2, 3, 4]; 1290 | expect(store.instance.get(7)).to.be.undefined; 1291 | expect(store.instance.get(-10)).to.be.undefined; 1292 | expect(store.instance.get()).to.be.undefined; 1293 | expect(store.instance.get('missing')).to.be.undefined; 1294 | }); 1295 | }); 1296 | 1297 | context('#set', () => { 1298 | it('should mutate the array', () => { 1299 | let pre = store.state; 1300 | store.instance.set(0, 3); 1301 | actions.should.have.length(1); 1302 | store.state.should.not.equal(pre); 1303 | }); 1304 | 1305 | it('should set elements in the array', () => { 1306 | store.state = [1, 2, 3, 4]; 1307 | store.instance.set(2, 8); 1308 | store.state.should.deep.equal([1, 2, 8, 4]); 1309 | }); 1310 | 1311 | it('should extend the array when setting at high index', () => { 1312 | store.state = [1, 2, 3, 4]; 1313 | store.instance.set(7, 8); 1314 | store.state.should.deep.equal([1, 2, 3, 4, undefined, undefined, undefined, 8]); 1315 | }); 1316 | 1317 | it('should fail for non-natural number properties', () => { 1318 | store.state = [1, 2, 3, 4]; 1319 | expect(()=>store.instance.set(-10, 5)).to.throw(TypeError); 1320 | expect(()=>store.instance.set(undefined, 5)).to.throw(TypeError); 1321 | expect(()=>store.instance.set('missing', 5)).to.throw(TypeError); 1322 | }); 1323 | }); 1324 | }); 1325 | }); 1326 | --------------------------------------------------------------------------------