Skip to content

Commit 2103fde

Browse files
Merge pull request #35 from scup/hooks
After Hooks
2 parents a4f3c8e + d548706 commit 2103fde

File tree

8 files changed

+179
-57
lines changed

8 files changed

+179
-57
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ cache:
55
npm: true
66
directories:
77
- node_modules/
8-
script: npm run coverage
8+
script: npm run validate
99
after_script:
1010
- git config credential.helper "store --file=.git/credentials"
1111
- echo "https://${GH_TOKEN}:@github.com" > .git/credentials
1212
- git config --global user.email "[email protected]"
1313
- git config --global user.name "Travis CI"
1414
- rm -rf lib
15-
- npm run build
15+
- npm run prepublish
1616
- bin/commit_lib_files.sh
1717
env:
1818
global:

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,34 @@ To understand the validators [React PropTypes](https://facebook.github.io/react/
200200
console.log(entity.errors.requiredProp1); // undefined
201201
console.log(contextValidated.requiredProp1); // { errors: [Error: Error -1] }
202202
```
203+
204+
### Hooks
205+
```javascript
206+
class EntityWithHook extends Speck {
207+
static SCHEMA = {
208+
fieldWithHook: {
209+
validator: joiAdapter(Joi.number()),
210+
hooks: {
211+
afterSet(data, fieldName) {
212+
// data is the whole data of the instance
213+
// fieldName the current fieldName
214+
// DO WHATEVER YOU WANT
215+
}
216+
}
217+
},
218+
anotherFieldWithHook: {
219+
validator: joiAdapter(Joi.number()),
220+
hooks: {
221+
afterSet(data, fieldName) {
222+
return { anotherField: data[fieldName] * 2 } // if the afterSet hook returns an object is merged to data
223+
}
224+
}
225+
},
226+
anotherField: joiAdapter(Joi.number()),
227+
}
228+
}
229+
230+
const myEntity = new EntityWithHook({ fieldWithHook: 'foo', anotherFieldWithHook: 'bar', anotherField: null });
231+
232+
myEntity.anotherFieldWithHook = 10 //according to the after set hook anotherField newValue will be 20
233+
```

lib/Speck.js

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
'use strict';
22

3+
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
4+
35
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
46

5-
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
7+
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
68

79
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
810

@@ -12,26 +14,41 @@ var _require = require('lodash'),
1214
clone = _require.clone,
1315
get = _require.get,
1416
isNil = _require.isNil,
15-
isFunction = _require.isFunction;
17+
isFunction = _require.isFunction,
18+
noop = _require.noop;
19+
20+
function applyAfterSetHook(field, data, afterSetHook) {
21+
var newData = afterSetHook(data, field);
22+
(typeof newData === 'undefined' ? 'undefined' : _typeof(newData)) === 'object' && _extends(data, newData);
23+
}
24+
25+
function createGetterAndSetter(instance, field) {
26+
var schemaField = instance.schema[field];
27+
28+
var afterSet = get(schemaField, 'hooks.afterSet');
29+
var afterSetHook = typeof afterSet === 'function' ? applyAfterSetHook : noop;
1630

17-
var createGetterAndSetter = function createGetterAndSetter(instance, field) {
1831
return {
19-
set: function set(value) {
20-
if (instance.data[field] !== value) {
21-
instance.data[field] = value;
32+
set: function set(newValue) {
33+
if (instance.data[field] !== newValue) {
34+
instance.data[field] = newValue;
35+
36+
afterSetHook(field, instance.data, afterSet);
37+
2238
return instance._validate();
2339
}
2440
},
2541
get: function get() {
2642
return instance.data[field];
2743
},
44+
2845
enumerable: true
2946
};
30-
};
47+
}
3148

3249
var Speck = function () {
33-
function Speck(data) {
34-
var _this = this;
50+
function Speck() {
51+
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
3552

3653
_classCallCheck(this, Speck);
3754

@@ -46,22 +63,24 @@ var Speck = function () {
4663
});
4764

4865
Object.defineProperty(this, 'childrenEntities', {
49-
value: Object.keys(this.constructor.SCHEMA).filter(function (field) {
50-
return !!_this.constructor.SCHEMA[field].type;
51-
}),
66+
value: Object.keys(this.constructor.SCHEMA).filter(this.__fieldHasType.bind(this)),
5267
enumerable: false
5368
});
5469

55-
this.errors = {};
5670
Object.defineProperty(this, 'data', {
57-
value: this._mergeDefault(data || {}),
71+
value: this._mergeDefault(data),
5872
enumerable: false
5973
});
6074

6175
this._validate();
6276
}
6377

6478
_createClass(Speck, [{
79+
key: '__fieldHasType',
80+
value: function __fieldHasType(field) {
81+
return !!this.constructor.SCHEMA[field].type;
82+
}
83+
}, {
6584
key: '__initFieldValue',
6685
value: function __initFieldValue(field, data) {
6786
var hasValue = !isNil(data[field]);
@@ -107,20 +126,19 @@ var Speck = function () {
107126
value: function __validateField(field) {
108127
var validator = typeof this.schema[field] === 'function' ? this.schema[field] : this.schema[field].validator;
109128

110-
var error = validator(this.data, field, this.constructor.name + 'Entity');
111-
112-
if (error) {
113-
this.errors[field] = { errors: [error.message || error] };
114-
}
129+
return validator(this.data, field, this.constructor.name + 'Entity');
115130
}
116131
}, {
117132
key: '_validate',
118133
value: function _validate() {
119134
this.errors = {};
120135

121-
var field = void 0;
122-
for (field in this.schema) {
123-
this.__validateField(field);
136+
for (var field in this.schema) {
137+
var error = this.__validateField(field);
138+
139+
if (error) {
140+
this.errors[field] = { errors: [error.message || error] };
141+
}
124142
}
125143
this.valid = Object.keys(this.errors).length === 0;
126144

@@ -169,10 +187,10 @@ var Speck = function () {
169187
}, {
170188
key: 'toJSON',
171189
value: function toJSON() {
172-
var _this2 = this;
190+
var _this = this;
173191

174192
var rawData = Object.keys(this.data).reduce(function (data, field) {
175-
return _extends(data, _defineProperty({}, field, _this2._fetchChild(_this2.data[field])));
193+
return _extends(data, _defineProperty({}, field, _this._fetchChild(_this.data[field])));
176194
}, {});
177195

178196
return JSON.parse(JSON.stringify(rawData));
@@ -187,7 +205,7 @@ var Speck = function () {
187205
}, {
188206
key: 'validateContext',
189207
value: function validateContext(context) {
190-
var _this3 = this;
208+
var _this2 = this;
191209

192210
if (!get(this.contexts, context)) return this.errors;
193211

@@ -196,15 +214,15 @@ var Speck = function () {
196214
};
197215
if (this.contexts[context].exclude && Object.keys(this.contexts[context].exclude).length > 0) {
198216
validation = function validation(error) {
199-
return _this3.contexts[context].exclude.find(function (exclude) {
217+
return _this2.contexts[context].exclude.find(function (exclude) {
200218
return exclude === error;
201219
}) === undefined;
202220
};
203221
}
204222

205223
if (this.contexts[context].include && Object.keys(this.contexts[context].include).length > 0) {
206224
validation = function validation(error) {
207-
return _this3.contexts[context].include.find(function (include) {
225+
return _this2.contexts[context].include.find(function (include) {
208226
return include === error;
209227
}) !== undefined;
210228
};
@@ -213,8 +231,8 @@ var Speck = function () {
213231
var contextErrors = _extends({}, this.errors);
214232
if (this.contexts[context].fields) {
215233
Object.keys(this.contexts[context].fields).forEach(function (field) {
216-
var result = _this3.contexts[context].fields[field](_this3, field, _this3.constructor.name);
217-
if (result) contextErrors[field] = { errors: result };else delete contextErrors[field];
234+
var result = _this2.contexts[context].fields[field](_this2, field, _this2.constructor.name);
235+
result && (contextErrors[field] = { errors: result });
218236
});
219237
}
220238

lib/SpeckHooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"use strict";

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "speck-entity",
3-
"version": "0.1.9",
3+
"version": "0.2.0",
44
"description": "Domain entities with reactive validation",
55
"main": "lib/index.js",
66
"scripts": {
@@ -9,8 +9,8 @@
99
"coverage": "nyc npm test",
1010
"lint": "standard -v \"src/**/*.js\" \"spec/**/*.js\"",
1111
"lint:fix": "standard -v --fix \"src/**/*.js\" \"spec/**/*.js\"",
12-
"build": "babel src --out-dir lib",
13-
"prepublish": "npm run build"
12+
"prepublish": "babel src --out-dir lib",
13+
"validate": "npm run lint && npm run coverage"
1414
},
1515
"nyc": {
1616
"branches": 95,

spec/SpeckHooks.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const Speck = require('Speck')
2+
const { noop } = require('lodash')
3+
const { expect } = require('chai')
4+
const { mock, assert } = require('sinon')
5+
6+
describe('SpeckHooks', function () {
7+
beforeEach(function () {
8+
class MyEntitiesWithHooks extends Speck {}
9+
const afterSetMock = mock('afterSetHook')
10+
11+
MyEntitiesWithHooks.SCHEMA = {
12+
anyField: noop,
13+
fieldWithHook: {
14+
validator: noop,
15+
hooks: {
16+
afterSet: afterSetMock
17+
}
18+
}
19+
}
20+
21+
this.MyEntitiesWithHooks = MyEntitiesWithHooks
22+
this.afterSetMock = afterSetMock
23+
})
24+
25+
it('calls the hook afterSet after set happens', function () {
26+
const data = { fieldWithHook: 'bla', anyField: 'foo' }
27+
const instance = new this.MyEntitiesWithHooks(data)
28+
instance.fieldWithHook = 'newValue'
29+
30+
assert.calledWithExactly(this.afterSetMock, { fieldWithHook: 'newValue', anyField: 'foo' }, 'fieldWithHook')
31+
})
32+
33+
it('assigns result of afterSet to data', function () {
34+
const data = { fieldWithHook: 'bla', anyField: 'foo' }
35+
const instance = new this.MyEntitiesWithHooks(data)
36+
37+
const newData = { fieldWithHook: 'valueChanged', anyField: 'valueChanged' }
38+
this.afterSetMock
39+
.withExactArgs({ fieldWithHook: 'newValueToAssign', anyField: 'foo' }, 'fieldWithHook')
40+
.returns(newData)
41+
42+
instance.fieldWithHook = 'newValueToAssign'
43+
44+
expect(instance.data).to.deep.equal(newData)
45+
})
46+
47+
it('does not assign result of afterSet to data when it not object', function () {
48+
const data = { fieldWithHook: 'bla', anyField: 'foo' }
49+
const instance = new this.MyEntitiesWithHooks(data)
50+
51+
this.afterSetMock
52+
.withExactArgs({ fieldWithHook: 'newValueToAssign', anyField: 'foo' }, 'fieldWithHook')
53+
.returns('not object')
54+
55+
instance.fieldWithHook = 'newValueToAssign'
56+
57+
expect(instance.data).to.deep.equal({ fieldWithHook: 'newValueToAssign', anyField: 'foo' })
58+
})
59+
})

0 commit comments

Comments
 (0)