Skip to content

Commit

Permalink
Allow to disable coercion (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
talyssonoc committed Mar 27, 2020
1 parent 93584cf commit ec206e8
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,7 @@
## Unreleased

- Allows to disable coercion

## 2.0.0-alpha.4 - 2020-03-21

- Publish only src folder for jest-structure
Expand Down
2 changes: 1 addition & 1 deletion docs/SUMMARY.md
Expand Up @@ -10,7 +10,7 @@
- [Arrays coercion](coercion/arrays-and-array-subclasses.md)
- [Generic coercion](coercion/generic-coercion.md)
- [Recursive coercion](coercion/recursive-coercion.md)
- [Observations](coercion/observations.md)
- [Disabling coercion](coercion/disabling-coercion.md)
- [Validation](validation/README.md)
- [String validations](validation/string-validations.md)
- [Number validations](validation/number-validations.md)
Expand Down
35 changes: 31 additions & 4 deletions docs/coercion/README.md
@@ -1,9 +1,36 @@
# Coercion

Structure does type coercion based on the declared [schema](../schema-concept/README.md). It's important to note that it __never__ coerces the following scenarios:
Structure does type coercion based on the declared [schema](../schema-concept/README.md), let's break it into 3 categories:

- `undefined`;
- `null` when `nullable` option is enabled;
- [Primitive type coercion](primitive-type-coercion.md)
- [Arrays coercion](arrays-and-array-subclasses.md)
- [Generic coercion](generic-coercion.md)

## Observations

Structure **never** coerces the following scenarios:

- value is `undefined`;
- value is `null` when `nullable` option is enabled;
- value is already of the declared type (except for arrays, we'll talk more about this soon).

Let's break the coercion into 3 categories.
Also, Structure only does **array items coercion** during instantiation, so mutating an array (using push, for example) won't coerce the new item:

```javascript
const Library = attributes({
books: {
type: Array,
itemType: String,
},
})(class Library {});

const library = new Library({
books: [1984],
});

library.books; // ['1984'] => coerced number to string

library.books.push(42);

library.books; // ['1984', 42] => new item was not coerced
```
85 changes: 85 additions & 0 deletions docs/coercion/disabling-coercion.md
@@ -0,0 +1,85 @@
# Disabling coercion

You can disable coercion for a whole structure or for attributes individually using the `coercion` option in the schema and attribute options, respectively. Notice that it will cause validation to fail when the passed value is not of the expected value:

## Disabling for the whole structure

```js
const User = attributes(
{
name: String,
age: Number,
},
{
coercion: false,
}
)(class User {});

const user = new User({ name: 123, age: '42' });

user.name; // 123
user.age; // '42'

const { valid, errors } = user.validate();

valid; // false
errors; /*
[
{ message: '"name" must be a string', path: ['name'] },
{ message: '"age" must be a number', path: ['age'] }
]
*/
```

## Disabling for specific attributes

```js
const User = attributes({
name: { type: String, coercion: false },
age: Number,
})(class User {});

const user = new User({ name: 123, age: '42' });

user.name; // 123
user.age; // 42

const { valid, errors } = user.validate();

valid; // false
errors; /*
[
{ message: '"name" must be a string', path: ['name'] }
]
*/
```

## Overwritting structure option with attribute option

If you define the `coercion` option both for the structure _and_ for an attribute, the structure one will apply for the whole schema except the specific attributes that overwrite it:

```js
const User = attributes(
{
name: { type: String, coercion: true },
age: Number,
},
{
coercion: false,
}
)(class User {});

const user = new User({ name: 123, age: '42' });

user.name; // '123'
user.age; // '42'

const { valid, errors } = user.validate();

valid; // false
errors; /*
[
{ message: '"age" must be a number', path: ['age'] }
]
*/
```
23 changes: 0 additions & 23 deletions docs/coercion/observations.md

This file was deleted.

4 changes: 4 additions & 0 deletions packages/structure/src/coercion/coercion.js
Expand Up @@ -20,6 +20,10 @@ exports.create = function createCoercionFor(coercion, attributeDefinition) {
};
};

exports.disabled = {
coerce: (value) => value,
};

const getNullableValue = (coercion, attributeDefinition) =>
needsNullableInitialization(attributeDefinition) ? getNullValue(coercion) : null;

Expand Down
4 changes: 4 additions & 0 deletions packages/structure/src/coercion/index.js
Expand Up @@ -10,6 +10,10 @@ const types = [
];

exports.for = function coercionFor(attributeDefinition) {
if (!attributeDefinition.options.coercion) {
return Coercion.disabled;
}

const coercion = getCoercion(attributeDefinition);

return Coercion.create(coercion, attributeDefinition);
Expand Down
@@ -1,4 +1,4 @@
const { isFunction, isString } = require('lodash');
const { isFunction, isString, isUndefined } = require('lodash');
const Coercion = require('../../coercion');
const Validation = require('../../validation');
const Errors = require('../../errors');
Expand Down Expand Up @@ -61,7 +61,7 @@ class AttributeDefinition {
this.__isAttributeDefinition = true;

this.name = name;
this.options = options;
options = this.options = applyDefaultOptions(options, schema);
this.hasDefault = 'default' in options;
this.isDynamicDefault = isFunction(options.default);
this.hasDynamicType = hasDynamicType(options);
Expand Down Expand Up @@ -140,6 +140,16 @@ const makeComplete = (options) => {
return { type: options };
};

const applyDefaultOptions = (options, schema) => {
return {
...options,
coercion: inheritOptionFromSchema(options.coercion, schema.options.coercion),
};
};

const inheritOptionFromSchema = (option, schemaOption) =>
!isUndefined(option) ? option : schemaOption;

const isShorthand = (options) => isFunction(options) || isString(options);

const hasStaticType = (options) => isFunction(options.type);
Expand Down
11 changes: 10 additions & 1 deletion packages/structure/src/schema/index.js
Expand Up @@ -48,7 +48,7 @@ class Schema {
}

constructor({ attributeDefinitions, wrappedClass, options }) {
this.options = options;
this.options = applyDefaultOptions(options);
this.attributeDefinitions = AttributeDefinitions.for(attributeDefinitions, { schema: this });
this.wrappedClass = wrappedClass;
this.identifier = options.identifier || wrappedClass.name;
Expand Down Expand Up @@ -97,4 +97,13 @@ class Schema {
}
}

const defaultOptions = {
coercion: true,
};

const applyDefaultOptions = (options) => ({
...defaultOptions,
...options,
});

module.exports = Schema;

0 comments on commit ec206e8

Please sign in to comment.