/**
* Create custom objects and methods by aggregating and abstracting esources.
*
* @version 0.0.1
* @module abstract
* @constractor
*/
function Abstract() {
// Create Model Builder context
if( 'function' === typeof arguments[0] ) {
return Abstract.createModel( arguments[0] );
}
// Create Plain object
if( 'object' === typeof arguments[0] ) {
return Abstract.create( arguments[0], arguments[1] );
}
// Return for context
return this;
}
/**
* Constructor Properties
*
* The following properties are available within the constructor factory or by
* referencing the constructor.
*
*/
Object.defineProperties( module.exports = Abstract, {
get: {
/**
* Get a key from current context's _meta
*
* @todo Migrate to use object-settings once Abstract core is ready.
* @temp
* @param key
* @returns {string}
*/
get: function() {
return function get() {
return require( 'object-settings' ).prototype ? require( 'object-settings' ).prototype.get.apply( Abstract, arguments ) : Abstract.utility.noop();
}
},
configurable: true,
enumerable: false
},
set: {
/**
* Set a key and value to current's context's _meta
*
* @todo Migrate to use object-settings once Abstract core is ready.
* @temp
* @param key
* @param value
* @returns {string}
*/
get: function() {
return function set() {
return require( 'object-settings' ).prototype ? require( 'object-settings' ).prototype.set.apply( Abstract, arguments ) : Abstract.utility.noop();
}
},
configurable: true,
enumerable: false
},
mixin: {
/**
*
* @returns {Function}
*/
value: function( obj ) {
var target = obj || this;
Object.keys( Abstract ).forEach( function( key ) {
target[ key ] = target[ key ] || Abstract[ key ];
});
return target;
},
configurable: true,
enumerable: false
},
apply: {
/**
* Instantiate within a custom context.
*
* @params target {Object} Target object to use as the context.
* @params arguments {Object} Arguments to instantiate with.
* @returns {Function}
*/
get: function() {
return function apply( target, arguments ) {
return Abstract.prototype.mixin( target, target );
}
},
configurable: true,
enumerable: false
},
create: {
/**
* Creates a new object with the specified prototype object and properties.
* Copy the functions from the superclass prototype to the subclass prototype.
*
* @param proto {Object|null} Superclass to use as prototype for new object.
*/
value: function create( proto, properties ) {
// If first argument is a method, assume we are making a model
if( 'function' === typeof proto ) {
return Abstract.createModel( proto, properties );
}
var Instance = Object.create( proto );
Abstract.defineProperties( Instance, properties );
// Return for context
return Instance;
},
configurable: true,
writable: false,
enumerable: true
},
createModel: {
/**
* Create Model Environment
*
* This method expects a function to be passed to be used as the Model Builder.
* Within the context of the Model Builder new methods become available.
*
* Removed for now:
* Abstract.copyProperties( Model.create, Model );
* Abstract.addPrototype( Model.create, Model );
*
*/
value: function createModel( Model ) {
// console.log( 'createModel' );
// Call Constructor, pass in some arguments maybe
if( 'function' !== typeof Model ) {
return new Error( 'Abstract.createModel() requires a callable method as the first argument.' );
}
if( Model.is_constructed ) {
//return Model.create();
}
// If Instantiator already bound
if( 'function' === typeof Model.create && model.create.name === 'createInstance' ) {
return Model.create;
}
// Create dynamically-created Model Constructor context
Abstract.defineProperties( Model, {
use: {
value: Abstract.prototype.use.bind( Model ),
configurable: true,
enumerable: false,
writable: true
},
require: {
value: Abstract.prototype.require.bind( Model ),
configurable: true,
writable: true,
enumerable: false
},
get: {
value: Abstract.prototype.get.bind( Model ),
configurable: true,
enumerable: false,
writable: true
},
set: {
value: Abstract.prototype.set.bind( Model ),
configurable: true,
enumerable: false,
writable: true
},
properties: {
value: Abstract.prototype.properties.bind( Model ),
configurable: true,
enumerable: false,
writable: true
},
defineConstructor: {
value: Abstract.defineConstructor.bind( Model, Model ),
configurable: true,
enumerable: false,
writable: true
},
defineProperties: {
value: Abstract.defineProperties.bind( Model ),
configurable: true,
enumerable: false,
writable: true
},
defineProperty: {
value: Abstract.defineProperty.bind( Model ),
configurable: true,
enumerable: false,
writable: true
}
});
// Create Instance Prototype from Abstract Prototype
Model.prototype = Object.create( Abstract.prototype );
// Instantiation Method
Object.defineProperty( Model, 'create', {
value: function createInstance() { return Abstract.createInstance.apply( Model, arguments ); },
enumerable: true,
configurable: true,
writable: true
})
// Run Builder in Model context
Model.call( Model, Model, Model.prototype );
// Set custom name for creating method if available
var _custom_name = undefined;
if( Model._instantiator && Object.keys( Model._instantiator ).length ) {
Model._instantiator.forEach( function( fn ) { _custom_name = fn.name || _custom_name; });
}
// Context wrapper for instantiation
if( _custom_name && 'string' === typeof _custom_name ) {
Object.defineProperty( Model, _custom_name, {
value: Model.create,
enumerable: true,
configurable: true,
writable: true
});
// Hide "create()" since we have a cool custom name
Object.defineProperty( Model, 'create', {
enumerable: false
})
}
// Mark model as constructred
Object.defineProperty( Model, 'is_constructed', {
value: Model.name,
enumerable: false,
configurable: true,
writable: true
});
// Return constructor function
return Model;
},
configurable: true,
enumerable: true,
writable: true
},
copyProperties: {
/**
* Iterate through target's properties and reference them into the source object
*
* @param target {Object}
* @param source {Object}
* @returns {Object} Target object.
*/
value: function referenceProperties( target, source ) {
Object.getOwnPropertyNames( source ).forEach( function( key ) {
var descriptor = Object.getOwnPropertyDescriptor( source, key );
if( descriptor.enumerable ) {
Object.defineProperty( target, key, {
enumerable: true,
value: source[ key ],
writable: descriptor.writable,
configurable: descriptor.configurable
});
}
});
return target;
},
configurable: true,
enumerable: false,
writable: true
},
createInstance: {
/**
* Create Instance
*
* It's rather imperative that this method be run in context.
*
* @returns {*}
*/
value: function createInstance() {
// console.log( 'createInstance' );
var args = arguments;
// Create the instance
var Instance = Object.create( this.prototype );
// Not very elegant... @todo Revise prototype chain logic if this causes issues.
for( var key in this.__proto__ ) {
// Only check own properties of model for inheritence; all "use()'d" properties will be non-enumerable
var is_enumerable = this.propertyIsEnumerable( key );
Object.defineProperty( Instance, key, {
value: this.__proto__[key],
enumerable: is_enumerable,
writable: true,
configurable: true
});
}
// Set instance ID and Schema scaffolding
Instance.set({
id: Abstract.utility.hash( null, { random: true }),
model: this.name || 'Model',
schema: {
id: [ '#', Instance.get( 'id' ) || this.name || 'Model' ].join( '' ),
type: 'object',
properties: {}
}
});
// Call createInstance
if( this._instantiator && Object.keys( this._instantiator ).length ) {
this._instantiator.forEach( function( fn ) { fn.apply( Instance, args ); });
}
return Instance;
},
configurable: true,
enumerable: false,
writable: true
},
defineConstructor: {
/**
* Bind createInstance to Context
*
* @method defineConstructor
* @param target
* @param fn
*/
value: function defineConstructor( target, fn ) {
// console.log( 'defineConstructor' );
if( target._instantiator && Object.keys( target._instantiator ).length ) {
target._instantiator.push( 'function' === typeof fn ? fn : Abstract.utility.noop );
return target;
}
Object.defineProperty( target, '_instantiator', {
value: [ 'function' === typeof fn ? fn : Abstract.utility.noop ],
configurable: true,
enumerable: false,
writable: true
});
// Set instantiator using custom name
if( fn.name !== 'create' ) {
Object.defineProperty( target, fn.name, {
get: function() { return target.create; },
enumerable: true,
configurable: true
});
}
// Context wrapper for instantiation
return function createInstance() {
return Abstract.createInstance.apply( target, arguments );;
}
},
configurable: true,
enumerable: false,
writable: true
},
defineProperties: {
/**
* Configure multiple object properties.
*
* @param obj
* @param props
* @returns {*}
*/
value: function defineProperties( target, props ) {
if( !props && 'object' === typeof props && this.hasOwnProperty( 'defineConstructor' ) ) {
props = target;
target = this;
}
if( !props || 'object' !== typeof props ) {
return target || {};
}
Object.keys( props ).forEach( function( key ) {
Abstract.defineProperty( target, key, props[ key ] );
});
return target;
},
configurable: true,
enumerable: true,
writable: true
},
defineProperty: {
/**
* Configure single object property.
*
* @param obj
* @param key
* @param prop
* @returns {*}
*/
value: function defineProperty( obj, key, prop ) {
// @issue https://github.com/UsabilityDynamics/abstract/issues/2
if( !prop ) {
// If "obj" was omitted we assue we are binding to context
if( 'string' === typeof obj && 'object' === typeof key && this.hasOwnProperty( 'defineConstructor' ) ) {
obj = this;
key = obj;
prop = key;
} else {
prop = {};
}
}
if( !obj ) {
obj = {};
}
// Handle Abstract instances because they have a get and set method inherited form Object Settings
if( prop instanceof Abstract && prop.get && prop.set && prop._meta ) {
prop = {
value: prop,
configurable: true,
writable: true,
type: typeof prop,
enumerable: true
}
}
// General Descriptors for basic data
if( 'object' !== typeof prop || ( !prop.get && !prop.set && !prop.value ) ) {
if( prop.value ) { prop = prop.value; }
prop = {
value: prop,
configurable: true,
writable: true,
type: typeof prop,
enumerable: true
}
}
// Apply Defaults
Abstract.utility.extend( prop, this.meta ? this.meta.get( 'defaults' ) : {} );
// Store in Schema and add to Object actual
try {
Object.defineProperty( obj, key, prop );
} catch( error ) {
switch( error.type ) {
case 'redefine_disallowed':
console.error( 'Cannot redefine [%s] property %s', key, obj.name ? 'for [' + obj.name + '] model' : '' );
break;
default:
console.error( error.message );
break;
}
}
if( obj._meta && obj._meta.schema ) {
obj._meta.schema[ key ] = prop;
}
// Handle constructor property
if( key === 'constructor' ) {}
// Handle prototypal properties
if( key === 'prototype' ) {}
// Handle __proto__ property
if( key === '__proto__' ) {}
// Monitor a Property.
if( prop.hasOwnProperty( 'watch' ) ) {
// Abstract.utility.watch( prop, this.watcher ); // @todo Not sure which function to pipeline to.
}
// Wrap the property into a getter and setter
if( prop.hasOwnProperty( 'wrap' ) ) {
// @todo
}
// Add Properties to (presumably) constructor.
if( prop.hasOwnProperty( 'properties' ) ) {
Abstract.utility.each( prop.properties, function( fn, key, array ) {
if( 'function' === typeof prop.value ) {
Object.defineProperty( prop.value, key, {
value: fn,
configurable: true,
enumerable: true
});
//Abstract.use( fn, obj );
}
});
}
return obj;
},
configurable: true,
enumerable: true
},
addPrototype: {
/**
* Allow Prototype useage method to be ran in custom context for static calls
*
* @returns {Function}
*/
get: function() {
return function( context, proto ) {
return Abstract.prototype.use.call( context, proto );
}
},
configurable: false,
enumerable: false
},
getPropertyDescriptors: {
/**
* Allow Prototype useage method to be ran in custom context for static calls
*
* @returns {Function}
*/
value: function getPropertyDescriptors( target, options ) {
var _extend = require( 'util' )._extend;
options = _extend({
include_inherited: false,
default_descriptor: {
writable: true,
enumerable: true,
configurable: true
}
}, options );
var response = {}
for( var key in target ) {
if( Object.getOwnPropertyDescriptor( target, key ) ) {
response[ key ] = Object.getOwnPropertyDescriptor( target, key );
} else {
if( options.include_inherited ) {
response[ key ] = _extend( options.default_descriptor, { value: target[key] });
}
}
}
return response;
},
configurable: false,
enumerable: false
},
extendPrototype: {
/**
* Allow Prototype useage method to be ran in custom context for static calls
*
* @returns {Function}
*/
value: function extendPrototype() {
var result = {};
var list = arguments;
Object.keys( arguments ).forEach( function( index ) {
//Abstract.utility.extend( result, list[index] );
//Abstract.utility.extend( result, Object.create( list[index] ) );
if( index == 0 ) {
result.__proto__ = list[index];
} else {
var depth = result;
for( i=1; i<=index; i++ ) {
depth = depth.__proto__;
}
depth.__proto__ = list[index];
}
});
// Get immediate prototype
return Abstract.getPrototypeOf( result );
},
configurable: false,
enumerable: false,
writable: true
},
getPrototypeOf: {
/**
* Cross Browser Compatible prototype getter.
*
* @param obj
*/
value: function getPrototypeOf( obj ) {
if( Object.getPrototypeOf ) {
return Object.getPrototypeOf( obj ) || undefined;
} else if( obj.__proto__ ) {
return obj.__proto__ || undefined;
} else if( obj.constructor.prototype ) {
return constructor.prototype || undefined;
} else {
return undefined;
}
},
configurable: true,
enumerable: false,
writable: true
},
setPrototypeOf: {
/**
* Basic Wrapper - will be more fault-tolerant in future.
*
* @param obj
* @param proto
*/
value: function setPrototypeOf( obj, proto ) {
if( Object.setPrototypeOf ) {
Object.setPrototypeOf( obj, proto );
} else if( obj.__proto__ ) {
obj.__proto__ = obj;
}
return obj;
},
configurable: true,
enumerable: false,
writable: true
},
utility: {
value: require( './utility' ),
configurable: true,
enumerable: false,
writable: true
},
version: {
value: require( '../package' ).version,
enumerable: false,
writable: false,
configurable: false
}
});
/**
* Instance Properties
*
* The follow properties are available to each instance created from a constructor.
* Some of the Abstract Static methods reference the prototypal methods.
* Prototyal methods all work with the existing context.
*
*/
Object.defineProperties( Abstract.prototype, {
get: {
/**
* Get a key from current context's _meta
*
* @todo Migrate to use object-settings once Abstract core is ready.
* @temp
* @param key
* @returns {string}
*/
get: function() {
return function get() {
return require( 'object-settings' ).prototype ? require( 'object-settings' ).prototype.get.apply( this, arguments ) : Abstract.utility.noop();
}
},
configurable: true,
enumerable: true
},
set: {
/**
* Set a key and value to current's context's _meta
*
* @todo Migrate to use object-settings once Abstract core is ready.
* @temp
* @param key
* @param value
* @returns {string}
*/
get: function() {
return function set( ) {
return require( 'object-settings' ).prototype ? require( 'object-settings' ).prototype.set.apply( this, arguments ) : Abstract.utility.noop();
}
},
configurable: true,
enumerable: true
},
use: {
/**
* Inserts an object/prototype into a target object.
*
* @todo Try using in some instances.
* require( 'util' ).inherits( this, source );
*
* @param target
* @param proto
* @returns {*}
*/
value: function use( source ) {
// @todo Implement to mock util.inherts()
/*
Object.defineProperty( 'super_', {
value: source,
enumerable: false,
configurable: false,
writable: true
});
*/
// Check if already required to prevent Cyclic __proto__ error.
if( 'function' === typeof source && source.name && this.get( 'required.' + source.name ) ) {
return this;
}
try {
// Move current immediate prototype object into new protototype object
source.__proto__ = this.__proto__;
// Insert new prototype into chain.
this.__proto__ = source;
// Note required
if( 'function' === typeof source && source.name ) {
this.set( 'required.' + source.name, true );
}
} catch( error ) {
//console.error( error.message );
if( error.message === 'Cyclic __proto__ value' ) {}
}
return this;
},
configurable: true,
enumerable: true,
writable: true
},
mixin: {
/**
* Mixin current prototype into target object
*
* Honors property descriptor settings, if available.
*
* @param target
* @param source
*/
get: function() {
return require( './utility' ).mixin.bind( this, this );
},
configurable: true,
enumerable: false
},
require: {
/**
* Requires and inserts an object/prototype into this context object.
*
* @example
*
* require( 'abstract' ).createModel( function MyAbstraction( model ) {
*
* this.require( 'util' );
* this.require( 'async' );
*
* this.auto({}); // -> [Function]
*
* });
*
* @todo Try using in some instances.
* require( 'util' ).inherits( this, source );
*
* @param name {String} Module name or file path to require.
* @returns {*}
*/
value: function use( name ) {
if( 'string' === typeof name ) {
try {
// Resolve module
require.resolve( name );
// Check if already required to prevent Cyclic __proto__ error.
if( this.get( 'required.' + name ) ) {
return this;
}
var source = require( name );
} catch( error ) { var source = null }
}
if( !source ) {
return this;
}
try {
// Move current immediate prototype object into new protototype object
source.__proto__ = this.__proto__;
// Insert new prototype into chain.
this.__proto__ = source;
// Add meta note about required source
this.set( 'required.' + name, true );
} catch( error ) { console.error( error.message ); }
return this;
},
configurable: true,
enumerable: true,
writable: true
},
properties: {
/**
* Get / Set Properties
*
* @returns {Function}
*/
get: function() {
return function properties( properties ) {
if( arguments.length === 1 ) {
return Abstract.defineProperties( this, arguments[0] );
} else {
arguments[0] = arguments[0] || {};
return Abstract.defineProperties( arguments[0], arguments[1] );
}
}
},
configurable: true,
enumerable: false
}
});