Skip to content

Commit

Permalink
refactor: reduce amount of work on initialization
Browse files Browse the repository at this point in the history
- Simplify approach to wrapping most properties
- Reduce the amount of work done when initializing objects
  • Loading branch information
alexlafroscia committed Jan 7, 2019
1 parent 81ae6c1 commit db4fcbd
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 247 deletions.
15 changes: 12 additions & 3 deletions addon/-private/extensions/with-validation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { wrapField } from '../wrap-field';
import Ember from 'ember';

import wrapComputedProperty from '../wrap-computed-property';
import { getValidationsFor } from '../validations-for';

const HAS_VALIDATION = new WeakSet();
Expand All @@ -21,10 +23,17 @@ export function withExtension(klass) {
init(...args) {
super.init(...args);

const validations = getValidationsFor(this.constructor);
const { constructor } = this;

const validations = getValidationsFor(constructor);
const meta = Ember.meta(this);

for (let key in validations) {
wrapField(this.constructor, this, validations, key);
const validation = validations[key];

wrapComputedProperty(constructor, this, meta, validation, key);

validation.run(constructor, key, this[key], 'init');
}
}
};
Expand Down
25 changes: 0 additions & 25 deletions addon/-private/utils/computed.js

This file was deleted.

17 changes: 0 additions & 17 deletions addon/-private/utils/object.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
/**
* Walk up the prototype chain and find the property descriptor for the
* given property
*
* @param {object} target
* @param {string} property
* @return {Descriptor|undefined}
*/
export function getPropertyDescriptor(target, property) {
if (target === undefined) return;

return (
Object.getOwnPropertyDescriptor(target, property) ||
getPropertyDescriptor(Object.getPrototypeOf(target), property)
);
}

export function isExtensionOf(childClass, parentClass) {
return childClass.prototype instanceof parentClass;
}
77 changes: 77 additions & 0 deletions addon/-private/wrap-computed-property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
function guardBind(fn, ...args) {
if (typeof fn === 'function') {
return fn.bind(...args);
}
}

class ComputedValidatedProperty {
constructor(desc, klass, originalValue, typeValidators) {
this.isDescriptor = true;

this.desc = desc;
this.klass = klass;
this.originalValue = originalValue;
this.typeValidators = typeValidators;

this.setup = guardBind(desc.setup, desc);
this.teardown = guardBind(desc.teardown, desc);
this.willChange = guardBind(desc.willChange, desc);
this.didChange = guardBind(desc.didChange, desc);
this.willWatch = guardBind(desc.willWatch, desc);
this.didUnwatch = guardBind(desc.didUnwatch, desc);
}

get(obj, keyName) {
let { klass, typeValidators } = this;
let newValue = this.desc.get(obj, keyName);

if (typeValidators) {
typeValidators.run(klass, keyName, newValue, 'get');
}

return newValue;
}

set(obj, keyName, value) {
let { klass, typeValidators } = this;
let newValue = this.desc.set(obj, keyName, value);

if (typeValidators) {
typeValidators.run(klass, keyName, newValue, 'set');
}

return newValue;
}
}

export default function wrapComputedProperty(
klass,
instance,
meta,
typeValidators,
key
) {
const possibleDesc = meta.peekDescriptors(key);

// Handle the argument being overwritten by a computed property
if (possibleDesc && possibleDesc.isDescriptor) {
let originalValue = possibleDesc.get(instance, key);

let validatedProperty = new ComputedValidatedProperty(
possibleDesc,
klass,
originalValue,
typeValidators
);

Object.defineProperty(instance, key, {
configurable: true,
enumerable: true,
get() {
return validatedProperty.get(instance, key);
}
});

meta.writeDescriptors(key, validatedProperty);
}
}
98 changes: 98 additions & 0 deletions addon/-private/wrap-descriptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Transforms a `field` descriptor into a property `getter/setter` that
* ensures that the value matches the validators.
*
* @param {Object} element
* @param {Function} validator
*/
function wrapFieldDescriptor(
{ descriptor, initializer, key, placement },
validator
) {
const { ...descriptorToApply } = descriptor;
delete descriptorToApply.writable;

let initialized = false;
let cachedValue;

const newElement = {
key,
placement,
kind: 'method',
descriptor: {
...descriptorToApply,
get() {
if (!initialized) {
initialized = true;
cachedValue = initializer ? initializer.call(this) : undefined;
}

return cachedValue;
},
set(newValue) {
initialized = true;

validator.run(this.constructor, key, newValue, 'set');

cachedValue = newValue;
}
}
};

return newElement;
}

/**
* Wraps a property `getter/setter` such that the value must match the
* validator.
*
* @param {Object} element
* @param {Function} validator
*/
function wrapMethodDescriptor({ descriptor, key, ...rest }, validator) {
const { get, set, ...restOfDescriptor } = descriptor;

const newElement = {
...rest,
key,
descriptor: {
...restOfDescriptor,
get() {
const value = get.call(this);

validator.run(this.constructor, key, value, 'get');

return value;
},
set(newValue) {
const setterReturnedValue = set.call(this, newValue);

validator.run(this.constructor, key, this[key], 'set');

return setterReturnedValue;
}
}
};

return newElement;
}

/**
* Transforms a `field` descriptor, wrapping it with a setter that performs
* validator.
*
* @param {Object} element
* @param {Function} validator validator function for new values
*/
export default function wrapDescriptor(element, validator) {
switch (element.kind) {
case 'field':
return wrapFieldDescriptor(element, validator);
case 'method':
return wrapMethodDescriptor(element, validator);
default:
throw new Error(
'`@argument` must be applied to a `field` or property accessor'
);
}
}
Loading

0 comments on commit db4fcbd

Please sign in to comment.