├── 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 |
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 |
7 | {todos.all.filter(todo => filter === 'all' || (filter === 'completed') === todo.completed).map(todo =>
8 |
12 | )}
13 |
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 |
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 | [](https://travis-ci.org/ddsol/redux-schema)
4 | [](https://coveralls.io/github/ddsol/redux-schema)
5 | [](https://www.npmjs.com/package/redux-schema)
6 | [](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 |
--------------------------------------------------------------------------------