proteus
Version:
A declarative way of creating objects, properties, and classes in ES5 JavaScript
280 lines (253 loc) • 8.27 kB
JavaScript
var putil = require("proteus/util"),
EMPTY = putil.EMPTY,
slice = putil.slice,
enumerate = putil.enumerate,
merge = putil.merge,
hasOwn = Object.prototype.hasOwnProperty,
protoOf = Object.getPrototypeOf,
PROPERTY_SPEC
;
PROPERTY_SPEC = Object.create(Object.prototype, {
init: {
value: function (val, descriptors) {
var desc, i;
this.value = val;
this.writable = true;
this.configurable = true;
this.enumerable = true;
this.init = true;
i = descriptors.length;
while (i--) {
desc = descriptors[i];
desc = typeof desc === "string" ? desc : desc.source;
if (typeof this[desc] === "function") {
this[desc]();
}
}
return this;
}
},
apply: {
value: function (obj, name) {
name = name || this.name;
if (name) {
Object.defineProperty(obj, name, this);
}
return this;
}
},
hidden: {
value: function () {
this.enumerable = false;
return this;
}
},
invariable: {
value: function () {
this.writable = false;
return this;
}
},
immutable: {
value: function () {
this.configurable = false;
return this;
}
},
getter: {
value: function (fn) {
fn = fn || this.value;
this.get = fn;
delete this.value;
delete this.writable;
return this;
}
},
setter: {
value: function (fn) {
fn = fn || this.value;
this.set = fn;
delete this.value;
delete this.writable;
return this;
}
},
accessor: {
value: function (fn) {
fn = fn || this.value;
return this.getter(fn).setter(fn);
}
}
});
function isPropertySpec (obj) {
var p = obj;
if (typeof obj !== "object") {
return false;
}
do {
if (p === PROPERTY_SPEC) {
return true;
}
} while ((p = Object.getPrototypeOf(p)));
return hasOwn.call(obj, "value") || hasOwn.call(obj, "configurable") ||
hasOwn.call(obj, "writable") || hasOwn.call(obj, "enumerable") ||
hasOwn.call(obj, "get") || hasOwn.call(obj, "set") ||
false;
}
function property (val) {
return Object.create(PROPERTY_SPEC).init(val, slice(arguments, 1));
}
function method (fn) {
return property.apply(this, arguments);
}
function getter (fn) {
return property.apply(this, arguments).getter();
}
function setter (fn) {
return property.apply(this, arguments).setter();
}
function accessor (fn) {
return property.apply(this, arguments).accessor();
}
function getKeys (obj, hidden) {
return hidden ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
}
/**
* Get all enumerable property names for an object, all the way up the
* prototype chain.
*
* @method getPropertyNames
* @param {Object} obj object to get property names from
* @returns {Array}
*/
function getPropertyNames (obj, hidden) {
var protoOf = Object.getPrototypeOf,
props = enumerate.apply(this, getKeys(obj, hidden))
;
while ((obj = protoOf(obj))) {
merge(props, enumerate.apply(this, getKeys(obj, hidden)));
}
return getKeys(props, hidden);
}
/**
* Get a property descriptor for an object whereever it may exist in the
* prototype chain.
*
* @method getPropertyDescriptor
* @param {Object} obj object to get property descriptor for
* @param {String} key name of the property to look up
* @returns {Object}
*/
function getPropertyDescriptor (obj, key) {
while (obj && !hasOwn.call(obj, key)) {
obj = protoOf(obj);
}
return obj && Object.getOwnPropertyDescriptor(obj, key);
}
module.exports = {
isPropertySpec: isPropertySpec,
property: property,
prop: property,
method: method,
fn: method,
getter: getter,
setter: setter,
accessor: accessor,
getKeys: getKeys,
getPropertyNames: getPropertyNames,
getPropertyDescriptor: getPropertyDescriptor,
/**
* Utility method for creating 'plain' properties on an object. Plain
* being {enumerable: true, writable: true, configurable: true}, the
* passed spec can override these defaults.
*
* @method defineProperty
* @param {Object} obj object to define property on
* @param {String} name property name
* @param {Mixed} val the value for the property
* @param {Object} spec optional, property specification
* @returns {Object} obj
*/
defineProperty: function (obj, name, val, spec) {
spec = merge(property(val), spec || EMPTY);
return Object.defineProperty(obj, name, spec);
},
/**
* Define multiple properties.
*
* @method defineProperties
* @param {Object} obj object to define property on
* @param {Array} list list of arguments to pass to 'defineProperty'
* @returns {Object} the object passed in
*/
defineProperties: function (obj, list) {
var i = list.length;
while (i--) {
this.defineProperty.apply(this, [obj].concat(list[i]));
}
return obj;
},
//-----------------------------------------------------------------------
// Deprecated
//-----------------------------------------------------------------------
/**
* Utility method for creating a getter on an object. The property
* definition will default to {enumerable: true, configurable: true}
* unless overridden with the spec object.
*
* @method defineGetter
* @param {Object} obj object to define property on
* @param {String} name property name
* @param {Function} fn the getter function
* @param {Object} spec optional, property specification
* @returns {Object} obj
*/
defineGetter: function (obj, name, fn, spec) {
spec = merge(getter(fn), spec || EMPTY);
return Object.defineProperty(obj, name, spec);
},
/**
* Utility method for creating a setter on an object. The property
* definition will default to {enumerable: true, configurable: true}
* unless overridden with the spec object.
*
* @method defineSetter
* @param {Object} obj object to define property on
* @param {String} name property name
* @param {Function} fn the setter function
* @param {Object} spec optional, property specification
* @returns {Object} obj
*/
defineSetter: function (obj, name, fn, spec) {
spec = merge(setter(fn), spec || EMPTY);
return Object.defineProperty(obj, name, spec);
},
/**
* Utility method for creating both a getter and a setter on an object.
* The property definition will default to {enumerable: true, configurable:
* true} unless overridden with the spec object.
*
* @method defineGetSet
* @param {Object} obj object to define property on
* @param {String} name property name
* @param {Function} getter the getter function, or if 'setter' is not
* given, then the function will be used for both getting and
* setting.
* @param {Function} setter optional, the setter function
* @param {Object} spec optional, property specification
* @returns {Object} obj
*/
defineGetSet: function (obj, name, getter, setter, spec) {
if (typeof setter !== "function") {
spec = setter;
setter = getter;
}
spec = merge(property().getter(getter).setter(setter), spec || EMPTY);
return Object.defineProperty(obj, name, spec);
}
};
Object.defineProperty(
module.exports,
"PROPERTY_SPEC",
property(PROPERTY_SPEC, /immutable/, /invariable/)
);