Skip to content

Commit

Permalink
Merge 8a7b1ca into 9c7cb47
Browse files Browse the repository at this point in the history
  • Loading branch information
talyssonoc committed Mar 12, 2020
2 parents 9c7cb47 + 8a7b1ca commit 46692c6
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,7 @@ Refactors:
Enhancements

- Implement jest-structure assertions
- It's possible to set custom getters e setters directly in the structure class

Breaking changes:

Expand All @@ -18,6 +19,7 @@ Breaking changes:
- Attribute path in validation _messages_ contains the whole path joined by '.'
- The name used for the dynamic import should aways be the same as the name of its type or else a custom identifier must be used
- Non-nullable attributes with value null will use default value the same way undefined does
- Structure classes now have two methods to generically set and get the value of the attributes, `.get(attributeName)` and `.set(attributeName, attributeValue)`

## 1.8.0 - 2019-09-16

Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Expand Up @@ -5,6 +5,7 @@
- [Shorthand and complete type descriptor](schema-concept/shorthand-and-complete-type-descriptor.md)
- [Circular reference](schema-concept/circular-references-and-dynamic-types.md)
- [Nullable attributes](schema-concept/nullable-attributes.md)
- [Custom setters and getters](custom-setters-and-getters.md)
- [Coercion](coercion/README.md)
- [Primitive type coercion](coercion/primitive-type-coercion.md)
- [Arrays coercion](coercion/arrays-and-array-subclasses.md)
Expand Down
86 changes: 86 additions & 0 deletions docs/custom-setters-and-getters.md
@@ -0,0 +1,86 @@
# Custom setters and getters

Sometimes it may be necessary to have custom setters and/or getters for some attributes. Structure allows you to do that using native JavaScript setters and getters. It will even support coercion.

It's important to notice that you **should not** try to access the attribute directly inside its getter or to set it directly inside its setter because it will cause infinite recursion, this is default JavaScript behavior. To access an attribute value inside its getter you should use `this.get(attributeName)`, and to set the value of an attribute a setter you should use `this.set(attributeName, attributeValue)`:

```js
const User = attributes({
firstName: String,
lastName: String,
age: Number,
})(
class User {
get firstName() {
return `-> ${this.get('firstName')}`;
}

set lastName(newLastname) {
return this.set('lastName', `Mac${newLastName}`);
}

get age() {
// do NOT do that. Instead, use this.get and this.set inside getters and setters
return this.age * 1000;
}

// this is NOT an attribute, just a normal getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
);

const user = new User({ firstName: 'Connor', lastName: 'Leod' });

user.firstName; // -> Connor
user.lastName; // MacLeod
user.fullName; // -> Connor MacLeod
```

## Inheritance

Custom setters and getters are also inherited, be your superclass a pure JavaScript class or another structure:

```js
class Person {
// If Person was a structure instead of a pure class, that would work too
get name() {
return 'The person';
}
}

const User = attributes({
name: String,
})(class User extends Person {});

const user = new User({ name: 'Will not be used' });

user.name; // -> The person
```

**Important**

JavaScript nativelly won't let you inherit only one of the accessors (the getter or the setter) if you define the other accessor in a subclass:

```js
class Person {
get name() {
return 'Person';
}
}

class User extends Person {
set name(newName) {
this._name = newName;
}
}

const user = new Person();
user.name = 'The user';
user.name; // -> The user
```

It happens because _once you define one of the accessors in a subclass_, all the accessors for the same attribute inherited from the superclass will be ignored.

While it's a weird behavior, Structure will follow the same functionality so the Structure classes inheritance work the same way of pure JavaScript classes, avoiding inconsistencies.
2 changes: 1 addition & 1 deletion packages/structure/package.json
Expand Up @@ -37,7 +37,7 @@
"build": "webpack",
"prepublish": "yarn run build",
"coveralls": "yarn run coverage --coverageReporters=text-lcov | coveralls",
"lint": "eslint {src,test,benchmark}/**/*.js",
"lint": "eslint {src,test}/**/*.js",
"format": "prettier --write {src,test}/**/*.js"
},
"dependencies": {
Expand Down
8 changes: 8 additions & 0 deletions packages/structure/src/attributes/index.js
@@ -0,0 +1,8 @@
const { ATTRIBUTES } = require('../symbols');

exports.setInInstance = function setAttributesInInstance(instance, attributes) {
Object.defineProperty(instance, ATTRIBUTES, {
configurable: true,
value: attributes,
});
};
101 changes: 85 additions & 16 deletions packages/structure/src/descriptors/index.js
@@ -1,14 +1,16 @@
const { isObject } = require('lodash');
const { SCHEMA, ATTRIBUTES } = require('../symbols');
const { SCHEMA, ATTRIBUTES, DEFAULT_ACCESSOR } = require('../symbols');
const Errors = require('../errors');
const StrictMode = require('../strictMode');
const Cloning = require('../cloning');
const { defineProperty } = Object;
const Attributes = require('../attributes');
const { defineProperty, defineProperties } = Object;

exports.addTo = function addDescriptorsTo(schema, StructureClass) {
setSchema();
setBuildStrict();
setAttributesGetterAndSetter();
setGenericAttributeGetterAndSetter();
setEachAttributeGetterAndSetter();
setValidation();
setSerialization();
Expand Down Expand Up @@ -43,10 +45,31 @@ exports.addTo = function addDescriptorsTo(schema, StructureClass) {
throw Errors.nonObjectAttributes();
}

defineProperty(this, ATTRIBUTES, {
configurable: true,
value: newAttributes,
});
const coercedAttributes = schema.coerce(newAttributes);

Attributes.setInInstance(this, coercedAttributes);
},
});
}

