Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions addon/-private/extensions/with-validation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { wrapField } from '../wrap-field';
import Ember from 'ember';

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

const HAS_VALIDATION = new WeakSet();
Expand All @@ -21,10 +26,19 @@ 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];

if (isComputedProperty(meta, 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 || target === null) return;

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

export function isExtensionOf(childClass, parentClass) {
return childClass.prototype instanceof parentClass;
}
80 changes: 80 additions & 0 deletions addon/-private/wrap-computed-property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 function isComputedProperty(meta, key) {
const possibleDesc = meta.peekDescriptors(key);

return possibleDesc && possibleDesc.isDescriptor;
}

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

let originalValue = desc.get(instance, key);

let validatedProperty = new ComputedValidatedProperty(
desc,
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;
}

/**
* Wrap a property in a `getter/setter` that validates it is the right
* type.
*
* @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