UNPKG

joii

Version:

Javascript Object Inheritance Implementation

1,111 lines (947 loc) 47.7 kB
/* Javascript Object Inheritance Implementation ______ ________ * (c) 2016 <harold@iedema.me> __ / / __ \/ _/ _/ * Licensed under MIT. / // / /_/ // /_/ / * ------------------------------------------------------ \___/\____/___/__*/ JOII = typeof (JOII) !== 'undefined' ? JOII : {}; JOII.InternalPropertyNames = ['__joii__', 'super', 'instanceOf', 'deserialize', 'serialize', 'getStatic', '__api__']; JOII.InternalTypeNames = [ 'undefined', 'object', 'boolean', 'number' , 'string', 'symbol', 'function' , 'const' ]; /** * The PrototypeBuilder is responsible for creating a prototype of the * final 'class'- or 'interface'-type. * * Parameters can consist of one or more of the following: * 'extends' : <class-type> Inherit a parent type * 'implements' : <array of interface-type> * * @param {String} name * @param {Object} parameters * @param {Object} body * @param {Boolean} is_interface * @return {Object} */ JOII.PrototypeBuilder = function(name, parameters, body, is_interface, is_static_generated) { is_static_generated = (is_static_generated === true); // Create a clean prototype of the class body. var prototype = {}, deep_copy = JOII.Compat.extend(true, {}, body); // Create the internal JOII-object. JOII.CreateProperty(prototype, '__joii__', { name : name, parent : undefined, metadata : {}, constants : {}, implementations : [name], is_abstract : parameters.abstract === true, is_final : parameters.final === true, is_static : parameters['static'] === true || is_static_generated }); // Apply traits / mix-ins if (typeof (parameters.uses) !== 'undefined') { var traits = JOII.Compat.flexibleArgumentToArray(parameters.uses); for (var t in traits) { deep_copy = JOII.Compat.extend(true, deep_copy, traits[t]); } } if (prototype.__joii__.is_abstract && prototype.__joii__.is_final) { throw 'A class cannot be both abstract and final simultaniously.'; } // Iterate over properties from the deep_copy, get the metadata of the // property and move them in the prototype. for (var i in deep_copy) { if (deep_copy.hasOwnProperty(i) === false) continue; var meta = JOII.ParseClassProperty(i, name); // make sure this prototype only has members that match it's static state if (prototype.__joii__['is_static'] !== true && meta.is_static && !is_interface) continue; if (prototype.__joii__['is_static'] && !meta.is_static) { if (is_static_generated) { continue; } else { throw 'Member ' + meta.name + ' is non-static. A static class cannot contain non-static members.'; } } if (typeof (deep_copy[i]) === 'function' || meta.parameters.length > 0 || (meta.name in prototype.__joii__.metadata && 'overloads' in prototype.__joii__.metadata[meta.name])) { if (typeof (deep_copy[i]) !== 'function') { if (meta.parameters.length > 0) { throw 'Member ' + meta.name + ' specifies parameters, but it\'s value isn\'t a function.'; } else { throw 'Member ' + meta.name + ' overloads an existing function, but it\'s value isn\'t a function.'; } } if (meta.name in prototype && typeof (prototype[meta.name]) !== 'function') { throw 'Member ' + meta.name + ' overloads an existing property, but the previous property isn\'t a function.'; } meta.class_name = name; JOII.addFunctionToPrototype(prototype, meta, deep_copy[i]); } else if (meta.is_constant) { prototype.__joii__.constants[meta.name] = deep_copy[i]; JOII.CreateProperty(prototype, meta.name, deep_copy[i], false); meta.class_name = name; prototype.__joii__.metadata[meta.name] = meta; } else { prototype[meta.name] = deep_copy[i]; meta.class_name = name; prototype.__joii__.metadata[meta.name] = meta; } // Don't create getters and setters if we are an interface. if (is_interface === true) { continue; } } if (typeof (parameters.abstract) !== 'undefined') { prototype.__joii__.is_abstract = true; if (is_interface) { throw 'An interface cannot be declared abstract.'; } } if (prototype.__joii__.is_static && is_interface) { throw 'An interface cannot be declared static.'; } // Create getters and setters for properties. We do this _after_ the // copying of the parent object because that prototype doesn't contain // the getter/setter methods yet. (Fixes issue #10) // NOTE: The comment implies that this will apply getters/setters for inherited properties // however, it's only looping over "deep_copy", which only contains information from this class // it doesn't contain parent properties... // Moved back above the parent implementations, so that the getter/setters for this class // take priority over inherited (applies to static overloaded accessor methods) for (var i in deep_copy) { if (deep_copy.hasOwnProperty(i) === false) continue; var meta = JOII.ParseClassProperty(i, name); if (typeof (deep_copy[i]) === 'function' || meta.parameters.length > 0 || 'overloads' in meta) { continue; } // make sure this prototype only has members that match it's static state if (prototype.__joii__['is_static'] !== true && meta.is_static && !is_interface) continue; if (prototype.__joii__['is_static'] && !meta.is_static) { if (is_static_generated) { continue; } else { throw 'Member ' + meta.name + 'is non-static. A static class cannot contain non-static members.'; } } // Generate getters and setters if we're not dealing with anything // that is a function or declared private. if (meta.visibility !== 'private') { var gs = JOII.CreatePropertyGetterSetter(deep_copy, meta, name); gs.getter.meta.class_name = name; if (typeof (prototype.__joii__.metadata[gs.getter.name]) == 'undefined' || !prototype.__joii__.metadata[gs.getter.name].has_parameterless) { JOII.addFunctionToPrototype(prototype, gs.getter.meta, gs.getter.fn, true); } //prototype[gs.getter.name] = gs.getter.fn; //prototype.__joii__.metadata[gs.getter.name] = gs.getter.meta; if (typeof (gs.setter.meta) !== 'undefined') { gs.setter.meta.class_name = name; if (typeof (prototype.__joii__.metadata[gs.getter.name]) == 'undefined' || !prototype.__joii__.metadata[gs.getter.name].has_parameterless) { JOII.addFunctionToPrototype(prototype, gs.setter.meta, gs.setter.fn, true); } //prototype[gs.setter.name] = gs.setter.fn; //prototype.__joii__.metadata[gs.setter.name] = gs.setter.meta; } } } // Apply the parent prototype. if (typeof (parameters['extends']) !== 'undefined') { var parent = parameters['extends']; // If the given parent is a function, use its prototype. if (typeof (parent) === 'function') { if (is_static_generated) { parent = parent.__joii__.prototype; } else { parent = parent.prototype; } } // Only Object-types can be used as a parent object. if (typeof (parent) !== 'object' && !is_static_generated) { throw (is_interface ? 'An interface' : 'A class') + ' may only extend on functions or object-types.'; } // Create a parent property in the prototype which contains a deep- // copy of the prototype of the given parent. prototype.__joii__.parent = JOII.Compat.extend(true, {}, parent); // If the parent is final, it cannot be extended upon. if (parent.__joii__.is_final === true) { throw 'Unable to extend on a final class.'; } // Iterate over parent classes and apply the implementations for the instanceOf verifications. var current = prototype.__joii__.parent; while (typeof current !== 'undefined') { prototype.__joii__.implementations.push(current.__joii__.name); // Move to the next underlying class. current = current.__joii__.parent; } // Clone the constants of the parent into this one. prototype.__joii__.constants = JOII.Compat.extend(true, prototype.__joii__.constants, parent.__joii__.constants); // The __joii__ property is usually hidden and not enumerable, so we // need to re-create it ourselves. JOII.CreateProperty(prototype.__joii__.parent, '__joii__', (parent.__joii__)); // Iterate over the properties of the parent object and apply the // contents in our own prototype where applicable. for (i in prototype.__joii__.parent) { // We're only interested in properties that really belong to // the object. So we'll skip any inherited things from the // native JavaScript's "Object". if (!prototype.__joii__.parent.hasOwnProperty(i)) { continue; } // If the property is an internal method, skip it. if (JOII.Compat.indexOf(JOII.InternalPropertyNames, i) !== -1) { continue; } var property = prototype.__joii__.parent[i]; var property_meta = prototype.__joii__.parent.__joii__.metadata[i]; var proto_meta = prototype.__joii__.metadata[i]; // make sure this prototype only has members that match it's static state if (prototype.__joii__['is_static'] !== true && property_meta.is_static && !is_interface) continue; if (prototype.__joii__['is_static'] && !property_meta.is_static) { if (is_static_generated) { continue; } else { throw 'Member ' + property_meta.name + 'is non-static. A static class cannot contain non-static members.'; } } if (typeof (proto_meta) === 'undefined') { proto_meta = prototype.__joii__.metadata[i] = JOII.Compat.extend(true, {}, property_meta); proto_meta.has_parameterless = false; if ('overloads' in proto_meta) { delete proto_meta.overloads; } } // If another property with the same name already exists within // our own prototype, skip its inherited implementation. if (typeof (prototype[i]) !== 'undefined' && typeof (property_meta) === 'object' && typeof (proto_meta) === 'object') { if (proto_meta.is_generated === false) { // Check for visibility change. if (property_meta.visibility !== proto_meta.visibility) { throw 'Member "' + i + '" must be ' + property_meta.visibility + ' as defined in the parent ' + (is_interface ? 'interface' : 'class') + '.'; } if (typeof (property) === 'function' || property_meta.parameters.length > 0 || 'overloads' in proto_meta || 'overloads' in property_meta) { // if it's a function, don't check the following. // is_final is checked when adding the function later // read_only and nullable don't apply to functions } else { // Check final properties. if (property_meta.is_final === true) { throw 'Final member "' + i + '" cannot be overwritten.'; } // Is the property read-only? if (property_meta.is_read_only !== proto_meta.is_read_only) { throw 'Member "' + i + '" must be read-only as defined in the parent ' + (is_interface ? 'interface' : 'class') + '.'; } // Is the property nullable? if (property_meta.is_nullable !== proto_meta.is_nullable) { throw 'Member "' + i + '" must be nullable as defined in the parent ' + (is_interface ? 'interface' : 'class') + '.'; } } } if (typeof (property) === 'function' || property_meta.parameters.length > 0 || 'overloads' in proto_meta || 'overloads' in property_meta) { // if it's a function, we still want to proceed } else { continue; } } if (typeof (property) === 'function' || property_meta.parameters.length > 0 || 'overloads' in proto_meta || 'overloads' in property_meta) { // if it's a function, we still want to proceed } else { // It's safe to apply non-function properties immediatly. if (typeof (property) !== 'function' || is_interface === true) { if (!property_meta.is_static) { prototype[i] = property; } // if it's static, we need to reference the original functions, since static members stay with the class they're defined in if (property_meta.is_static) { var gs = JOII.CreatePropertyGetterSetter(prototype, property_meta, name); gs.getter.fn = Function('\ var args = ["' + gs.getter.name + '"];\ for (var i in arguments) { args.push(arguments[i]); }\ return this[\'super\'].apply(this, args);\ '); gs.setter.fn = Function('\ var args = ["' + gs.setter.name + '"];\ for (var i in arguments) { args.push(arguments[i]); }\ return this[\'super\'].apply(this, args);\ '); if (typeof gs.getter.meta !== 'undefined') { gs.getter.meta.is_generated = true; gs.getter.meta.is_inherited = true; JOII.addFunctionToPrototype(prototype, gs.getter.meta, gs.getter.fn, true); //prototype[gs.getter.name] = gs.getter.fn; //prototype.__joii__.metadata[gs.getter.name] = gs.getter.meta; } if (typeof gs.setter.meta !== 'undefined') { gs.setter.meta.is_generated = true; gs.setter.meta.is_inherited = true; JOII.addFunctionToPrototype(prototype, gs.setter.meta, gs.setter.fn, true); //prototype[gs.setter.name] = gs.setter.fn; //prototype.__joii__.metadata[gs.setter.name] = gs.setter.meta; } } continue; } } // From this point on, the 'property' variable only contains // functions. This is where the funny business starts. Instead // of simply copying the 'function' into our own prototype, // we'll create our own function which calls the function from // the parent object. (Fixes issue #9) // The function "super" is implemented from the ClassBuilder. var generated_fn = null; if (property_meta.is_static) { generated_fn = function (prop_name, parent_name) { return function() { return inner_static_objects[parent_name][prop_name].apply(inner_static_objects[parent_name], arguments); }; }(i, property_meta.class_name); } else { generated_fn = Function('\ var args = ["' + i + '"];\ for (var i in arguments) { args.push(arguments[i]); }\ return this[\'super\'].apply(this, args);\ '); } var tmp_meta = JOII.Compat.extend(true, {}, property_meta); delete tmp_meta.overloads; tmp_meta.is_inherited = true; tmp_meta.has_parameterless = false; if (proto_meta.has_parameterless) { if (property_meta.is_final) { throw 'Final member "' + property_meta.name + '(' + property_meta.parameters.join(', ') + ')" cannot be overwritten.'; } } else { if (typeof (property) === 'function' || property_meta.parameters.length > 0 || 'overloads' in proto_meta || 'overloads' in property_meta) { if ('overloads' in property_meta && typeof (property_meta.overloads) === 'object' && property_meta.overloads.length > 1) { // parent has multiple overloads specified. Loop through them, and apply each. for (var idx = 0; idx < property_meta.overloads.length; idx++) { tmp_meta.parameters = property_meta.overloads[idx].parameters; tmp_meta.is_abstract = property_meta.overloads[idx].is_abstract; tmp_meta.has_parameters = property_meta.overloads[idx].has_parameters; tmp_meta.is_final = property_meta.overloads[idx].is_final; JOII.addFunctionToPrototype(prototype, tmp_meta, generated_fn, true); } } else { JOII.addFunctionToPrototype(prototype, tmp_meta, generated_fn, true); } } else { prototype[i] = generated_fn; } } } } if (is_interface !== true) { /** * Calls a method from the parent prototype (if it exists). * * @param {String} method * @return {*} */ prototype['super'] = function(method) { var args = Array.prototype.slice.call(arguments, 1), current_scope = this, original_prop = this.__joii__, call = function(scope, method, args) { if (typeof (scope) === 'undefined') { throw new Error('Parent method "' + method + '" does not exist.'); } if (typeof (scope.__joii__.parent) !== 'undefined' && typeof (scope.__joii__.parent[method]) === 'undefined') { return call(scope.__joii__.parent, method, args); } var parent = scope.__joii__.parent; if (typeof (scope.__joii__.parent) === 'undefined') { if (typeof (scope.__api__.__joii__.parent) !== 'undefined') { parent = scope.__api__.__joii__.parent; } else { throw new Error('Method "' + method + '" does not exist in the parent class. (called using \'super()\')'); } } var m = parent[method]; current_scope.__joii__ = parent.__joii__; var r = m.apply(current_scope, args); current_scope.__joii__ = original_prop; return r; }; return call(this, method, args); }; /** * Tests if the prototype implements an interface of the given name. * * @param {String} name * @return {Boolean} */ prototype.instanceOf = function(name) { // Find the JOII scope of the given object. if (typeof (name) === 'function') { name = name.prototype.__joii__.name; } else if (typeof (name) === 'object') { name = name.__joii__.name; } // Match against defined interfaces implemented in the class. var interfaces = this.__joii__.getInterfaces(); for (var i in interfaces) { if (interfaces.hasOwnProperty(i) === false) continue; if (interfaces[i].prototype.__joii__.name === name) { return true; } if (JOII.Compat.indexOf(interfaces[i].prototype.__joii__.implementations, name) !== -1) { return true; } } if (this.__joii__.name !== name) { // Attempt to validate by parent. if (typeof (this.__joii__.parent) !== 'undefined') { // Temporarily bind instanceOf to the parent scope. var cur_scope = this; var par_scope = this.__joii__.parent; JOII.Compat.Bind(par_scope.instanceOf, par_scope); var result = par_scope.instanceOf(name); // Restore the scope and return the result. JOII.Compat.Bind(par_scope.instanceOf, cur_scope); return result; } return false; } return true; }; } return prototype; }; /** * Parses a class property name and returns an object of property * metadata such as 'final', 'abstract', 'protected', etc. * * @param {String} str * @return {Object} */ JOII.ParseClassProperty = function(str, currently_defining) { // Parse the given string and set some defaults. var function_parameters = (/\(.*\)/).exec(str.toString()); var has_parameters = false; if (function_parameters !== null) { has_parameters = true; function_parameters = function_parameters[0].match(/[^\(,\s\)]+/g); } if (typeof (function_parameters) != 'object' || function_parameters === null) { function_parameters = []; } var data = str.toString().replace(/\s?\(.*\)\s?|^\s+|\s+(?=\s)|\s+$/g, '').split(/\s/), name = data[data.length - 1], types = JOII.InternalTypeNames, explicit_serialize = false, metadata = { 'name' : name, 'type' : null, // Allow all types by default. 'visibility' : 'public', // Can be one of: public, protected, private. 'is_abstract' : false, // Force implementation in child. 'is_final' : false, // Disallow implementation in child. 'is_nullable' : false, // Allow "null" or "undefined" in properties. 'is_read_only' : false, // Don't generate a setter for the property. 'is_constant' : false, // Is the property publicly accessible? 'is_static' : false, // Is the property static? 'is_enum' : false, // Is the property an enumerator? 'is_generated' : false, // Is the property generated? 'is_inherited' : false, // Is the property inherited? 'is_joii_object': false, // Does this represent a joii class/interface ? 'serializable' : false, // Is the property serializable? 'has_parameters': has_parameters, 'parameters' : function_parameters }, i; for (var c in JOII.Config.callables) { if (JOII.Config.callables[c] == name) { metadata.is_static = true; } } // Remove the name from the list. data.pop(); // If there are no flags set, simply return the defaults. if (data.length === 0) { return metadata; } var raw_data = []; // Make sure all property flags are lowercase. We don't use Array.map // for this because Internet Explorer 8 (and below) doesn't know it. for (i in data) { if (typeof (JOII.InterfaceRegistry[data[i]]) === 'undefined' && typeof (JOII.ClassRegistry[data[i]]) === 'undefined') { raw_data[i] = data[i].toString(); data[i] = raw_data[i].toLowerCase(); } } // Shorthand for validating other flags within the same declaration. // If args exists in data, msg is thrown. var metaHas = function(args, data, msg) { if (typeof (args) !== 'object') { args = [args]; } for (var i in args) { if (args.hasOwnProperty(i) === false) continue; if (JOII.Compat.indexOf(data, args[i]) !== -1) { throw msg; } } }; for (i in data) { switch (data[i]) { case 'public': metaHas('protected', data, 'Property "' + name + '" cannot be both public and protected at the same time.'); metaHas('private', data, 'Property "' + name + '" cannot be both public and private at the same time.'); metadata.visibility = 'public'; if (!explicit_serialize) { metadata.serializable = true; } break; case 'protected': metaHas('public', data, 'Property "' + name + '" cannot be both protected and public at the same time.'); metaHas('private', data, 'Property "' + name + '" cannot be both protected and private at the same time.'); metadata.visibility = 'protected'; break; case 'private': metaHas('public', data, 'Property "' + name + '" cannot be both private and public at the same time.'); metaHas('protected', data, 'Property "' + name + '" cannot be both private and protected at the same time.'); metadata.visibility = 'private'; break; case 'abstract': metaHas('final', data, 'Property "' + name + '" cannot be both abstract and final at the same time.'); metadata.is_abstract = true; break; case 'final': metaHas('abstract', data, 'Property "' + name + '" cannot be both abstract and final at the same time.'); metadata.is_final = true; break; case 'nullable': metadata.is_nullable = true; break; case 'static': metadata.is_static = true; break; case 'read': case 'immutable': metadata.is_read_only = true; break; case 'serializable': metadata.serializable = true; explicit_serialize = true; break; case 'notserializable': metadata.serializable = false; explicit_serialize = true; break; case 'const': metaHas(['private', 'protected', 'public'], data, 'A constant cannot have visibility modifiers.'); metaHas('final', data, 'A constant cannot be final.'); metaHas('abstract', data, 'A constant cannot be abstract.'); metaHas(['nullable', 'immutable', 'read'], data, 'A constant cannot be nullable or immutable.'); metadata.is_constant = true; break; default: if (JOII.Compat.indexOf(types, data[i]) !== -1) { if (metadata.type !== null) { throw 'Property "' + name + '" has multiple type defintions.'; } metadata.type = data[i]; break; } // Check for Interface-types if (typeof (JOII.InterfaceRegistry[data[i]]) !== 'undefined') { metadata.is_joii_object = true; metadata.type = JOII.InterfaceRegistry[data[i]].definition.__interface__.name; break; } // Check for Class-types if (typeof (JOII.ClassRegistry[data[i]]) !== 'undefined') { metadata.is_joii_object = true; metadata.type = JOII.ClassRegistry[data[i]].prototype.__joii__.name; break; } // Check for enumerators if (typeof (JOII.EnumRegistry[data[i]]) !== 'undefined') { metadata.is_enum = true; metadata.type = data[i]; break; } // Check for self reference (the class we're defining) if (currently_defining == raw_data[i]) { metadata.is_joii_object = true; metadata.type = data[i]; break; } throw 'Syntax error: unexpected "' + data[i] + '" in property declaration of "' + name + '".'; } } return metadata; }; JOII.CreatePropertyGetterSetter = function(deep_copy, meta, name) { "use strict"; // If the meta type is boolean, prefix the getter with 'is' // rather than 'get'. var getter, getter_meta, getter_fn; if (meta.type === 'boolean') { if (JOII.CamelcaseName(meta.name).substr(0, 2) === 'Is') { getter = JOII.CamelcaseName(meta.name); getter = getter.substring(0, 1).toLowerCase() + getter.substring(1); } else { getter = 'is' + JOII.CamelcaseName(meta.name); } } else { getter = 'get' + JOII.CamelcaseName(meta.name); } var setter = 'set' + JOII.CamelcaseName(meta.name), setter_meta, setter_fn; // Create a getter if (typeof (deep_copy[getter]) === 'undefined') { if (meta.is_static) { getter_fn = function (name, prop_name) { return function () { return inner_static_objects[name][prop_name]; }; }(name, meta.name); } else { getter_fn = new Function('return this["' + meta.name + '"];'); } getter_meta = JOII.ParseClassProperty(meta.visibility + ' function ' + getter + '()', name); getter_meta.visibility = meta.visibility; getter_meta.is_abstract = meta.is_abstract; getter_meta.is_final = meta.is_final; getter_meta.is_static = meta.is_static; getter_meta.class_name = meta.class_name; } // Create a setter if (typeof (deep_copy[setter]) === 'undefined' && meta.is_read_only === false) { var nullable = meta.is_nullable, validator; if (meta.type !== null) { // InstanceOf validator (in case of interfaces & classes) if (typeof (JOII.InterfaceRegistry[meta.type]) !== 'undefined' || typeof (JOII.ClassRegistry[meta.type]) !== 'undefined') { setter_fn = function (name, prop_name, prop_type, nullable, setter) { return function (v) { if (prop_type !== null) { if (JOII.Compat.findJOIIName(v[0]) === prop_type) {} else { if (v[0] !== null && typeof (v[0].instanceOf) !== 'function' || (typeof (v[0]) === 'object' && v[0] !== null && typeof (v[0].instanceOf) === 'function' && !v[0].instanceOf(prop_type)) || v[0] === null) { if (nullable === false || (nullable === true && v[0] !== null && typeof (v[0]) !== "undefined")) { throw setter + ' expects an instance of ' + prop_type + ', ' + (v[0] === null ? "null" : typeof (v[0])) + " given."; } }}; } this[prop_name] = v[0]; return this.__api__; }; }(name, meta.name, meta.type, nullable, setter); } else { // Native type validator setter_fn = function (name, prop_name, prop_type, nullable, setter) { return function (v) { if (prop_type !== null) { if (typeof (JOII.EnumRegistry[prop_type]) !== 'undefined') { var _e = JOII.EnumRegistry[prop_type]; if (!_e.contains(v[0])) { throw setter + ": '" + v[0] + "' is not a member of enum " + _e.getName() + "."; } } else { if (typeof (v[0]) !== prop_type) { if (nullable === false || (nullable === true && v[0] !== null && typeof (v[0]) !== "undefined")) { throw setter + " expects " + prop_type + ", " + typeof (v[0]) + " given."; } }; } } this[prop_name] = v[0]; return this.__api__; }; }(name, meta.name, meta.type, nullable, setter); } } else { setter_fn = function (prop_name) { return function (v) { this[prop_name] = v[0]; return this.__api__; }; }(meta.name); } // if it's static, make sure we're referring to it explicitly // so that we reference the same property, no matter which subclass it's called from // static properties stay with the class they're defined in. if (meta.is_static) { if (typeof (JOII.InterfaceRegistry[meta.type]) !== 'undefined' || typeof (JOII.ClassRegistry[meta.type]) !== 'undefined') { setter_fn = function (name, prop_name, prop_type, nullable, setter) { return function (v) { if (prop_type !== null) { if (JOII.Compat.findJOIIName(v[0]) === prop_type) {} else { if (v[0] !== null && typeof (v[0].instanceOf) !== 'function' || (typeof (v[0]) === 'object' && v[0] !== null && typeof (v[0].instanceOf) === 'function' && !v[0].instanceOf(prop_type)) || v[0] === null) { if (nullable === false || (nullable === true && v[0] !== null && typeof (v[0]) !== "undefined")) { throw setter + ' expects an instance of ' + prop_type + ', ' + (v[0] === null ? "null" : typeof (v[0])) + " given."; } }}; } inner_static_objects[name][prop_name] = v[0]; return this.__api__; }; }(name, meta.name, meta.type, nullable, setter); } else { // Native type validator setter_fn = function (name, prop_name, prop_type, nullable, setter) { return function (v) { if (prop_type !== null) { if (typeof (JOII.EnumRegistry[prop_type]) !== 'undefined') { var _e = JOII.EnumRegistry[prop_type]; if (!_e.contains(v[0])) { throw setter + ": '" + v[0] + "' is not a member of enum " + _e.getName() + "."; } } else { if (typeof (v[0]) !== prop_type) { if (nullable === false || (nullable === true && v[0] !== null && typeof (v[0]) !== "undefined")) { throw setter + " expects " + prop_type + ", " + typeof (v[0]) + " given."; } }; } } inner_static_objects[name][prop_name] = v[0]; return this.__api__; }; }(name, meta.name, meta.type, nullable, setter); } } // we want to take in ANY type, so we can provide better feedback with our setter function setter_meta = JOII.ParseClassProperty(meta.visibility + ' function ' + setter + '(...)', name); setter_meta.visibility = meta.visibility; setter_meta.is_abstract = meta.is_abstract; setter_meta.is_final = meta.is_final; setter_meta.is_static = meta.is_static; setter_meta.class_name = meta.class_name; } return { 'getter' : { name: getter, fn: getter_fn, meta: getter_meta }, 'setter' : { name: setter, fn: setter_fn, meta: setter_meta } }; }; /** * Creates a non-enumerable property in the given object. * * If the browser doesn't support Object.defineProperty, a regular * property is created instead. Please be aware that unit-tests using * deepEqual-assertions might fail on this using older browsers. * * @param {Object} obj * @param {String} name * @param {*} val * @param {Boolean} writable */ JOII.CreateProperty = function(obj, name, val, writable) { try { if (typeof (Object.defineProperty) !== 'undefined') { Object.defineProperty(obj, name, { value : val, enumerable : (writable === false), configurable : (writable !== false), writable : (writable !== false) }); } else { obj[name] = val; } } catch (e) { obj[name] = val; } }; /** * Adds a function to the prototype. * * Meta.parameters should include the types in order to support overloading. * * @param {Object} prototype * @param {Object} meta * @param {Function} fn * @param {Boolean} ignore_duplicate */ JOII.addFunctionToPrototype = function(prototype, meta, fn, ignore_duplicate) { if (meta.is_abstract && meta.is_final) { throw 'Member "' + meta.name + '(' + meta.parameters.join(', ') + ')" cannot be both abstract and final simultaniously.'; } if (typeof (ignore_duplicate) === 'undefined') { ignore_duplicate = false; } if (typeof (prototype.__joii__.metadata[meta.name]) !== 'object') { prototype.__joii__.metadata[meta.name] = JOII.Compat.extend(true, {}, meta); prototype.__joii__.metadata[meta.name].overloads = []; } var proto_meta = prototype.__joii__.metadata[meta.name]; if (typeof (proto_meta.overloads) !== 'object') { proto_meta.overloads = []; } if (proto_meta.visibility !== meta.visibility) { throw 'Member ' + meta.name + ': inconsistent visibility.'; } var has_parameterless = proto_meta.has_parameterless; proto_meta.is_abstract = false; proto_meta.is_final = false; proto_meta.has_parameterless = false; for (var i = 0; i < meta.parameters.length - 1; i++) { if (meta.parameters[i] == '...') { throw 'Member ' + meta.name + ': Variadic parameter (...) must be the last in the function parameter list.'; } } var not_all_overloads_final = !meta.is_final; var first_loop = true; for (var idx = 0; idx < proto_meta.overloads.length; idx++) { var function_parameters_meta = proto_meta.overloads[idx]; var found_abstract_this_loop = false; if (function_parameters_meta.is_abstract) { found_abstract_this_loop = true; } if (!function_parameters_meta.has_parameters) { proto_meta.has_parameterless = true; } not_all_overloads_final = not_all_overloads_final || (!function_parameters_meta.is_final); if (function_parameters_meta.parameters.length === meta.parameters.length) { // this signature has the same number of types as the new signature // check to see if the types are the same (duplicate signature) var different = false; for (var j = 0; j < function_parameters_meta.parameters.length; j++) { if (function_parameters_meta.parameters[j] != meta.parameters[j]) { different = true; } } if (!different) { if (function_parameters_meta.is_abstract) { proto_meta.overloads.splice(idx, 1); // remove the abstract version, since we're about to add a non-abstract idx--; // adjust the idx for the changed array found_abstract_this_loop = false; // we're removing this, so don't count it for abstract check } else if (meta.is_final) { throw 'Final member "' + meta.name + '(' + meta.parameters.join(', ') + ')" cannot be overwritten.'; } else { if (!ignore_duplicate) { throw 'Member "' + meta.name + '(' + meta.parameters.join(', ') + ')" is defined twice.'; } else { return false; } } } } if (found_abstract_this_loop) { proto_meta.is_abstract = true; } } var function_meta = { fn: fn, parameters : meta.parameters, is_abstract : meta.is_abstract, is_final : meta.is_final, is_inherited : meta.is_inherited, has_parameters : meta.has_parameters }; if (!meta.has_parameters) { proto_meta.has_parameterless = true; } prototype.__joii__.metadata[meta.name].overloads.push(function_meta); if (function_meta.is_abstract) { prototype.__joii__.metadata[meta.name].is_abstract = true; } prototype.__joii__.metadata[meta.name].is_final = !not_all_overloads_final; // create function shim to validate the parameters, and allow overloading //if (typeof (prototype[meta.name]) !== 'function') { // this test was preventing it from overriding toString prototype[meta.name] = JOII.createFunctionShim(meta.name, prototype.__joii__.metadata[meta.name].overloads); //} return true; }; /** * Creates a function detour which checks type parameters, then dispatches to the appropriate overloaded function. * * @param {String} name * @param {Object} overloads */ JOII.createFunctionShim = function(name, overloads) { return function() { // If there's only one function, and it has no parameters, we'll assume it's old-style to preserve backwards compatibility, so just pass the list of parameters if (overloads.length === 1 && overloads[0].parameters.length === 0) { return overloads[0].fn.apply(this, arguments); } var closest_variadic = null; var closest_variadic_parameter_count = -1; var parameterless = null; for (var overload_index = 0; overload_index < overloads.length; overload_index++) { var func = overloads[overload_index]; var parameters = func.parameters; var valid = true; if (!func.has_parameters) { parameterless = func; } // test exact matches if (parameters.length == arguments.length) { for (var i = 0; i < parameters.length; i++) { if (!JOII.Compat.canTypeBeCastTo(arguments[i], parameters[i])) { valid = false; break; } } if (valid) { // found an overload that matches the inputs - call it return func.fn.apply(this, arguments); } } if (parameters[parameters.length - 1] == '...') { // test for variadic valid = null; var variadic_parameter_count = 0; for (var i = 0; i < parameters.length; i++) { if (parameters[i] === '...') { valid = true; } else { if (!JOII.Compat.canTypeBeCastTo(arguments[i], parameters[i])) { valid = false; break; } variadic_parameter_count++; } } if (valid) { if (variadic_parameter_count > closest_variadic_parameter_count) { closest_variadic = func; closest_variadic_parameter_count = variadic_parameter_count; } } } } if (closest_variadic != null) { if (!func.is_inherited) { // extract the variadic portion of the call to an array var args = []; // arguments.splice(closestVariadic.parameters.length -1); for (var i = closest_variadic.parameters.length - 1; i < arguments.length; i++) { args.push(arguments[i]); } arguments.length = closest_variadic.parameters.length; arguments[closest_variadic.parameters.length - 1] = args; } // found an overload that matches the inputs - call it return closest_variadic.fn.apply(this, arguments); } if (parameterless != null) { // found an old-style parameterless function. Use it for the "catch all" return parameterless.fn.apply(this, arguments); } // create a type list of the arguments for error handling purposes var parameter_types = []; for (var i = 0; i < arguments.length; i++) { var JOIIName = JOII.Compat.findJOIIName(arguments[i]); if (!JOIIName) { JOIIName = typeof (arguments[i]); } parameter_types.push(JOIIName === null ? typeof (arguments[i]) : JOIIName); } throw 'Couldn\'t find a function handler to match: ' + name + '(' + parameter_types.join(', ') + ').'; }; }; /** * Camelcase a name. * * @param {String} input * @return {String} */ JOII.CamelcaseName = function(input) { input = input.toLowerCase().replace(/_(.)/g, function(match, group1) { return group1.toUpperCase(); }); return input.charAt(0).toUpperCase() + input.slice(1); };