Skip to content

Commit

Permalink
Add custom error for strict mode (#95)
Browse files Browse the repository at this point in the history
* Allow custom error class for strict mode

* Cover custom error class with tests

* Make linter happy

* Bump minor

* Move validation error class decision
  • Loading branch information
talyssonoc committed Aug 27, 2019
1 parent 6de520a commit d5e88f2
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 51 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
85 changes: 64 additions & 21 deletions dist/structure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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') + ')`.');
Expand All @@ -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);
}
};

/***/ },
Expand Down Expand Up @@ -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;

Expand Down
41 changes: 40 additions & 1 deletion docs/strict-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,43 @@ var user = User.buildStrict({
// { message: '"name" is required', path: 'name' },
// { message: '"age" must be a number', path: 'age' }
// ]
```
```

## 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' }
// ]
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
12 changes: 2 additions & 10 deletions src/attributes/decorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/errors/DefaultValidationError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class DefautValidationError extends Error {
constructor(errors) {
super('Invalid Attributes');
this.details = errors;
}
}

module.exports = DefautValidationError;
8 changes: 1 addition & 7 deletions src/errors.js → src/errors/index.js
Original file line number Diff line number Diff line change
@@ -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)
};
20 changes: 20 additions & 0 deletions src/strictMode/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
};
55 changes: 44 additions & 11 deletions test/unit/instanceAndUpdate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});

Expand All @@ -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');
});
});
});
Expand Down

0 comments on commit d5e88f2

Please sign in to comment.