From d5e88f25532f0e1629e4e933ffd348d5a0908980 Mon Sep 17 00:00:00 2001 From: Talysson de Oliveira Cassiano Date: Tue, 27 Aug 2019 11:44:10 -0300 Subject: [PATCH] Add custom error for strict mode (#95) * Allow custom error class for strict mode * Cover custom error class with tests * Make linter happy * Bump minor * Move validation error class decision --- CHANGELOG.md | 4 ++ dist/structure.js | 85 +++++++++++++++++++++------- docs/strict-mode.md | 41 +++++++++++++- package.json | 2 +- src/attributes/decorator.js | 12 +--- src/errors/DefaultValidationError.js | 8 +++ src/{errors.js => errors/index.js} | 8 +-- src/strictMode/index.js | 20 +++++++ test/unit/instanceAndUpdate.spec.js | 55 ++++++++++++++---- 9 files changed, 184 insertions(+), 51 deletions(-) create mode 100644 src/errors/DefaultValidationError.js rename src/{errors.js => errors/index.js} (82%) create mode 100644 src/strictMode/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f99613..b322ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.6.0 - 2019-08-27 +Enhancements: +* Allow custom error class to static mode + ## 1.5.0 - 2019-07-08 Enhancements: * Add `buildStrict` static method diff --git a/dist/structure.js b/dist/structure.js index fc3e6f7..8ea6a8f 100644 --- a/dist/structure.js +++ b/dist/structure.js @@ -87,12 +87,13 @@ return /******/ (function(modules) { // webpackBootstrap var Serialization = __webpack_require__(33); var Validation = __webpack_require__(6); var Initialization = __webpack_require__(18); + var StrictMode = __webpack_require__(36); var Errors = __webpack_require__(23); var _require = __webpack_require__(15), SCHEMA = _require.SCHEMA; - var _require2 = __webpack_require__(36), + var _require2 = __webpack_require__(38), attributeDescriptorFor = _require2.attributeDescriptorFor, attributesDescriptorFor = _require2.attributesDescriptorFor; @@ -117,19 +118,7 @@ return /******/ (function(modules) { // webpackBootstrap } }); - function buildStrict(constructorArgs) { - var instance = new WrapperClass(constructorArgs); - - var _instance$validate = instance.validate(), - valid = _instance$validate.valid, - errors = _instance$validate.errors; - - if (!valid) throw Errors.invalidAttributes(errors); - - return instance; - } - - WrapperClass.buildStrict = buildStrict; + define(WrapperClass, 'buildStrict', StrictMode.buildStrictDescriptorFor(WrapperClass, schemaOptions)); if (WrapperClass[SCHEMA]) { schema = Object.assign({}, WrapperClass[SCHEMA], schema); @@ -852,12 +841,6 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - function invalidAttributes(errors) { - var error = new Error('Invalid Attributes'); - error.details = errors; - return error; - } - module.exports = { classAsSecondParam: function classAsSecondParam(ErroneousPassedClass) { return new Error('You passed the structure class as the second parameter of attributes(). The expected usage is `attributes(schema)(' + (ErroneousPassedClass.name || 'StructureClass') + ')`.'); @@ -874,7 +857,9 @@ return /******/ (function(modules) { // webpackBootstrap invalidType: function invalidType(attributeName) { return new TypeError('Attribute type must be a constructor or the name of a dynamic type: ' + attributeName + '.'); }, - invalidAttributes: invalidAttributes + invalidAttributes: function invalidAttributes(errors, StructureValidationError) { + return new StructureValidationError(errors); + } }; /***/ }, @@ -1217,6 +1202,64 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; + var Errors = __webpack_require__(23); + var DefaultValidationError = __webpack_require__(37); + + exports.buildStrictDescriptorFor = function buildStrictDescriptorFor(StructureClass, schemaOptions) { + var StructureValidationError = schemaOptions.strictValidationErrorClass || DefaultValidationError; + + return { + value: function buildStrict(constructorArgs) { + var instance = new StructureClass(constructorArgs); + + var _instance$validate = instance.validate(), + valid = _instance$validate.valid, + errors = _instance$validate.errors; + + if (!valid) { + throw Errors.invalidAttributes(errors, StructureValidationError); + } + + return instance; + } + }; + }; + +/***/ }, +/* 37 */ +/***/ function(module, exports) { + + 'use strict'; + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var DefautValidationError = function (_Error) { + _inherits(DefautValidationError, _Error); + + function DefautValidationError(errors) { + _classCallCheck(this, DefautValidationError); + + var _this = _possibleConstructorReturn(this, (DefautValidationError.__proto__ || Object.getPrototypeOf(DefautValidationError)).call(this, 'Invalid Attributes')); + + _this.details = errors; + return _this; + } + + return DefautValidationError; + }(Error); + + module.exports = DefautValidationError; + +/***/ }, +/* 38 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + var _require = __webpack_require__(9), isObject = _require.isObject; diff --git a/docs/strict-mode.md b/docs/strict-mode.md index 1193f92..2851a0b 100644 --- a/docs/strict-mode.md +++ b/docs/strict-mode.md @@ -21,4 +21,43 @@ var user = User.buildStrict({ // { message: '"name" is required', path: 'name' }, // { message: '"age" must be a number', path: 'age' } // ] -``` \ No newline at end of file +``` + +## Custom error + +Normally `buildStrict` will throw a default `Error` when attributes are invalid but you can customize the error class that will be used passing a `strictValidationErrorClass` to the _second_ parameter of the `attributes` function. + +The value of `strictValidationErrorClass` should be a class that accepts an array of erros in the constructor. + +```js +const { attributes } = require('structure'); + +class InvalidBookError extends Error { + constructor(errors) { + super('Wait, this book is not right'); + this.code = 'INVALID_BOOK'; + this.errors = errors; + } +} + +const Book = attributes({ + name: { + type: String, + required: true + }, + year: Number +}, { + strictValidationErrorClass: InvalidBookError +})(class Book {}); + +var book = Book.buildStrict({ + year: 'Twenty' +}); + +// InvalidBookError: Wait, this book is not right +// code: 'INVALID_BOOK' +// details: [ +// { message: '"name" is required', path: 'name' }, +// { message: '"year" must be a number', path: 'year' } +// ] +``` diff --git a/package.json b/package.json index 0247e2a..caaa8d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "structure", - "version": "1.5.0", + "version": "1.6.0", "description": "A simple schema/attributes library built on top of modern JavaScript", "main": "src/index.js", "browser": "dist/structure.js", diff --git a/src/attributes/decorator.js b/src/attributes/decorator.js index e060eb7..634c321 100644 --- a/src/attributes/decorator.js +++ b/src/attributes/decorator.js @@ -2,6 +2,7 @@ const Schema = require('../schema'); const Serialization = require('../serialization'); const Validation = require('../validation'); const Initialization = require('../initialization'); +const StrictMode = require('../strictMode'); const Errors = require('../errors'); const { SCHEMA } = require('../symbols'); const { @@ -28,16 +29,7 @@ function attributesDecorator(schema, schemaOptions = {}) { } }); - function buildStrict(constructorArgs){ - const instance = new WrapperClass(constructorArgs); - - const {valid, errors} = instance.validate(); - if(!valid) throw Errors.invalidAttributes(errors); - - return instance; - } - - WrapperClass.buildStrict = buildStrict; + define(WrapperClass, 'buildStrict', StrictMode.buildStrictDescriptorFor(WrapperClass, schemaOptions)); if(WrapperClass[SCHEMA]) { schema = Object.assign({}, WrapperClass[SCHEMA], schema); diff --git a/src/errors/DefaultValidationError.js b/src/errors/DefaultValidationError.js new file mode 100644 index 0000000..e49f3c6 --- /dev/null +++ b/src/errors/DefaultValidationError.js @@ -0,0 +1,8 @@ +class DefautValidationError extends Error { + constructor(errors) { + super('Invalid Attributes'); + this.details = errors; + } +} + +module.exports = DefautValidationError; diff --git a/src/errors.js b/src/errors/index.js similarity index 82% rename from src/errors.js rename to src/errors/index.js index 43b5fb9..01e8cc6 100644 --- a/src/errors.js +++ b/src/errors/index.js @@ -1,14 +1,8 @@ -function invalidAttributes(errors){ - let error = new Error('Invalid Attributes'); - error.details = errors; - return error; -} - module.exports = { classAsSecondParam: (ErroneousPassedClass) => new Error(`You passed the structure class as the second parameter of attributes(). The expected usage is \`attributes(schema)(${ ErroneousPassedClass.name || 'StructureClass' })\`.`), nonObjectAttributes: () => new TypeError('#attributes can\'t be set to a non-object.'), arrayOrIterable: () => new TypeError('Value must be iterable or array-like.'), missingDynamicType: (attributeName) => new Error(`Missing dynamic type for attribute: ${ attributeName }.`), invalidType: (attributeName) => new TypeError(`Attribute type must be a constructor or the name of a dynamic type: ${ attributeName }.`), - invalidAttributes + invalidAttributes: (errors, StructureValidationError) => new StructureValidationError(errors) }; diff --git a/src/strictMode/index.js b/src/strictMode/index.js new file mode 100644 index 0000000..5e506f0 --- /dev/null +++ b/src/strictMode/index.js @@ -0,0 +1,20 @@ +const Errors = require('../errors'); +const DefaultValidationError = require('../errors/DefaultValidationError'); + +exports.buildStrictDescriptorFor = function buildStrictDescriptorFor(StructureClass, schemaOptions) { + const StructureValidationError = schemaOptions.strictValidationErrorClass || DefaultValidationError; + + return { + value: function buildStrict(constructorArgs) { + const instance = new StructureClass(constructorArgs); + + const { valid, errors } = instance.validate(); + + if (!valid) { + throw Errors.invalidAttributes(errors, StructureValidationError); + } + + return instance; + } + }; +}; diff --git a/test/unit/instanceAndUpdate.spec.js b/test/unit/instanceAndUpdate.spec.js index 3c69073..194e666 100644 --- a/test/unit/instanceAndUpdate.spec.js +++ b/test/unit/instanceAndUpdate.spec.js @@ -143,15 +143,48 @@ describe('instantiating a structure', () => { describe('instantiating a structure with buildStrict', () => { context('when object is invalid', () => { - it('throw an error', () => { - let errorDetails = [{ - message: '"password" is required', - path: 'password' - }]; - - expect(() => { - User.buildStrict(); - }).to.throw(Error, 'Invalid Attributes').with.property('details').that.deep.equals(errorDetails); + context('when using default error class', () => { + it('throws a default error', () => { + let errorDetails = [{ + message: '"password" is required', + path: 'password' + }]; + + expect(() => { + User.buildStrict(); + }).to.throw(Error, 'Invalid Attributes').with.property('details').that.deep.equals(errorDetails); + }); + }); + + context('when using custom error class', () => { + var UserWithCustomError; + var InvalidUser; + + beforeEach(() => { + InvalidUser = class InvalidUser extends Error { + constructor(errors) { + super('There is something wrong with this user'); + this.errors = errors; + } + }; + + UserWithCustomError = attributes({ + name: { + type: String, + minLength: 3 + } + }, { + strictValidationErrorClass: InvalidUser + })(class UserWithCustomError {}); + }); + + it('throws a custom error', () => { + expect(() => { + UserWithCustomError.buildStrict({ + name: 'JJ' + }); + }).to.throw(InvalidUser, 'There is something wrong with this user'); + }); }); }); @@ -160,8 +193,8 @@ describe('instantiating a structure', () => { const user = User.buildStrict({ password: 'My password' }); - - expect(user.password).to.equal('My password'); + + expect(user.password).to.equal('My password'); }); }); });