Skip to content

Commit

Permalink
Move fields initialization to init method
Browse files Browse the repository at this point in the history
Initialization should be carried out by constructor but unfortunately ES6
specification forbides referring to `this` before calling super. It makes
impossible to validate default values by calling deserialize method in
constructor. To workaround this behaviour this commit moves actual
initialization to `init` function and makes constructor responsible only for
validation of default value and freezing params.
  • Loading branch information
qzb committed May 3, 2016
1 parent b93c8cf commit c08804e
Show file tree
Hide file tree
Showing 15 changed files with 114 additions and 99 deletions.
4 changes: 0 additions & 4 deletions lib/fields/boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ class BooleanField extends Field {
*/
constructor(params) {
super(params);

if (this.params.default !== undefined && typeof this.params.default !== 'boolean') {
throw new Error('Default value must be a boolean');
}
}

/**
Expand Down
9 changes: 5 additions & 4 deletions lib/fields/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ class EmailField extends StringField {
* @param {string} [params.default]
*/
constructor(params) {
super(Object.assign({}, params, { trim: true }));
super(params);
}

if (this.params.default !== undefined && !validateEmail(this.params.default)) {
throw new Error('Default value must be a valid email address');
}
init(params) {
super.init(params);
this.params.trim = true;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion lib/fields/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ class EnumField extends Field {
* @param {string} [params.default]
*/
constructor(choices, params) {
super(params);
super(choices, params);
}

init(choices, params) {
super.init(params);

if (!choices) {
throw new Error('Choices list is missing');
Expand Down
37 changes: 32 additions & 5 deletions lib/fields/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,40 @@ const customError = require('custom-error');
* Generic field class. Every other field class must inherit from this one.
*/
class Field {
constructor() {
this.init.apply(this, arguments);

// Validate default value

if (this.params.default !== undefined) {
// Check if default value is deserializable

try {
this.deserialize(this.params.default);
} catch (error) {
if (error instanceof Field.ValidationError) {
throw new Error(`Default value is invalid: ${error.message.toLowerCase()}`);
} else {
throw error;
}
}
}

// Freeze params

Object.freeze(this.params);
}

/**
* @param {object} [params]
* @param {boolean} [params.optional]
* @param {*} [params.default]
* Initializes field's instance. It should be carried out by constructor but unfortunately ES6 specification
* forbides referring to `this` before calling super. It makes impossible to validate default values by calling
* deserialize method in constructor. To workaround this behaviour actual initialization is made by init function
* and constructor is responsible only for validation of default value and freezing params.
*
* @param params
*/
constructor(params) {
this.params = Object.freeze(Object.assign({}, params));
init(params) {
this.params = Object.assign({}, params);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/fields/integer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class IntegerField extends NumberField {
*/
constructor(params) {
super(params);
}

if (this.params.default !== undefined && !Number.isInteger(this.params.default)) {
throw new Error('Default value must be an integer');
}
init(params) {
super.init(params);

if (this.params.min !== undefined && !Number.isInteger(this.params.min)) {
throw new Error('Min value must be an integer');
Expand Down
22 changes: 11 additions & 11 deletions lib/fields/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@ const Field = require('./field');
*/
class ModelField extends Field {
/**
* @param {object} FieldModel - model which will be used to serialize/deserialize object's properties
* @param {object|function} model - model which will be used to serialize/deserialize object's properties
* @param {object} [params]
* @param {boolean} [params.optional]
*/
constructor(FieldModel, params) {
super(params);
constructor(model, params) {
super(model, params);
}

if (!FieldModel) {
throw new Error('Model is missing');
}
init(model, params) {
super.init(params);

if (typeof FieldModel === 'object') {
FieldModel = Model.extend(FieldModel);
if (!model) {
throw new Error('Model is missing');
}

if (this.params.default !== undefined) {
throw new Error('This field doesn\'t accept default value');
if (typeof model === 'object') {
model = Model.extend(model);
}

this.Model = FieldModel;
this.Model = model;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/fields/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ class NumberField extends Field {
*/
constructor(params) {
super(params);
}

if (this.params.default !== undefined && !Number.isFinite(this.params.default)) {
throw new Error('Default value must be a number');
}
init(params) {
super.init(params);

if (this.params.min !== undefined && !Number.isFinite(this.params.min)) {
throw new Error('Min value must be a number');
Expand Down
6 changes: 3 additions & 3 deletions lib/fields/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class StringField extends Field {
*/
constructor(params) {
super(params);
}

if (this.params.default !== undefined && typeof this.params.default !== 'string') {
throw new Error('Default value must be a string');
}
init(params) {
super.init(params);

if (this.params.minLength !== undefined && (!Number.isInteger(this.params.minLength) || this.params.minLength < 0)) {
throw new Error('Min length must be a positive integer');
Expand Down
6 changes: 0 additions & 6 deletions test/fields/boolean-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ describe('BooleanField', function () {
expect(field).to.be.instanceOf(Field);
expect(field).to.be.instanceOf(BooleanField);
});

it('should throw error when default value is not a boolean', function () {
let call = () => new BooleanField({ default: 'true' });

expect(call).to.throw(Error, 'Default value must be a boolean');
});
});

describe('deserialize method', function () {
Expand Down
12 changes: 0 additions & 12 deletions test/fields/email-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,6 @@ describe('EmailField', function () {
expect(field).to.be.instanceOf(Field);
expect(field).to.be.instanceOf(EmailField);
});

it('should throw error when default value is not a string', function () {
let call = () => new EmailField({ default: 7 });

expect(call).to.throw(Error, 'Default value must be a string');
});

it('should throw error when default value is not a valid email address', function () {
let call = () => new EmailField({ default: 'abcdef' });

expect(call).to.throw(Error, 'Default value must be a valid email address');
});
});

describe('deserialize method', function () {
Expand Down
55 changes: 51 additions & 4 deletions test/fields/field-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,65 @@ describe('Field', function () {
expect(field.params).to.be.deep.equal({});
});

it('should create new instance when all params are specified', function () {
it('should pass all specified arguments to init method', function () {
class TestField extends Field {
init() {
this.args = Array.prototype.slice.call(arguments);
super.init.apply(this, arguments);
}
}

let field = new TestField(1, 2, 3);

expect(field.args).to.be.deep.equal([1, 2, 3]);
});

it('should freeze params', function () {
let field = new Field();

expect(field.params).to.be.frozen;
});

it('should throw error when default value is invalid', function () {
class TestField extends Field {
deserialize() {
throw new Field.ValidationError('Value is invalid');
}
}

let call = () => new TestField({ default: true });

expect(call).to.throw(Error, 'Default value is invalid: value is invalid');
});

it('should throw error when unexpected error occurs during deserialization of default value', function () {
let error = new Error();
class TestField extends Field {
deserialize() {
throw error;
}
}

let call = () => new TestField({ default: true });

expect(call).to.throw(error);
});
});

describe('init method', function () {
it('should copy params to field', function () {
let params = {
default: {},
optional: true,
foo: 'bar'
};

let field = new Field(params);
let field = {};
Field.prototype.init.call(field, params);

expect(field).to.be.instanceOf(Field);
expect(field.params).to.be.deep.equal(params);
expect(field.params).to.be.deep.frozen;
expect(field.params).to.be.not.equal(params);
expect(field.params).to.be.not.frozen;
});
});

Expand Down
18 changes: 0 additions & 18 deletions test/fields/integer-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,6 @@ describe('IntegerField', function () {
expect(field).to.be.instanceOf(IntegerField);
});

it('should throw error when default value is not a number', function () {
let call = () => new IntegerField({ default: '7' });

expect(call).to.throw(Error, 'Default value must be a number');
});

it('should throw error when default value is not finite', function () {
let call = () => new IntegerField({ default: Infinity });

expect(call).to.throw(Error, 'Default value must be a number');
});

it('should throw error when default value is not an integer', function () {
let call = () => new IntegerField({ default: 7.7 });

expect(call).to.throw(Error, 'Default value must be an integer');
});

it('should throw error when min value is not a number', function () {
let call = () => new IntegerField({ min: null });

Expand Down
6 changes: 0 additions & 6 deletions test/fields/model-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ describe('ModelField', function() {

expect(call).to.throw(Error, 'Model is missing');
});

it('should throw error when default value is specified', function() {
let call = () => new ModelField({}, { default: {} });

expect(call).to.throw(Error, 'This field doesn\'t accept default value');
});
});

describe('deserialize method', function () {
Expand Down
12 changes: 0 additions & 12 deletions test/fields/number-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,6 @@ describe('NumberField', function () {
expect(field).to.be.instanceOf(NumberField);
});

it('should throw error when default value is not a number', function () {
let call = () => new NumberField({ default: '7' });

expect(call).to.throw(Error, 'Default value must be a number');
});

it('should throw error when default value is not finite', function () {
let call = () => new NumberField({ default: Infinity });

expect(call).to.throw(Error, 'Default value must be a number');
});

it('should throw error when min value is not a number', function () {
let call = () => new NumberField({ min: null });

Expand Down
8 changes: 1 addition & 7 deletions test/fields/string-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('StringField', function () {

it('should create new instance of field when all params are specified', function () {
let field = new StringField({
default: 'abc',
default: 'a b c d e f g',
optional: true,
trim: true,
maxLength: 100,
Expand All @@ -27,12 +27,6 @@ describe('StringField', function () {
expect(field).to.be.instanceOf(StringField);
});

it('should throw error when default value is not a string', function () {
let call = () => new StringField({ default: 7 });

expect(call).to.throw(Error, 'Default value must be a string');
});

it('should throw error when min length isn\'t an integer', function () {
let call = () => new StringField({ minLength: 7.000001 });

Expand Down

0 comments on commit c08804e

Please sign in to comment.