joii
Version:
Javascript Object Inheritance Implementation
578 lines (510 loc) • 16.7 kB
JavaScript
/* Javascript Object Inheritance Implementation ______ ________
* (c) 2016 <harold@iedema.me> __ / / __ \/ _/ _/
* Licensed under MIT. / // / /_/ // /_/ /
* ------------------------------------------------------ \___/\____/___/__*/
// Register JOII 'namespace'.
JOII = typeof (JOII) !== 'undefined' ? JOII : {};
JOII.Reflection = {};
/**
* ReflectionClass
*
* Retrieves and presents meta information about the given class.
*
* API / Usage:
* var r = new JOII.Reflection.Class(MyClass);
* r.getMethods(); - Returns array of JOII.Reflection.Method
* r.getMethod(name); - Returns JOII.Reflection.Method
* r.getProperties(); - Returns array of JOII.Reflection.Property
* r.getProperty(name); - Returns JOII.Reflection.Property
* r.isFinal() - Returns true if the class is final
* r.hasParent() - Returns true if the class has a parent
* r.getParent() - Returns JOII.Reflection.Class of the parent
*/
JOII.Reflection.Class = JOII.ClassBuilder({}, {
/**
* Contains the __joii__ metadata object.
*
* @var object
*/
'protected immutable object meta': null,
/**
* Contains the prototype of the class.
*
* @var object
*/
'protected immutable object proto': null,
/**
* Represents the Reflection.Class instance of the parent definition.
*
* @var JOII.Reflection.Class
*/
'public immutable object parent': null,
/**
* Constructor
*
* @param {Function} definition
*/
'protected __construct': function(definition) {
if (typeof (definition) === 'function') {
definition = definition.prototype;
}
// Is the passed argument an actual JOII class?
if (typeof (definition) !== 'object' ||
typeof (definition.__joii__) !== 'object') {
throw 'Reflection.Class requires a JOII-created definition.';
}
this.proto = definition;
this.meta = definition.__joii__;
// Does the class definition have a parent?
if (typeof (this.meta.parent) !== 'undefined') {
this.parent = new JOII.Reflection.Class(this.meta.parent);
}
},
/**
* Returns the name of the class.
*
* @return string
*/
'public getName': function() {
return this.meta.name;
},
/**
* Returns true if the class is marked as abstract.
*
* @return bool
*/
'public isAbstract': function() {
return this.meta.is_abstract === true;
},
/**
* Returns true if the property is static.
*
* @return bool
*/
'public isStatic': function() {
return this.meta.is_static;
},
/**
* Returns true if a property by the given name exists.
*
* @return bool
*/
'public hasProperty': function(name) {
var list = this.getProperties();
for (var i in list) {
if (list[i].getName() === name) {
return true;
}
}
return false;
},
/**
* Returns true if the class being reflected has a parent class.
*
* @return bool
*/
'public hasParent': function() {
return this.parent !== null;
},
/**
* Returns the reflector of the parent class.
*
* @return JOII.Reflection.Class
*/
'public getParent': function() {
return this.parent;
},
/**
* Returns an array of JOII.Reflection.Method based on the methods
* defined in this class.
*
* @param string filter Optional filter for 'private' or 'public'.
* @return JOII.Reflection.Method[]
*/
'public getMethods': function(filter) {
var result = [];
for (var i in this.proto) {
if (typeof (this.proto[i]) === 'function' && JOII.Compat.indexOf(JOII.InternalPropertyNames, i) === -1) {
result.push(new JOII.Reflection.Method(this, i));
}
}
return result;
},
/**
* Returns true if a method by the given name exists.
*
* @return bool
*/
'public hasMethod': function(name) {
var list = this.getMethods();
for (var i in list) {
if (list[i].getName() === name) {
return true;
}
}
return false;
},
/**
* Returns an instance of JOII.Reflection.Method of a method by the
* given name.
*
* @param string name
* @return JOII.Reflection.Method
*/
'public getMethod': function(name) {
var list = this.getMethods();
for (var i in list) {
if (list[i].getName() === name) {
return list[i];
}
}
throw 'Method "' + name + '" does not exist.';
},
/**
* Returns an array of JOII.Reflection.Property based on the properties
* defined in this class.
*
* @param string filter Optional filter for 'private' or 'public'.
* @return JOII.Reflection.Property[]
*/
'public getProperties': function(filter) {
var result = [];
for (var i in this.proto) {
if (typeof (this.proto[i]) !== 'function' && JOII.Compat.indexOf(JOII.InternalPropertyNames, i) === -1) {
result.push(new JOII.Reflection.Property(this, i));
}
}
return result;
},
/**
* Returns an instance of JOII.Reflection.Property of a property by the
* given name.
*
* @param string name
* @return JOII.Reflection.Property
*/
'public getProperty': function(name) {
var list = this.getProperties();
for (var i in list) {
if (list[i].getName() === name) {
return list[i];
}
}
throw 'Property "' + name + '" does not exist.';
}
});
/**
* Defines a property declared in a JOII class and provides meta
* information about it.
*/
JOII.Reflection.Property = JOII.ClassBuilder({}, {
/**
* Represents the reflector of the owning class.
*
* @var JOII.Reflection.Class
*/
'protected nullable object reflector': null,
/**
* Represents the metadata of this property.
*
* @var object
*/
'protected nullable object meta': null,
/**
* Represents the name of the property.
*
* @var string
*/
'public read string name': null,
/**
* Constructor.
*
* @param JOII.Reflection.Class reflector
* @param string property_name
*/
'protected __construct': function(reflector, property_name) {
this.reflector = reflector;
this.name = property_name;
this.meta = reflector.getMeta().metadata[property_name];
// If we, for some strange reason don't have metadata, fill it in
// with some default values.
if (typeof (this.meta) === 'undefined') {
this.meta = {
name : this.name,
type : null,
visibility : 'public',
is_nullable : false,
is_abstract : false,
is_read_only: false,
is_final : false
};
}
// Attempt to fetch the type by fetching the predefined value.
// However, only do this for non-nullable types to avoid type
// mismatching exceptions in setters.
if (this.meta.type === null && this.meta.is_nullable === false) {
this.meta.type = typeof (this.reflector.getProto()[this.meta.name]);
}
},
/**
* Returns the type of the property.
*
* @return string
*/
'public getType': function() {
return this.meta.type;
},
/**
* Returns true if the property is abstract.
*
* @return bool
*/
'public isAbstract': function() {
return this.meta.is_abstract;
},
/**
* Returns true if the property is static.
*
* @return bool
*/
'public isStatic': function() {
return this.meta.is_static;
},
/**
* Returns true if the property is nullable.
*
* @return bool
*/
'public isNullable': function() {
return this.meta.is_nullable;
},
/**
* Returns true if the property is final.
*
* @return bool
*/
'public isFinal': function() {
return this.meta.is_final;
},
/**
* Returns true if the property is private.
*
* @return bool
*/
'public isPrivate': function() {
return this.meta.visibility === 'private';
},
/**
* Returns true if the property is protected.
*
* @return bool
*/
'public isProtected': function() {
return this.meta.visibility === 'protected';
},
/**
* Returns true if the property is public.
*
* @return bool
*/
'public isPublic': function() {
return this.meta.visibility === 'public';
},
/**
* Returns true if the property is public.
*
* @return bool
*/
'public isImmutable': function() {
return this.meta.is_read_only;
},
/**
* Returns true if the property is a constant.
*
* @return bool
*/
'public isConstant': function() {
return this.meta.is_constant;
},
/**
* Returns true if the given type matches the type of this property.
*
* @param string type
* @return bool
*/
'public isType': function(type) {
return type === this.meta.type;
},
/**
* Returns the visibility of the property as a string.
*
* @return string
*/
'public getVisibility': function() {
return this.meta.visibility;
},
/**
* Returns a string representation of this object.
*
* @return string
*/
toString: function() {
var name_parts = [],
proto_ref = this.reflector.getProto()[this.name],
name = '',
body = '';
if (this.meta.is_abstract) { name_parts.push('abstract'); }
if (this.meta.is_final) { name_parts.push('final'); }
name_parts.push(this.meta.visibility);
if (this.meta.is_static) { name_parts.push('static'); }
if (this.meta.is_nullable) { name_parts.push('nullable'); }
if (this.meta.is_read_only) { name_parts.push('read'); }
// If type === null, attempt to detect it by the predefined value.
if (this.meta.type === null) {
if (proto_ref === null) {
name_parts.push('mixed');
} else {
name_parts.push(typeof (proto_ref));
}
} else {
name_parts.push(this.meta.type);
}
name_parts.push('"' + this.meta.name + '"');
name = name_parts.join(' ');
if (typeof (proto_ref) === 'function') {
body = '[Function]';
} else if (typeof (proto_ref) === 'object' && proto_ref !== null) {
body = '[Object (' + proto_ref.length + ')]';
} else if (typeof (proto_ref) === 'string') {
body = '"' + proto_ref + '"';
} else {
body = proto_ref;
}
return name + ': ' + body;
}
});
JOII.Reflection.Method = JOII.ClassBuilder({ 'extends': JOII.Reflection.Property }, {
/**
* Returns an array of strings based on the parameters defined in
* the declared function.
*
* @return string[]
*/
'public getParameters': function() {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
FN_ARG_SPLIT = /,/,
FN_ARG = /^\s*(_?)(\S+?)\1\s*$/,
STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
getParams = function(fn) {
var fnText, argDecl;
var args = [];
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
var r = argDecl[1].split(FN_ARG_SPLIT), repl = function(all, underscore, name) {
args.push(name);
};
for (var a in r) {
var arg = r[a];
arg.replace(FN_ARG, repl);
}
return args;
};
var prototype = this.reflector.getProto();
var overloads = prototype.__joii__.metadata[this.name].overloads;
if (!overloads || overloads.length === 0) {
// old method for BC (wasn't recognized as a function when prototyping)
return getParams(this.reflector.getProto()[this.name]);
} else if (overloads.length === 1 && overloads[0].parameters.length === 0) {
// old method for BC (was recognized when prototyping, but old style)
return getParams(overloads[0].fn);
}
else {
var ret = [];
for (var idx = 0; idx < overloads.length; idx++) {
var fn_meta = [];
var function_parameters_meta = overloads[idx];
var parsed_params = getParams(function_parameters_meta.fn);
for (var j = 0; j < function_parameters_meta.parameters.length; j++) {
var param = {
name: parsed_params.length > j ? parsed_params[j] : null,
type: function_parameters_meta.parameters[j]
};
fn_meta.push(param);
}
ret.push(fn_meta);
}
return ret;
}
},
/**
* Returns the body of this method as a string.
*
* @return string
*/
'public getBodyAsString': function(f) {
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
fn_text = this.reflector.getProto()[this.name].toString().replace(STRIP_COMMENTS, '');
return fn_text.substr(fn_text.indexOf('{') + 1, fn_text.lastIndexOf('}') - 4).replace(/}([^}]*)$/, '$1');
},
/**
* Returns true if the function body contains "arguments", making
* a _guess_ the function uses variadic arguments.
*
* This is only used in toString() to show an indication of the
* function signature. Do NOT rely on this in functional code!!
*
* @return bool
*/
'public usesVariadicArguments': function() {
var data = this.reflector.getProto()[this.name].toString();
return data.match(/[\(|\.|\ ](arguments)[\)|\.|\,|\ |]/g);
},
/**
* Returns a string representation of the method.
*
* @return string
*/
toString: function() {
// Get the "declaration" part of the method.
var prefix = this['super']('toString').split(':')[0],
body = '[Function',
args = this.getParameters(),
is_var = this.usesVariadicArguments();
if (args.length > 0 && typeof (args[0]) === 'object') {
// right now, this is spitting out every overload's signature one after another, each on a new line.
// should probably find a better way to do this
for (var idx = 0; idx < args.length; idx++) {
var function_parameters_meta = args[idx];
body += ' (';
var first_time = true;
for (var i = 0; i < function_parameters_meta.length; i++) {
if (!first_time) {
body += ', ';
}
first_time = false;
body += function_parameters_meta[i].type;
if (function_parameters_meta[i].name !== null) {
body += " " + function_parameters_meta[i].name;
is_var = true;
}
}
var data = this.reflector.getProto().__joii__.metadata[this.name].overloads[idx].fn.toString();
is_var = data.match(/[\(|\.|\ ](arguments)[\)|\.|\,|\ |]/g);
if (is_var) {
body += ', ...';
}
body += ')\n';
}
} else if (args.length > 0) {
body += ' (' + args.join(', ');
if (is_var) {
body += ', ...';
}
body += ')';
} else if (args.length === 0 && is_var) {
body += ' (...)';
}
body += ']';
return prefix + ': ' + body;
}
});