function setGenericAttributeGetterAndSetter() {
defineProperties(StructureClass.prototype, {
get: {
value: function get(attributeName) {
return this.attributes[attributeName];
},
},
set: {
value: function set(attributeName, attributeValue) {
const attributeDefinition = schema.attributeDefinitions[attributeName];

if (!attributeDefinition) {
throw Errors.inexistentAttribute(attributeName);
}

const coercedValue = attributeDefinition.coerce(attributeValue);
this.attributes[attributeName] = coercedValue;
},
},
});
}
Expand All @@ -56,23 +79,25 @@ exports.addTo = function addDescriptorsTo(schema, StructureClass) {
defineProperty(
StructureClass.prototype,
attrDefinition.name,
attributeDescriptor(attrDefinition)
attributeDescriptorFor(attrDefinition)
);
});
}

function attributeDescriptor(attrDefinition) {
function attributeDescriptorFor(attrDefinition) {
const { name } = attrDefinition;

return {
get() {
return this.attributes[name];
},
const attributeDescriptor = findAttributeDescriptor(name);

set(value) {
this.attributes[name] = schema.attributeDefinitions[name].coerce(value);
},
};
if (isDefaultAccessor(attributeDescriptor.get)) {
attributeDescriptor.get = defaultGetterFor(name);
}

if (isDefaultAccessor(attributeDescriptor.set)) {
attributeDescriptor.set = defaultSetterFor(name);
}

return attributeDescriptor;
}

function setValidation() {
Expand Down Expand Up @@ -104,4 +129,48 @@ exports.addTo = function addDescriptorsTo(schema, StructureClass) {
value: cloning.clone,
});
}

function defaultGetterFor(name) {
function get() {
return this.get(name);
}

get[DEFAULT_ACCESSOR] = true;

return get;
}

function defaultSetterFor(name) {
function set(value) {
this.set(name, value);
}

set[DEFAULT_ACCESSOR] = true;

return set;
}

function isDefaultAccessor(accessor) {
return !accessor || accessor[DEFAULT_ACCESSOR];
}

function findAttributeDescriptor(propertyName) {
let proto = StructureClass.prototype;

while (proto !== Object.prototype) {
const attributeDescriptor = Object.getOwnPropertyDescriptor(proto, propertyName);

if (attributeDescriptor) {
return {
...attributeDescriptor,
enumerable: false,
configurable: true,
};
}

proto = proto.__proto__;
}

return {};
}
};
3 changes: 3 additions & 0 deletions packages/structure/src/errors/index.js
Expand Up @@ -18,3 +18,6 @@ exports.invalidType = (attributeName) =>

exports.invalidAttributes = (errors, StructureValidationError) =>
new StructureValidationError(errors);

exports.inexistentAttribute = (attributeName) =>
new Error(`${attributeName} is not an attribute of this structure`);
10 changes: 3 additions & 7 deletions packages/structure/src/initialization/index.js
@@ -1,15 +1,11 @@
const { ATTRIBUTES } = require('../symbols');
const Attributes = require('../attributes');

exports.for = function initializationForSchema(schema) {
return {
initialize(instance, { attributes }) {
Object.defineProperty(instance, ATTRIBUTES, {
configurable: true,
value: Object.create(null),
});
Attributes.setInInstance(instance, Object.create(null));

for (let i = 0; i < schema.attributeDefinitions.length; i++) {
const attrDefinition = schema.attributeDefinitions[i];
for (let attrDefinition of schema.attributeDefinitions) {
const attrPassedValue = attributes[attrDefinition.name];

// will coerce through setters
Expand Down
12 changes: 12 additions & 0 deletions packages/structure/src/schema/index.js
Expand Up @@ -83,6 +83,18 @@ class Schema {

return this.validation.validate(attributes);
}

coerce(newAttributes) {
const attributes = Object.create(null);

for (const attributeDefinition of this.attributeDefinitions) {
const { name } = attributeDefinition;
const value = newAttributes[name];
attributes[name] = attributeDefinition.coerce(value);
}

return attributes;
}
}

module.exports = Schema;
1 change: 1 addition & 0 deletions packages/structure/src/symbols.js
@@ -1,4 +1,5 @@
module.exports = {
SCHEMA: Symbol('schema'),
ATTRIBUTES: Symbol('attributes'),
DEFAULT_ACCESSOR: Symbol('defaultAccessor'),
};
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`instantiating a structure custom setters and getters when tries to set an attribute that does not exist fails and throws an error 1`] = `"NOPE is not an attribute of this structure"`;

0 comments on commit 46692c6

Please sign in to comment.