custom-ability
Advanced tools
Comparing version 2.0.0-alpha.3 to 2.0.0-alpha.4
/** | ||
* A symbol used to mark a class's abilities | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export declare const abilitiesSym = "$abilities"; | ||
/** | ||
* A symbol used to mark a class's additional ability whether injected | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export declare const abilitiesOptSym = "$abilitiesOpt"; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
export declare const AdditionalInjectionMode: { | ||
all: number; | ||
target: number; | ||
}; | ||
/** | ||
* The Ability Options | ||
@@ -6,2 +27,10 @@ */ | ||
/** | ||
* An optional id for AdditionalAbility option | ||
*/ | ||
id?: string; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* An optional list of method names to include. | ||
@@ -24,4 +53,48 @@ */ | ||
/** | ||
* An additional ability | ||
* | ||
*/ | ||
export interface AdditionalAbility { | ||
/** | ||
* the AdditionalAbilityOptions ID | ||
*/ | ||
id?: string; | ||
/** | ||
* the Additional Injection Mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* the list of required methods | ||
*/ | ||
required?: string[]; | ||
/** | ||
* Returns the additional ability options if they exist | ||
* | ||
* @param options the ability Options | ||
* @returns the Additional Ability options if exists | ||
*/ | ||
getOpts: (options?: AbilityOptions) => AbilityOptions | undefined; | ||
} | ||
/** | ||
* An object mapping target ability names to additional abilities | ||
* | ||
*/ | ||
export interface AdditionalAbilities { | ||
[key: string]: AdditionalAbility | Array<AdditionalAbility>; | ||
} | ||
/** | ||
* The ability injector options | ||
* | ||
*/ | ||
export interface AbilityInjectorOptions { | ||
/** | ||
* The optional depends abilities which can work together | ||
*/ | ||
depends?: AdditionalAbilities; | ||
} | ||
/** | ||
* A function that adds(injects) the ability of a specified ability class to a target class. | ||
* | ||
* Note: Maybe the ability will be injected into the inheritance class. | ||
* | ||
* @param {Function} targetClass - The target class to which the ability will be added. | ||
@@ -39,2 +112,3 @@ * @param {AbilityOptions} [options] - An optional ability configuration object. | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -44,3 +118,5 @@ * properties and methods. | ||
*/ | ||
export declare function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], isGetClassFunc?: boolean): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, injectorOpts?: AbilityInjectorOptions): AbilityFn; |
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createAbilityInjector = void 0; | ||
exports.createAbilityInjector = exports.AdditionalInjectionMode = exports.abilitiesOptSym = exports.abilitiesSym = void 0; | ||
const array_1 = __importDefault(require("util-ex/lib/is/type/array")); | ||
@@ -21,2 +21,20 @@ const function_1 = __importDefault(require("util-ex/lib/is/type/function")); | ||
/** | ||
* A symbol used to mark a class's abilities | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
exports.abilitiesSym = '$abilities'; | ||
/** | ||
* A symbol used to mark a class's additional ability whether injected | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
exports.abilitiesOptSym = '$abilitiesOpt'; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
exports.AdditionalInjectionMode = { all: 0, target: 1 }; | ||
/** | ||
* Inject non-enumerable members of the aObject into aTargetClass | ||
@@ -28,4 +46,4 @@ * | ||
* @param filter It'll be injected only when filter callback function return true, if exists | ||
* @param isStatic Whether is static members | ||
* @returns already injected members name list | ||
* @param isStatic Whether the members to be injected are static | ||
* @returns The names of members that have been injected | ||
*/ | ||
@@ -91,2 +109,3 @@ function injectMembersFromNonEnum(aTargetClass, aObject, filter, isStatic) { | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -96,7 +115,17 @@ * properties and methods. | ||
*/ | ||
function createAbilityInjector(abilityClass, aCoreMethod, isGetClassFunc) { | ||
function createAbilityInjector(abilityClass, aCoreMethod, isGetClassFunc, injectorOpts) { | ||
if (typeof aCoreMethod === 'boolean') { | ||
injectorOpts = isGetClassFunc; | ||
isGetClassFunc = aCoreMethod; | ||
aCoreMethod = undefined; | ||
} | ||
else if (typeof aCoreMethod === 'object') { | ||
injectorOpts = aCoreMethod; | ||
aCoreMethod = undefined; | ||
} | ||
if (isGetClassFunc !== undefined && typeof isGetClassFunc !== 'boolean') { | ||
injectorOpts = isGetClassFunc; | ||
isGetClassFunc = undefined; | ||
} | ||
const vDepends = injectorOpts && injectorOpts.depends; | ||
function abilityFn(aClass, aOptions) { | ||
@@ -112,8 +141,10 @@ let AbilityClass = abilityClass; | ||
if (aClass != null) { | ||
let $abilities, vAdditionalAbilityInjected, vExcludes, vIncludes, vInjectedOnParent; | ||
let aClassPrototype = aClass.prototype; | ||
let $abilities, vAdditionalAbilityInjected; | ||
const vTargetClass = aClass; | ||
let vClassPrototype = aClass.prototype; | ||
let vHasCoreMethod = (0, array_1.default)(aCoreMethod) ? aCoreMethod[0] : aCoreMethod; | ||
// TODO: Check the core method on the target class or the inheritance? | ||
if (vHasCoreMethod) { | ||
if (vHasCoreMethod[0] !== '@') { | ||
vHasCoreMethod = aClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
vHasCoreMethod = vClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
} | ||
@@ -125,102 +156,82 @@ else { | ||
} | ||
if (vName) { | ||
$abilities = aClass.prototype.$abilities; | ||
vInjectedOnParent = (0, injected_on_parent_1.default)(aClass, vName); | ||
if (!aClass.prototype.hasOwnProperty('$abilities')) { | ||
$abilities = null; | ||
} | ||
} | ||
if (!(vHasCoreMethod || ($abilities && $abilities['$' + vName]))) { | ||
if ((aOptions == null) || !(aOptions.include || aOptions.exclude)) { | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName); | ||
let vIncludeMembers; | ||
let vFilterMembers; | ||
const vHasIncludeOptions = aOptions && (aOptions.include || aOptions.exclude); | ||
if (vHasIncludeOptions) { | ||
let arr = aOptions.include; | ||
const hasExclude = typeof aOptions.exclude === 'string' || (aOptions.exclude && aOptions.exclude.length); | ||
if (typeof arr === 'string') { | ||
arr = [arr]; | ||
} | ||
vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
(0, extend_1.default)(aClass, AbilityClass, function (k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
if (!(arr && arr.length) || hasExclude) { | ||
vIncludeMembers = getMembers(AbilityClass); | ||
} | ||
if (!vInjectedOnParent) { | ||
vExcludes = injectMembersFromNonEnum(aClassPrototype, AbilityClass.prototype); | ||
(0, extend_1.default)(aClassPrototype, AbilityClass.prototype, function (k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
} | ||
} | ||
else { | ||
vIncludes = aOptions.include; | ||
if (vIncludes) { | ||
if (!(0, array_1.default)(vIncludes)) { | ||
vIncludes = [vIncludes]; | ||
} | ||
} | ||
else { | ||
vIncludes = []; | ||
vIncludeMembers = arr; | ||
} | ||
if (aCoreMethod) { | ||
if ((0, array_1.default)(aCoreMethod)) { | ||
vIncludes = vIncludes.concat(aCoreMethod); | ||
arr = aOptions.exclude; | ||
if (arr) { | ||
if (!(0, array_1.default)(arr)) { | ||
arr = [arr]; | ||
} | ||
else { | ||
vIncludes.push(aCoreMethod); | ||
} | ||
vIncludeMembers = vIncludeMembers.filter(item => arr.indexOf(item) === -1); | ||
} | ||
vExcludes = aOptions.exclude; | ||
if (vExcludes) { | ||
if (!(0, array_1.default)(vExcludes)) { | ||
vExcludes = [vExcludes]; | ||
} | ||
arrayPushOnly(vIncludeMembers, aCoreMethod); | ||
if (vIncludeMembers.length) { | ||
vFilterMembers = function filterMembers(name) { | ||
return vIncludeMembers.includes(name); | ||
}; | ||
} | ||
else { | ||
vExcludes = []; | ||
} | ||
if (vName) { | ||
const vInjectedOnParent = (0, injected_on_parent_1.default)(aClass, vName); | ||
// return the injected class if it has already been injected on parent | ||
if (vInjectedOnParent) { | ||
return vInjectedOnParent; | ||
} | ||
const vGenFilter = function (isStatic) { | ||
return function (k) { | ||
return filter(k, vIncludes, vExcludes, isStatic); | ||
}; | ||
}; | ||
let vAbilities = injectMembersFromNonEnum(aClass, AbilityClass, vGenFilter(true), true); | ||
vAbilities = vAbilities.concat(injectMembersFromNonEnum(aClass.prototype, AbilityClass.prototype, vGenFilter())); | ||
vExcludes = vExcludes.concat(vAbilities); | ||
vAbilities = undefined; | ||
const filterMethods = function (methods, isStatic) { | ||
if (methods instanceof Object) { | ||
const vFilter = vGenFilter(isStatic); | ||
for (const k in methods) { | ||
if (!vFilter(k)) { | ||
delete methods[k]; | ||
} | ||
} | ||
} | ||
}; | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, filterMethods); | ||
} | ||
(0, extend_1.default)(aClass, AbilityClass, vGenFilter(true)); | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, aOptions); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
aClass = vAdditionalAbilityInjected; | ||
vClassPrototype = vAdditionalAbilityInjected.prototype; | ||
} | ||
if (!vInjectedOnParent) { | ||
(0, extend_1.default)(aClassPrototype, AbilityClass.prototype, vGenFilter()); | ||
$abilities = aClass.prototype[exports.abilitiesSym]; | ||
if (!vClassPrototype.hasOwnProperty(exports.abilitiesSym)) { | ||
$abilities = null; | ||
} | ||
filterMethods(aOptions.methods); | ||
filterMethods(aOptions.classMethods); | ||
} | ||
if (!vHasIncludeOptions) { | ||
// inject the static methods | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
// inject the enumerable members | ||
(0, extend_1.default)(aClass, AbilityClass, function (k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
// inject the methods | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype); | ||
(0, extend_1.default)(vClassPrototype, AbilityClass.prototype, function (k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
} | ||
else { | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, vFilterMembers, true); | ||
// inject the enumerable members | ||
(0, extend_1.default)(aClass, AbilityClass, function (k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers('@' + k)); | ||
}); | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype, vFilterMembers); | ||
(0, extend_1.default)(vClassPrototype, AbilityClass.prototype, function (k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers(k)); | ||
}); | ||
} | ||
if (aOptions != null) { | ||
if (!vInjectedOnParent && aOptions.methods instanceof Object) { | ||
(0, injectMethods_1.default)(aClassPrototype, aOptions.methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
(0, injectMethods_1.default)(aClass, aOptions.classMethods, aOptions); | ||
} | ||
_applyAdditionalAbility(aClass, aOptions); | ||
} | ||
if (vName) { | ||
if (aClassPrototype !== aClass.prototype) { | ||
aClassPrototype = aClass.prototype; | ||
if (vClassPrototype.hasOwnProperty(exports.abilitiesSym)) { | ||
$abilities = vClassPrototype[exports.abilitiesSym]; | ||
} | ||
if (!aClassPrototype.hasOwnProperty('$abilities')) { | ||
else { | ||
$abilities = {}; | ||
(0, defineProperty_1.default)(aClassPrototype, '$abilities', $abilities); | ||
(0, defineProperty_1.default)(vClassPrototype, exports.abilitiesSym, $abilities); | ||
} | ||
@@ -230,2 +241,28 @@ $abilities['$' + vName] = abilityFn; | ||
} | ||
// Apply optional dependencies | ||
if (vDepends) { | ||
Object.keys(vDepends).forEach(function (name) { | ||
let vDepend = vDepends[name]; | ||
if (vDepend) { | ||
if (!Array.isArray(vDepend)) { | ||
vDepend = [vDepend]; | ||
} | ||
vDepend.forEach(item => !item.id && (item.id = vName)); | ||
let vDependAbility = $abilities[name]; | ||
if (vDependAbility) { | ||
if (!Array.isArray(vDependAbility)) { | ||
$abilities[name] = vDependAbility = [vDependAbility]; | ||
} | ||
vDependAbility.push(vDepends[name]); | ||
} | ||
else { | ||
$abilities[name] = vDepends[name]; | ||
} | ||
const vInjectedOnParent = (0, injected_on_parent_1.default)(vTargetClass, name); | ||
if (vInjectedOnParent) { | ||
injectAdditionalAbility(vTargetClass, name, aOptions); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
@@ -238,60 +275,59 @@ else { | ||
; | ||
function filter(k, aIncludes, aExcludes, aIsStatic) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
let result = aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} | ||
else if (aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
else { | ||
result = true; | ||
} | ||
return result; | ||
} | ||
; | ||
abilityFn.filter = filter; | ||
/* // eslint-disable-next-line unused-imports/no-unused-vars | ||
function getAdditionalAbility(aClass, aName) { | ||
const result = []; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities') && !aClass.prototype.$abilities['$' + aName] && aClass.prototype.$abilities[aName]) { | ||
result.push(aClass); | ||
} | ||
aClass = aClass.super_; | ||
} | ||
return result; | ||
}; | ||
*/ | ||
function injectAdditionalAbility(aClass, aName, filterMethods) { | ||
function injectAdditionalAbility(aClass, aName, aOptions) { | ||
let result; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities')) { | ||
const $abilities = aClass.prototype.$abilities; | ||
let vAbility; | ||
if (!$abilities['$' + aName] && (vAbility = $abilities[aName])) { | ||
const vOptions = vAbility(); | ||
result = aClass; | ||
if (vOptions != null) { | ||
if (filterMethods) { | ||
filterMethods(vOptions.methods); | ||
filterMethods(vOptions.classMethods); | ||
let vClass = aClass; | ||
let vOnTarget = true; | ||
const vTargets = []; | ||
while (vClass && vClass.prototype) { | ||
if (vClass.prototype.hasOwnProperty(exports.abilitiesSym)) { | ||
let vAbility = getAdditionalAbilityOptions(vClass, aName); | ||
if (vAbility) { | ||
if (!Array.isArray(vAbility)) { | ||
vAbility = [vAbility]; | ||
} | ||
for (const item of vAbility) { | ||
if (item && typeof item.getOpts === 'function') { | ||
const vOptions = item.getOpts(aOptions); | ||
if (vOptions != null) { | ||
if (item.required && item.required.length && aOptions && (aOptions.include || aOptions.exclude)) { | ||
let vMissingMethod; | ||
for (const n of item.required) { | ||
if (!filter(n, aOptions.include, aOptions.exclude)) { | ||
vMissingMethod = true; | ||
break; | ||
} | ||
} | ||
if (vMissingMethod) { | ||
continue; | ||
} | ||
} | ||
if (vOptions.id === undefined) { | ||
vOptions.id = item.id; | ||
} | ||
if (item.mode === exports.AdditionalInjectionMode.target) { | ||
vTargets.push([aClass, vOptions, vClass]); | ||
} | ||
else { | ||
applyAdditionalAbility(vClass, aName, vOptions); | ||
vOnTarget = false; | ||
} | ||
} | ||
} | ||
if (vOptions.methods instanceof Object) { | ||
(0, injectMethods_1.default)(aClass.prototype, vOptions.methods, vOptions); | ||
} | ||
if (vOptions.classMethods instanceof Object) { | ||
(0, injectMethods_1.default)(aClass, vOptions.classMethods, vOptions); | ||
} | ||
} | ||
result = vClass; | ||
} | ||
} | ||
aClass = aClass.super_; | ||
vClass = (0, inherits_ex_1.getParentClass)(vClass); | ||
} | ||
if (vTargets.length) { | ||
// apply additional ability from parent to child | ||
for (let i = vTargets.length - 1; i >= 0; i--) { | ||
const item = vTargets[i]; | ||
applyAdditionalAbility(item[0], aName, item[1], item[2]); | ||
} | ||
} | ||
if (vOnTarget) { | ||
result = aClass; | ||
} | ||
return result; | ||
@@ -304,1 +340,150 @@ } | ||
; | ||
/** | ||
* Returns the additional ability options for a specified ability class | ||
* | ||
* @param aClass - The class to which the additional ability options belong | ||
* @param aName - The name of the ability | ||
* @returns The additional ability options | ||
*/ | ||
function getAdditionalAbilityOptions(aClass, aName) { | ||
const $abilities = aClass.prototype[exports.abilitiesSym]; | ||
const result = $abilities && $abilities[aName]; | ||
return result; | ||
} | ||
function _applyAdditionalAbility(aClass, aOptions) { | ||
if (aOptions.methods instanceof Object) { | ||
const methods = getFilteredMembers(aOptions.methods, aOptions); | ||
(0, injectMethods_1.default)(aClass.prototype, methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
const classMethods = getFilteredMembers(aOptions.classMethods, aOptions, true); | ||
(0, injectMethods_1.default)(aClass, classMethods, aOptions); | ||
} | ||
} | ||
/** | ||
* Adds an additional ability to a class based on the provided options | ||
* | ||
* @param {Function} aClass - The target class to which the additional ability will be added | ||
* @param {string} aName - The name of the additional ability | ||
* @param {AbilityOptions} aOptions - The options that describe which methods to inject | ||
* @param {Function} [fromClass] - The class from which the additional ability is being applied | ||
*/ | ||
function applyAdditionalAbility(aClass, aName, aOptions, fromClass) { | ||
if (aOptions != null) { | ||
const fromId = fromClass && aClass !== fromClass ? '_' + fromClass.name : ''; | ||
const id = aName + (aOptions.id ? '_' + aOptions.id : fromId); | ||
let $abilitiesOpt = aClass.prototype[exports.abilitiesOptSym]; | ||
if (!$abilitiesOpt || !$abilitiesOpt[id]) { | ||
_applyAdditionalAbility(aClass, aOptions); | ||
if (!$abilitiesOpt) { | ||
$abilitiesOpt = {}; | ||
(0, defineProperty_1.default)(aClass.prototype, exports.abilitiesOptSym, $abilitiesOpt); | ||
} | ||
$abilitiesOpt[id] = true; | ||
} | ||
} | ||
} | ||
/** | ||
* Pushes an array of items into a destination array, but only if the items are not already in the destination array | ||
* | ||
* @param dest - The destination array | ||
* @param src - The source array or item | ||
* @returns The destination array with the new items added | ||
*/ | ||
function arrayPushOnly(dest, src) { | ||
if (src !== undefined) { | ||
if (!Array.isArray(src)) { | ||
src = [src]; | ||
} | ||
src.forEach(item => { | ||
dest.indexOf(item) === -1 && dest.push(item); | ||
}); | ||
} | ||
return dest; | ||
} | ||
/** | ||
* Returns an array of all members of a class, including static and prototype members | ||
* | ||
* @param aClass - The class to get the members from | ||
* @returns An array of member names | ||
*/ | ||
function getMembers(aClass) { | ||
let result = (0, get_non_enumerable_names_1.getNonEnumerableNames)(aClass).filter(n => !skipStaticNames.includes(n)).map(name => '@' + name); | ||
result = result.concat(Object.keys(aClass).map(name => '@' + name)); | ||
result = result.concat((0, get_non_enumerable_names_1.getNonEnumerableNames)(aClass.prototype).filter(n => !skipProtoNames.includes(n))); | ||
result = result.concat(Object.keys(aClass.prototype)); | ||
return result; | ||
} | ||
/** | ||
* Determines whether to include a member based on the provided options | ||
* | ||
* @private | ||
* @param {string} k - The name of the member | ||
* @param {string|string[]} aIncludes - The names of members to include | ||
* @param {string|string[]} aExcludes - The names of members to exclude | ||
* @param {boolean} [aIsStatic] - Whether the member is a static member | ||
* @returns {boolean} - Whether to include the member | ||
*/ | ||
function filter(k, aIncludes, aExcludes, aIsStatic) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
if (typeof aIncludes === 'string') { | ||
aIncludes = [aIncludes]; | ||
} | ||
if (typeof aExcludes === 'string') { | ||
aExcludes = [aExcludes]; | ||
} | ||
let result = aIncludes && aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} | ||
else if (aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
else { | ||
result = true; | ||
} | ||
return result; | ||
} | ||
; | ||
/** | ||
* Returns an object containing only the members that pass the filter | ||
* | ||
* @function | ||
* @private | ||
* @param {Object} obj - The object to filter | ||
* @param {AbilityOptions} aOptions - The options that describe which members to include | ||
* @param {boolean} [isStatic] - Whether the members are static members | ||
* @returns {Object} - An object containing only the members that pass the filter | ||
*/ | ||
function getFilteredMembers(obj, aOptions, isStatic) { | ||
const result = {}; | ||
Object.keys(obj).forEach(name => { | ||
if (filter(name, aOptions.include, aOptions.exclude, isStatic)) { | ||
result[name] = obj[name]; | ||
} | ||
}); | ||
return result; | ||
} | ||
/* | ||
function cloneObj(src: object, maxDeep = 5) { | ||
if (!src) {return src}; | ||
const result = {} | ||
Object.keys(src).forEach(key => { | ||
const value = src[key] | ||
if (Array.isArray(value)) { | ||
result[key] = value.slice() | ||
} else if (maxDeep > 0 && value instanceof Object) { | ||
--maxDeep | ||
result[key] = cloneObj(value, maxDeep) | ||
} else { | ||
result[key] = src[key] | ||
} | ||
}) | ||
return result | ||
} | ||
*/ |
/** | ||
* A symbol used to mark a class's abilities | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export declare const abilitiesSym = "$abilities"; | ||
/** | ||
* A symbol used to mark a class's additional ability whether injected | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export declare const abilitiesOptSym = "$abilitiesOpt"; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
export declare const AdditionalInjectionMode: { | ||
all: number; | ||
target: number; | ||
}; | ||
/** | ||
* The Ability Options | ||
@@ -6,2 +27,10 @@ */ | ||
/** | ||
* An optional id for AdditionalAbility option | ||
*/ | ||
id?: string; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* An optional list of method names to include. | ||
@@ -24,4 +53,48 @@ */ | ||
/** | ||
* An additional ability | ||
* | ||
*/ | ||
export interface AdditionalAbility { | ||
/** | ||
* the AdditionalAbilityOptions ID | ||
*/ | ||
id?: string; | ||
/** | ||
* the Additional Injection Mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* the list of required methods | ||
*/ | ||
required?: string[]; | ||
/** | ||
* Returns the additional ability options if they exist | ||
* | ||
* @param options the ability Options | ||
* @returns the Additional Ability options if exists | ||
*/ | ||
getOpts: (options?: AbilityOptions) => AbilityOptions | undefined; | ||
} | ||
/** | ||
* An object mapping target ability names to additional abilities | ||
* | ||
*/ | ||
export interface AdditionalAbilities { | ||
[key: string]: AdditionalAbility | Array<AdditionalAbility>; | ||
} | ||
/** | ||
* The ability injector options | ||
* | ||
*/ | ||
export interface AbilityInjectorOptions { | ||
/** | ||
* The optional depends abilities which can work together | ||
*/ | ||
depends?: AdditionalAbilities; | ||
} | ||
/** | ||
* A function that adds(injects) the ability of a specified ability class to a target class. | ||
* | ||
* Note: Maybe the ability will be injected into the inheritance class. | ||
* | ||
* @param {Function} targetClass - The target class to which the ability will be added. | ||
@@ -39,2 +112,3 @@ * @param {AbilityOptions} [options] - An optional ability configuration object. | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -44,3 +118,5 @@ * properties and methods. | ||
*/ | ||
export declare function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], isGetClassFunc?: boolean): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, aCoreMethod?: string | string[], injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export declare function createAbilityInjector(abilityClass: Function, injectorOpts?: AbilityInjectorOptions): AbilityFn; |
@@ -8,3 +8,3 @@ import isArray from 'util-ex/lib/is/type/array'; | ||
import { getNonEnumerableNames as getNonEnumNames } from 'util-ex/lib/get-non-enumerable-names'; | ||
import { isEmptyFunction } from 'inherits-ex'; | ||
import { getParentClass, isEmptyFunction } from 'inherits-ex'; | ||
import isInjectedOnParent from './injected-on-parent'; | ||
@@ -15,2 +15,20 @@ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; | ||
/** | ||
* A symbol used to mark a class's abilities | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export const abilitiesSym = '$abilities'; | ||
/** | ||
* A symbol used to mark a class's additional ability whether injected | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export const abilitiesOptSym = '$abilitiesOpt'; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
export const AdditionalInjectionMode = { all: 0, target: 1 }; | ||
/** | ||
* Inject non-enumerable members of the aObject into aTargetClass | ||
@@ -22,4 +40,4 @@ * | ||
* @param filter It'll be injected only when filter callback function return true, if exists | ||
* @param isStatic Whether is static members | ||
* @returns already injected members name list | ||
* @param isStatic Whether the members to be injected are static | ||
* @returns The names of members that have been injected | ||
*/ | ||
@@ -85,2 +103,3 @@ function injectMembersFromNonEnum(aTargetClass, aObject, filter, isStatic) { | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -90,7 +109,17 @@ * properties and methods. | ||
*/ | ||
export function createAbilityInjector(abilityClass, aCoreMethod, isGetClassFunc) { | ||
export function createAbilityInjector(abilityClass, aCoreMethod, isGetClassFunc, injectorOpts) { | ||
if (typeof aCoreMethod === 'boolean') { | ||
injectorOpts = isGetClassFunc; | ||
isGetClassFunc = aCoreMethod; | ||
aCoreMethod = undefined; | ||
} | ||
else if (typeof aCoreMethod === 'object') { | ||
injectorOpts = aCoreMethod; | ||
aCoreMethod = undefined; | ||
} | ||
if (isGetClassFunc !== undefined && typeof isGetClassFunc !== 'boolean') { | ||
injectorOpts = isGetClassFunc; | ||
isGetClassFunc = undefined; | ||
} | ||
const vDepends = injectorOpts && injectorOpts.depends; | ||
function abilityFn(aClass, aOptions) { | ||
@@ -106,8 +135,10 @@ let AbilityClass = abilityClass; | ||
if (aClass != null) { | ||
let $abilities, vAdditionalAbilityInjected, vExcludes, vIncludes, vInjectedOnParent; | ||
let aClassPrototype = aClass.prototype; | ||
let $abilities, vAdditionalAbilityInjected; | ||
const vTargetClass = aClass; | ||
let vClassPrototype = aClass.prototype; | ||
let vHasCoreMethod = isArray(aCoreMethod) ? aCoreMethod[0] : aCoreMethod; | ||
// TODO: Check the core method on the target class or the inheritance? | ||
if (vHasCoreMethod) { | ||
if (vHasCoreMethod[0] !== '@') { | ||
vHasCoreMethod = aClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
vHasCoreMethod = vClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
} | ||
@@ -119,102 +150,82 @@ else { | ||
} | ||
if (vName) { | ||
$abilities = aClass.prototype.$abilities; | ||
vInjectedOnParent = isInjectedOnParent(aClass, vName); | ||
if (!aClass.prototype.hasOwnProperty('$abilities')) { | ||
$abilities = null; | ||
} | ||
} | ||
if (!(vHasCoreMethod || ($abilities && $abilities['$' + vName]))) { | ||
if ((aOptions == null) || !(aOptions.include || aOptions.exclude)) { | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName); | ||
let vIncludeMembers; | ||
let vFilterMembers; | ||
const vHasIncludeOptions = aOptions && (aOptions.include || aOptions.exclude); | ||
if (vHasIncludeOptions) { | ||
let arr = aOptions.include; | ||
const hasExclude = typeof aOptions.exclude === 'string' || (aOptions.exclude && aOptions.exclude.length); | ||
if (typeof arr === 'string') { | ||
arr = [arr]; | ||
} | ||
vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
extendFilter(aClass, AbilityClass, function (k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
if (!(arr && arr.length) || hasExclude) { | ||
vIncludeMembers = getMembers(AbilityClass); | ||
} | ||
if (!vInjectedOnParent) { | ||
vExcludes = injectMembersFromNonEnum(aClassPrototype, AbilityClass.prototype); | ||
extendFilter(aClassPrototype, AbilityClass.prototype, function (k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
} | ||
} | ||
else { | ||
vIncludes = aOptions.include; | ||
if (vIncludes) { | ||
if (!isArray(vIncludes)) { | ||
vIncludes = [vIncludes]; | ||
} | ||
} | ||
else { | ||
vIncludes = []; | ||
vIncludeMembers = arr; | ||
} | ||
if (aCoreMethod) { | ||
if (isArray(aCoreMethod)) { | ||
vIncludes = vIncludes.concat(aCoreMethod); | ||
arr = aOptions.exclude; | ||
if (arr) { | ||
if (!isArray(arr)) { | ||
arr = [arr]; | ||
} | ||
else { | ||
vIncludes.push(aCoreMethod); | ||
} | ||
vIncludeMembers = vIncludeMembers.filter(item => arr.indexOf(item) === -1); | ||
} | ||
vExcludes = aOptions.exclude; | ||
if (vExcludes) { | ||
if (!isArray(vExcludes)) { | ||
vExcludes = [vExcludes]; | ||
} | ||
arrayPushOnly(vIncludeMembers, aCoreMethod); | ||
if (vIncludeMembers.length) { | ||
vFilterMembers = function filterMembers(name) { | ||
return vIncludeMembers.includes(name); | ||
}; | ||
} | ||
else { | ||
vExcludes = []; | ||
} | ||
if (vName) { | ||
const vInjectedOnParent = isInjectedOnParent(aClass, vName); | ||
// return the injected class if it has already been injected on parent | ||
if (vInjectedOnParent) { | ||
return vInjectedOnParent; | ||
} | ||
const vGenFilter = function (isStatic) { | ||
return function (k) { | ||
return filter(k, vIncludes, vExcludes, isStatic); | ||
}; | ||
}; | ||
let vAbilities = injectMembersFromNonEnum(aClass, AbilityClass, vGenFilter(true), true); | ||
vAbilities = vAbilities.concat(injectMembersFromNonEnum(aClass.prototype, AbilityClass.prototype, vGenFilter())); | ||
vExcludes = vExcludes.concat(vAbilities); | ||
vAbilities = undefined; | ||
const filterMethods = function (methods, isStatic) { | ||
if (methods instanceof Object) { | ||
const vFilter = vGenFilter(isStatic); | ||
for (const k in methods) { | ||
if (!vFilter(k)) { | ||
delete methods[k]; | ||
} | ||
} | ||
} | ||
}; | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, filterMethods); | ||
} | ||
extendFilter(aClass, AbilityClass, vGenFilter(true)); | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, aOptions); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
aClass = vAdditionalAbilityInjected; | ||
vClassPrototype = vAdditionalAbilityInjected.prototype; | ||
} | ||
if (!vInjectedOnParent) { | ||
extendFilter(aClassPrototype, AbilityClass.prototype, vGenFilter()); | ||
$abilities = aClass.prototype[abilitiesSym]; | ||
if (!vClassPrototype.hasOwnProperty(abilitiesSym)) { | ||
$abilities = null; | ||
} | ||
filterMethods(aOptions.methods); | ||
filterMethods(aOptions.classMethods); | ||
} | ||
if (!vHasIncludeOptions) { | ||
// inject the static methods | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
// inject the enumerable members | ||
extendFilter(aClass, AbilityClass, function (k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
// inject the methods | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype); | ||
extendFilter(vClassPrototype, AbilityClass.prototype, function (k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
} | ||
else { | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, vFilterMembers, true); | ||
// inject the enumerable members | ||
extendFilter(aClass, AbilityClass, function (k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers('@' + k)); | ||
}); | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype, vFilterMembers); | ||
extendFilter(vClassPrototype, AbilityClass.prototype, function (k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers(k)); | ||
}); | ||
} | ||
if (aOptions != null) { | ||
if (!vInjectedOnParent && aOptions.methods instanceof Object) { | ||
injectMethods(aClassPrototype, aOptions.methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
injectMethods(aClass, aOptions.classMethods, aOptions); | ||
} | ||
_applyAdditionalAbility(aClass, aOptions); | ||
} | ||
if (vName) { | ||
if (aClassPrototype !== aClass.prototype) { | ||
aClassPrototype = aClass.prototype; | ||
if (vClassPrototype.hasOwnProperty(abilitiesSym)) { | ||
$abilities = vClassPrototype[abilitiesSym]; | ||
} | ||
if (!aClassPrototype.hasOwnProperty('$abilities')) { | ||
else { | ||
$abilities = {}; | ||
defineProperty(aClassPrototype, '$abilities', $abilities); | ||
defineProperty(vClassPrototype, abilitiesSym, $abilities); | ||
} | ||
@@ -224,2 +235,28 @@ $abilities['$' + vName] = abilityFn; | ||
} | ||
// Apply optional dependencies | ||
if (vDepends) { | ||
Object.keys(vDepends).forEach(function (name) { | ||
let vDepend = vDepends[name]; | ||
if (vDepend) { | ||
if (!Array.isArray(vDepend)) { | ||
vDepend = [vDepend]; | ||
} | ||
vDepend.forEach(item => !item.id && (item.id = vName)); | ||
let vDependAbility = $abilities[name]; | ||
if (vDependAbility) { | ||
if (!Array.isArray(vDependAbility)) { | ||
$abilities[name] = vDependAbility = [vDependAbility]; | ||
} | ||
vDependAbility.push(vDepends[name]); | ||
} | ||
else { | ||
$abilities[name] = vDepends[name]; | ||
} | ||
const vInjectedOnParent = isInjectedOnParent(vTargetClass, name); | ||
if (vInjectedOnParent) { | ||
injectAdditionalAbility(vTargetClass, name, aOptions); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
@@ -232,60 +269,59 @@ else { | ||
; | ||
function filter(k, aIncludes, aExcludes, aIsStatic) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
let result = aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} | ||
else if (aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
else { | ||
result = true; | ||
} | ||
return result; | ||
} | ||
; | ||
abilityFn.filter = filter; | ||
/* // eslint-disable-next-line unused-imports/no-unused-vars | ||
function getAdditionalAbility(aClass, aName) { | ||
const result = []; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities') && !aClass.prototype.$abilities['$' + aName] && aClass.prototype.$abilities[aName]) { | ||
result.push(aClass); | ||
} | ||
aClass = aClass.super_; | ||
} | ||
return result; | ||
}; | ||
*/ | ||
function injectAdditionalAbility(aClass, aName, filterMethods) { | ||
function injectAdditionalAbility(aClass, aName, aOptions) { | ||
let result; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities')) { | ||
const $abilities = aClass.prototype.$abilities; | ||
let vAbility; | ||
if (!$abilities['$' + aName] && (vAbility = $abilities[aName])) { | ||
const vOptions = vAbility(); | ||
result = aClass; | ||
if (vOptions != null) { | ||
if (filterMethods) { | ||
filterMethods(vOptions.methods); | ||
filterMethods(vOptions.classMethods); | ||
let vClass = aClass; | ||
let vOnTarget = true; | ||
const vTargets = []; | ||
while (vClass && vClass.prototype) { | ||
if (vClass.prototype.hasOwnProperty(abilitiesSym)) { | ||
let vAbility = getAdditionalAbilityOptions(vClass, aName); | ||
if (vAbility) { | ||
if (!Array.isArray(vAbility)) { | ||
vAbility = [vAbility]; | ||
} | ||
for (const item of vAbility) { | ||
if (item && typeof item.getOpts === 'function') { | ||
const vOptions = item.getOpts(aOptions); | ||
if (vOptions != null) { | ||
if (item.required && item.required.length && aOptions && (aOptions.include || aOptions.exclude)) { | ||
let vMissingMethod; | ||
for (const n of item.required) { | ||
if (!filter(n, aOptions.include, aOptions.exclude)) { | ||
vMissingMethod = true; | ||
break; | ||
} | ||
} | ||
if (vMissingMethod) { | ||
continue; | ||
} | ||
} | ||
if (vOptions.id === undefined) { | ||
vOptions.id = item.id; | ||
} | ||
if (item.mode === AdditionalInjectionMode.target) { | ||
vTargets.push([aClass, vOptions, vClass]); | ||
} | ||
else { | ||
applyAdditionalAbility(vClass, aName, vOptions); | ||
vOnTarget = false; | ||
} | ||
} | ||
} | ||
if (vOptions.methods instanceof Object) { | ||
injectMethods(aClass.prototype, vOptions.methods, vOptions); | ||
} | ||
if (vOptions.classMethods instanceof Object) { | ||
injectMethods(aClass, vOptions.classMethods, vOptions); | ||
} | ||
} | ||
result = vClass; | ||
} | ||
} | ||
aClass = aClass.super_; | ||
vClass = getParentClass(vClass); | ||
} | ||
if (vTargets.length) { | ||
// apply additional ability from parent to child | ||
for (let i = vTargets.length - 1; i >= 0; i--) { | ||
const item = vTargets[i]; | ||
applyAdditionalAbility(item[0], aName, item[1], item[2]); | ||
} | ||
} | ||
if (vOnTarget) { | ||
result = aClass; | ||
} | ||
return result; | ||
@@ -297,1 +333,150 @@ } | ||
; | ||
/** | ||
* Returns the additional ability options for a specified ability class | ||
* | ||
* @param aClass - The class to which the additional ability options belong | ||
* @param aName - The name of the ability | ||
* @returns The additional ability options | ||
*/ | ||
function getAdditionalAbilityOptions(aClass, aName) { | ||
const $abilities = aClass.prototype[abilitiesSym]; | ||
const result = $abilities && $abilities[aName]; | ||
return result; | ||
} | ||
function _applyAdditionalAbility(aClass, aOptions) { | ||
if (aOptions.methods instanceof Object) { | ||
const methods = getFilteredMembers(aOptions.methods, aOptions); | ||
injectMethods(aClass.prototype, methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
const classMethods = getFilteredMembers(aOptions.classMethods, aOptions, true); | ||
injectMethods(aClass, classMethods, aOptions); | ||
} | ||
} | ||
/** | ||
* Adds an additional ability to a class based on the provided options | ||
* | ||
* @param {Function} aClass - The target class to which the additional ability will be added | ||
* @param {string} aName - The name of the additional ability | ||
* @param {AbilityOptions} aOptions - The options that describe which methods to inject | ||
* @param {Function} [fromClass] - The class from which the additional ability is being applied | ||
*/ | ||
function applyAdditionalAbility(aClass, aName, aOptions, fromClass) { | ||
if (aOptions != null) { | ||
const fromId = fromClass && aClass !== fromClass ? '_' + fromClass.name : ''; | ||
const id = aName + (aOptions.id ? '_' + aOptions.id : fromId); | ||
let $abilitiesOpt = aClass.prototype[abilitiesOptSym]; | ||
if (!$abilitiesOpt || !$abilitiesOpt[id]) { | ||
_applyAdditionalAbility(aClass, aOptions); | ||
if (!$abilitiesOpt) { | ||
$abilitiesOpt = {}; | ||
defineProperty(aClass.prototype, abilitiesOptSym, $abilitiesOpt); | ||
} | ||
$abilitiesOpt[id] = true; | ||
} | ||
} | ||
} | ||
/** | ||
* Pushes an array of items into a destination array, but only if the items are not already in the destination array | ||
* | ||
* @param dest - The destination array | ||
* @param src - The source array or item | ||
* @returns The destination array with the new items added | ||
*/ | ||
function arrayPushOnly(dest, src) { | ||
if (src !== undefined) { | ||
if (!Array.isArray(src)) { | ||
src = [src]; | ||
} | ||
src.forEach(item => { | ||
dest.indexOf(item) === -1 && dest.push(item); | ||
}); | ||
} | ||
return dest; | ||
} | ||
/** | ||
* Returns an array of all members of a class, including static and prototype members | ||
* | ||
* @param aClass - The class to get the members from | ||
* @returns An array of member names | ||
*/ | ||
function getMembers(aClass) { | ||
let result = getNonEnumNames(aClass).filter(n => !skipStaticNames.includes(n)).map(name => '@' + name); | ||
result = result.concat(Object.keys(aClass).map(name => '@' + name)); | ||
result = result.concat(getNonEnumNames(aClass.prototype).filter(n => !skipProtoNames.includes(n))); | ||
result = result.concat(Object.keys(aClass.prototype)); | ||
return result; | ||
} | ||
/** | ||
* Determines whether to include a member based on the provided options | ||
* | ||
* @private | ||
* @param {string} k - The name of the member | ||
* @param {string|string[]} aIncludes - The names of members to include | ||
* @param {string|string[]} aExcludes - The names of members to exclude | ||
* @param {boolean} [aIsStatic] - Whether the member is a static member | ||
* @returns {boolean} - Whether to include the member | ||
*/ | ||
function filter(k, aIncludes, aExcludes, aIsStatic) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
if (typeof aIncludes === 'string') { | ||
aIncludes = [aIncludes]; | ||
} | ||
if (typeof aExcludes === 'string') { | ||
aExcludes = [aExcludes]; | ||
} | ||
let result = aIncludes && aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} | ||
else if (aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
else { | ||
result = true; | ||
} | ||
return result; | ||
} | ||
; | ||
/** | ||
* Returns an object containing only the members that pass the filter | ||
* | ||
* @function | ||
* @private | ||
* @param {Object} obj - The object to filter | ||
* @param {AbilityOptions} aOptions - The options that describe which members to include | ||
* @param {boolean} [isStatic] - Whether the members are static members | ||
* @returns {Object} - An object containing only the members that pass the filter | ||
*/ | ||
function getFilteredMembers(obj, aOptions, isStatic) { | ||
const result = {}; | ||
Object.keys(obj).forEach(name => { | ||
if (filter(name, aOptions.include, aOptions.exclude, isStatic)) { | ||
result[name] = obj[name]; | ||
} | ||
}); | ||
return result; | ||
} | ||
/* | ||
function cloneObj(src: object, maxDeep = 5) { | ||
if (!src) {return src}; | ||
const result = {} | ||
Object.keys(src).forEach(key => { | ||
const value = src[key] | ||
if (Array.isArray(value)) { | ||
result[key] = value.slice() | ||
} else if (maxDeep > 0 && value instanceof Object) { | ||
--maxDeep | ||
result[key] = cloneObj(value, maxDeep) | ||
} else { | ||
result[key] = src[key] | ||
} | ||
}) | ||
return result | ||
} | ||
*/ |
@@ -1,2 +0,2 @@ | ||
import getPrototypeOf from 'inherits-ex/lib/getPrototypeOf'; | ||
import { getOwnPropValue, getPrototypeOf } from 'inherits-ex'; | ||
export function hasAbilityOnParent(aClass, aName) { | ||
@@ -10,3 +10,3 @@ let result, vPrototype; | ||
if (vPrototype.hasOwnProperty('$abilities') && vPrototype.$abilities.hasOwnProperty(aName)) { | ||
result = true; | ||
result = getOwnPropValue(vPrototype, 'Class') || vPrototype.constructor; | ||
break; | ||
@@ -13,0 +13,0 @@ } |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.hasAbilityOnParent = void 0; | ||
const getPrototypeOf_1 = __importDefault(require("inherits-ex/lib/getPrototypeOf")); | ||
const inherits_ex_1 = require("inherits-ex"); | ||
function hasAbilityOnParent(aClass, aName) { | ||
@@ -16,6 +13,6 @@ let result, vPrototype; | ||
if (vPrototype.hasOwnProperty('$abilities') && vPrototype.$abilities.hasOwnProperty(aName)) { | ||
result = true; | ||
result = (0, inherits_ex_1.getOwnPropValue)(vPrototype, 'Class') || vPrototype.constructor; | ||
break; | ||
} | ||
vPrototype = (0, getPrototypeOf_1.default)(vPrototype); | ||
vPrototype = (0, inherits_ex_1.getPrototypeOf)(vPrototype); | ||
} | ||
@@ -22,0 +19,0 @@ return result; |
{ | ||
"name": "custom-ability", | ||
"version": "2.0.0-alpha.3", | ||
"version": "2.0.0-alpha.4", | ||
"description": "make custom ability more easy. generate the ability which can be added to any class directly.", | ||
@@ -31,3 +31,3 @@ "homepage": "https://github.com/snowyu/custom-ability.js", | ||
"lint.fix": "npm run lint -- --fix", | ||
"release": "npm run clean && npm run build && git add docs && git ci -m 'docs: update API docs' && npx standard-version -s", | ||
"release": "npm run clean && npm run build && git add docs && git ci -m 'docs: update API docs' && npx commit-and-tag-version -s", | ||
"release.alpha": "npm run release -- --prerelease alpha", | ||
@@ -40,3 +40,3 @@ "test": "mocha" | ||
"dependencies": { | ||
"inherits-ex": "^2.1.0-alpha.10", | ||
"inherits-ex": "^2.1.0-alpha.11", | ||
"util-ex": "^2.0.0-alpha.8" | ||
@@ -43,0 +43,0 @@ }, |
244
README.md
@@ -7,6 +7,9 @@ # custom-ability [![Build Status](https://img.shields.io/travis/snowyu/custom-ability.js/master.png)](http://travis-ci.org/snowyu/custom-ability.js) [![npm](https://img.shields.io/npm/v/custom-ability.svg)](https://npmjs.org/package/custom-ability) [![downloads](https://img.shields.io/npm/dm/custom-ability.svg)](https://npmjs.org/package/custom-ability) [![license](https://img.shields.io/npm/l/custom-ability.svg)](https://npmjs.org/package/custom-ability) | ||
**Features:** | ||
## Features | ||
* Allows you to easily inject "abilities" (i.e. methods) from one class into another class, without needing to extend the target class. | ||
* Can inject both static and instance methods. | ||
* Allows the ability class to **overload** an existing method in the target class with its own implementation, while still being able to call the original implementation of the method in the target class | ||
* These methods should be **non-enumerable members** in the target class. All methods defined in an ES6 class are **non-enumerable**. | ||
* Which allows the ability to modify or supplement the behavior of the original method without fully replacing its functionality. | ||
* Allows you to specify a set of "core methods" for the ability class, which will be used to check if the ability has already been injected into a target class. | ||
@@ -18,18 +21,16 @@ * Prevents the same ability from being injected multiple times into the same class. | ||
* Supports optional include and exclude parameters, which allow you to specify which methods should be injected or excluded from injection. | ||
* Supports optional methods and classMethods parameters, which allow you to inject additional methods into the target class. | ||
* Supports optional methods and classMethods parameters, which allow you to inject/**overwrite** additional methods into the target class. | ||
* Supports optional additional abilities | ||
**Usage:** | ||
## Method overloading(Replace Exists Methods) | ||
1. Define an ability class that contains the methods you want to inject into other classes. | ||
2. Use the `createAbilityInjector` function to create an injector function that can inject the ability into target classes. | ||
3. Call the new function with the ability class and any optional parameters to inject the ability into a target class. | ||
All methods defined in an ES6 class are **non-enumerable**, which means that the ability injection system enables **method overloading** for all methods in the class. If a method is not defined in an ES6 class, it may be enumerable and will be overwritten directly with the ability's own implementation. | ||
**Note**: The all **non-enumerable members** on the Ability class will be injected into the target class. | ||
### The Advance Method overloading | ||
**Replace Exists Methods** | ||
if you wanna “replace” and call the methods that already exist in a class, you can add the same method name prefixed with "`$`" on the ability class, add call the original method in this way: | ||
```javascript | ||
const makeAbility = require('custom-ability') | ||
const createAbilityInjector = require('custom-ability') | ||
class Feature { | ||
@@ -50,6 +51,6 @@ // the same method name prefixed with "`$`" | ||
Feature.prototype.init = function() {this._init.apply(this, arguments)} | ||
const addFeatureTo = makeAbility(Feature) | ||
const addFeatureTo = createAbilityInjector(Feature) | ||
class My { | ||
} | ||
class My {} | ||
addFeatureTo(My) | ||
@@ -67,6 +68,14 @@ expect(My.prototype.init).toStrictEqual(Feature.prototype.init) | ||
## Usage | ||
1. Define an ability class that contains the static members or instance members you want to inject into other classes. | ||
2. Use the `createAbilityInjector` function to create an injector function that can inject the ability into target classes. | ||
3. Call the new injector function with any optional parameters to inject the ability into a target class. | ||
## Examples | ||
Suppose we wanna add the RefCount ability to any class directly. | ||
Suppose we wanna add the `RefCount` ability to any class directly. | ||
The `RefCount` ability enables `reference counting`, which helps track and manage instances of the class more easily. When a new reference to an instance is created, the addRef method is called, and when the reference is released, the release method is called. This enables the RefCount ability to keep track of the number of references to the instance and automatically free up memory when the last reference is released. This can be helpful when working with resources or objects that have complex lifecycle management requirements. | ||
the `RefCount` ability will add the following members to your class. | ||
@@ -90,3 +99,3 @@ and you should implement the `destroy` method which will be called | ||
// ability.js | ||
const makeAbility = require('custom-ability') | ||
import {createAbilityInjector} from 'custom-ability' | ||
@@ -120,3 +129,4 @@ class RefCountable { | ||
// # the first core method will be used to check the same ability whether the ability already added too. | ||
module.exports = makeAbility(RefCountable, 'addRef') | ||
export const refCountable = createAbilityInjector(RefCountable, 'addRef') | ||
export default refCountable | ||
``` | ||
@@ -147,3 +157,3 @@ | ||
// someClassMethod would not be added to the class | ||
addRefAbility(MyClass, exclude: '@someClassMethod') | ||
addRefAbility(MyClass, {exclude: '@someClassMethod'}) | ||
@@ -158,73 +168,165 @@ const my = new MyClass | ||
More complicated example, you can see the [events-ex/src/eventable.coffee](https://github.com/snowyu/events-ex.js). | ||
More complicated example, you can see the [events-ex/src/eventable.js](https://github.com/snowyu/events-ex.js). | ||
## Additional Abilities($abilities) | ||
The additional abilities injection feature provides a way to add more functionality to an injected ability by injecting other abilities that are dependent on it. This allows injected abilities to be more modular and flexible, and enables developers to compose complex behavior by combining multiple smaller abilities. The additional injection feature is especially useful when working with large, complex classes that require a lot of functionality, as it allows developers to break down the functionality into smaller, more manageable pieces that can be injected separately and combined together as needed. | ||
Another type of injection is the "**Additional Abilities**" that can be injected using the methods and classMethods parameters. These additional methods are necessary when modifying existing methods of the target class to call the old/original method to make a certain ability work. | ||
The injected methods are encapsulated in a closure. And the passed `this` object inside the closure is not the original instance object, but `self`, and the original method is referred to as `super`. | ||
The additional abilities injection feature allows injected abilities to work together and support each other. When a dependent ability is injected, any additional abilities associated with it will also be injected. For example, if a target class has the `refCountable` ability injected and the `eventable` ability is also added, the `refCountable` ability will support events because it has been configured to inject additional methods that are compatible with the eventable ability. | ||
**Note**: The methods must be **non-enumerable members** of the target class(prototype). | ||
```ts | ||
import {AbilityInjectorOptions, abilitiesSym, AdditionalInjectionMode, createAbilityInjector} from 'custom-ability'; | ||
class RefCountable { | ||
static someClassMethod() {} | ||
release() { | ||
let result = --this.RefCount | ||
if (result < 0) this.destroy() | ||
return result | ||
} | ||
free() { | ||
return this.release() | ||
} | ||
addRef() { | ||
if (!isUndefined(this.RefCount)) | ||
++this.RefCount | ||
else | ||
this.RefCount = 1 | ||
} | ||
} | ||
const injectorOptions: AbilityInjectorOptions = { | ||
depends: { | ||
Eventable: { | ||
mode: AdditionalInjectionMode.target, | ||
getOpts() { | ||
// These methods will be injected when the eventable ability is injected | ||
methods: { | ||
release() {const self = this.self; this.super(); self.emit('release', self.RefCount);}, | ||
addRef() {const self = this.self; this.super(); self.emit('addRef', self.RefCount);}, | ||
} | ||
} | ||
} | ||
} | ||
} | ||
export const refCountable = createAbilityInjector(RefCountable, 'addRef', injectorOptions) | ||
export default refCountable | ||
``` | ||
In the provided code example, the `refCountable` ability is defined using `createAbilityInjector`, and the `injectorOptions` object is passed to configure it. The `injectorOptions` object specifies that when the `Eventable` ability is injected, additional methods (`release` and `addRef`) should be injected into the target class that are compatible with the `eventable` ability. This ensures that the refCountable ability can work with the `eventable` ability and support events. | ||
The injected methods are encapsulated in a closure. And the passed `this` object inside the closure is not the original instance object, but `self`, and the original method is referred to as `super` which is already bind to original `this`. | ||
`AdditionalInjectionMode` provides flexibility in how additional abilities are injected into a target class, allowing developers to choose the mode that best fits their use case. The all mode injects additional abilities into all classes in the inheritance chain that are related to the ability being injected, while the target mode only injects the additional abilities into the target class itself. This can be useful when injecting abilities with different dependencies or when multiple abilities need to be injected into the same class. | ||
AdditionalInjectionMode is an option for createAbilityInjector that controls how additional abilities are injected into a target class. | ||
* The `all` mode is the default mode, which injects the additional ability into all classes in the target class's inheritance chain that have a `$abilities` own property containing a key that matches the ability being injected. The ability being injected is also injected into the "base" class (the farthest class in the inheritance chain that has the `$abilities` own property) rather than the target class itself. This mode is useful when the injected abilities have dependencies on the ability being injected and need to be applied to all derived classes. | ||
* The `target` mode, on the other hand, only injects the additional ability into the target class itself, rather than all classes in the inheritance chain. This mode is useful when injecting abilities with dependency parameters, such as in the refCountable example code provided earlier, where the eventable ability should be injected with method overloads on the same class where refCountable is injected, rather than on the class where refCountable is defined. | ||
**Note**: The methods must be **non-enumerable members** of the target class. | ||
**Note**: Once an ability has been injected into a target class, excluding certain methods of that ability may cause some additional abilities to become ineffective if they depend on the excluded methods. Currently, by adding a `required` parameter to the `AdditionalAbility` object, which specifies a list of method names that the additional ability must have in order to be injected into the target class. If the target class does not have these required methods, the additional ability will not be injected. | ||
BREAK CHANGE(from v1 to v2): | ||
```ts | ||
import {AbilityOptions, abilitiesSym, createAbilityInjector} from 'custom-ability'; | ||
function testableOpts(options?: AbilityOptions) { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
// CAN NOT WORK NOW | ||
// My.prototype[abilitiesSym] = { | ||
// MyAbility: testableOpts | ||
// }; | ||
// Changed to this: | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: testableOpts} | ||
}; | ||
``` | ||
In order to make certain ability to work, you need to modify some methods | ||
of the class which could call the old(original) method. this time we need | ||
the "Additional Abilities" now. eg, the event-able ability to [AbstractObject](https://github.com/snowyu/abstract-object). | ||
the "Additional Abilities" now. eg, the event-able ability to [AbstractObject@v2](https://github.com/snowyu/abstract-object). | ||
We need to send a notification event when the state of the object changes(life cycle). | ||
So the event-able of [AbstractObject](https://github.com/snowyu/abstract-object) | ||
So the event-able of [AbstractObject@v2](https://github.com/snowyu/abstract-object) | ||
should be: | ||
```js | ||
const eventable = require('events-ex/eventable') | ||
const eventableOptions = require('./eventable-options') | ||
// src/stateable.js | ||
import {AdditionalInjectionMode, createAbilityInjector} from 'custom-ability' | ||
import additionalOptions from './eventable-options' | ||
module.exports = function(aClass, aOptions){ | ||
return eventable(aClass, eventableOptions(aOptions)) | ||
//... | ||
const stateableOptions = { | ||
depends: { | ||
Eventable: { | ||
mode: AdditionalInjectionMode.target, | ||
getOpts: additionalOptions, | ||
} | ||
} | ||
} | ||
export const stateable = createAbilityInjector(Stateable, 'objectState', stateableOptions) | ||
export default stateable | ||
``` | ||
```js | ||
// eventable-options.js | ||
module.exports = function (aOptions){ | ||
if (!aOptions) aOptions = {} | ||
if (!aOptions.methods) aOptions.methods = {} | ||
extend( aOptions.methods, { | ||
// override methods: (btw: classMethods to override the class methods) | ||
setObjectState(value, emitted = true) { | ||
// The injected methods are encapsulated in a closure. | ||
// The `this` object inside the closure is not the original instance object, but `self`, and the original method is referred to as `super`. | ||
self= this.self | ||
this.super.setObjectState.call(self, value) | ||
if (emitted) self.emit(value, self) | ||
} | ||
// src/eventable-options.js | ||
export let MAX_LISTENERS = 2e308 | ||
export function eventableOptions(aOptions) { | ||
const result = {methods: {}, required: ['setMaxListeners', 'emit']} | ||
const maxListeners = (aOptions && aOptions.maxListeners) || MAX_LISTENERS | ||
extend(result.methods, { | ||
// ... | ||
setObjectState(value, emitted) { | ||
if (emitted == null) { | ||
emitted = true | ||
} | ||
const self = this.self | ||
this["super"].call(self, value) | ||
if (emitted) { | ||
self.emit(value, self) | ||
} | ||
}, | ||
// ... | ||
}) | ||
... | ||
return aOptions | ||
// more detail on [AbstractObject/src/eventable-options.coffee](https://github.com/snowyu/abstract-object) | ||
} | ||
export default eventableOptions | ||
``` | ||
But we wanna the original `eventable('events-ex/eventable')` knows the changes | ||
and use it automatically. | ||
Now, the MyClass(AbstractObject) will support the event ability when eventable the MyClass. | ||
```js | ||
const eventable = require 'events-ex/eventable' | ||
import {AbstractObject} from 'abstract-object' | ||
import {eventable} from 'events-ex' | ||
class MyClass extends AbstractObject {} | ||
// inherits MyClass, AbstractObject | ||
eventable(MyClass) | ||
``` | ||
you just do this on the AbstractObject: | ||
```js | ||
const AbstractObject = require('./lib/abstract-object') | ||
AbstractObject.$abilities = { | ||
// "Eventable" is the AbilityClass name | ||
Eventable: require('./lib/eventable-options') | ||
} | ||
module.exports = AbstractObject | ||
``` | ||
## API | ||
@@ -239,7 +341,10 @@ | ||
```js | ||
var customAbility = require('custom-ability') | ||
import {createAbilityInjector} from 'custom-ability' | ||
``` | ||
### customAbility(abilityClass: Function|object, coreMethod?: string|string[], isGetClassFunction = false): WithAbilityFn | ||
### createAbilityInjector(abilityClass: Function|object, coreMethod?: string|string[], isGetClassFunction = false, injectorOpts?: AbilityInjectorOptions): WithAbilityFn | ||
Creates a injector function that adds(injects) the ability to the target class based on the ability class. | ||
The injected abilities are provided by the `abilityClass` parameter, which is expected to be a class. The function takes the following parameters: | ||
@@ -257,4 +362,4 @@ | ||
* Whether abilityClass should be invoked with aClass and aOptions to get the actual ability class. | ||
* `injectorOpts` An optional injector options object, usage see the Additional Ability | ||
**return** | ||
@@ -264,10 +369,5 @@ | ||
the function customAbility should be modified its name. | ||
function customAbility(abilityClass: Function|object, coreMethod?: string|string[], isGetClassFunction = false): WithAbilityFn | ||
The exported function returns the injector function (`WithAbilityFn(targetClass, options?: {include?: string|string[], exclude?: string|string[], methods? : {[name: string]: Function}, , classMethods? : {[name: string]: Function}}): targetClass`) that takes two parameters: | ||
The exported function returns another function (`WithAbilityFn(targetClass, options?: {include?: string|string[], exclude?: string|string[], methods? : {[name: string]: Function}, , classMethods? : {[name: string]: Function}}): targetClass`) that takes two parameters: | ||
This custom ability injection function has two arguments: `function(class[, options])` | ||
* `class`: the target class to be injected the ability. | ||
* `targetClass`: the target class to be injected the ability. | ||
* `options` *(object)*: an optional options object that can contain the following properties: | ||
@@ -294,2 +394,10 @@ * `include`*(array|string)*: only these methods will be added(injected) to the class | ||
* NodeJS >= 12 | ||
* **broken change** `require` rename to `requireAbility` | ||
* **broken change** The additional ability options total changed. | ||
* **broken change** Add new injectorOpts option to createAbilityInjector for optional depends AdditionalAbility | ||
* **broken change** Support multi AdditionalAbilities on the same ability. The AdditionalAbity option is total changed. see AdditionalAbility type | ||
* fix: should inject the static methods on the same class for ES6 Class and inherits-ex supports static member inheritance now | ||
* refactor(**broken change**): the injector will return the injected class if already injected | ||
* fix: should not duplicate inject additional abilities on base class | ||
* fix: can not inject all inherited AdditionalAbility on ES6 Class | ||
@@ -296,0 +404,0 @@ ### V1.6.2 |
@@ -8,3 +8,3 @@ import isArray from 'util-ex/lib/is/type/array'; | ||
import {getNonEnumerableNames as getNonEnumNames} from 'util-ex/lib/get-non-enumerable-names'; | ||
import { isEmptyFunction } from 'inherits-ex'; | ||
import { getParentClass, isEmptyFunction } from 'inherits-ex'; | ||
@@ -18,2 +18,22 @@ import isInjectedOnParent from './injected-on-parent'; | ||
/** | ||
* A symbol used to mark a class's abilities | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export const abilitiesSym = '$abilities' | ||
/** | ||
* A symbol used to mark a class's additional ability whether injected | ||
* | ||
* @constant | ||
* @type {Symbol} | ||
*/ | ||
export const abilitiesOptSym = '$abilitiesOpt' | ||
/** | ||
* The additional injection mode | ||
*/ | ||
export const AdditionalInjectionMode = { all: 0, target: 1} | ||
/** | ||
* Inject non-enumerable members of the aObject into aTargetClass | ||
@@ -25,6 +45,6 @@ * | ||
* @param filter It'll be injected only when filter callback function return true, if exists | ||
* @param isStatic Whether is static members | ||
* @returns already injected members name list | ||
* @param isStatic Whether the members to be injected are static | ||
* @returns The names of members that have been injected | ||
*/ | ||
function injectMembersFromNonEnum(aTargetClass, aObject, filter?: (name:string)=>boolean, isStatic?: boolean) { | ||
function injectMembersFromNonEnum(aTargetClass: Function, aObject, filter?: (name:string)=>boolean, isStatic?: boolean) { | ||
const nonEnumNames = getNonEnumNames(aObject); | ||
@@ -77,2 +97,10 @@ const result = []; | ||
/** | ||
* An optional id for AdditionalAbility option | ||
*/ | ||
id?: string; | ||
/** | ||
* The additional injection mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* An optional list of method names to include. | ||
@@ -96,4 +124,52 @@ */ | ||
/** | ||
* An additional ability | ||
* | ||
*/ | ||
export interface AdditionalAbility { | ||
/** | ||
* the AdditionalAbilityOptions ID | ||
*/ | ||
id?: string; | ||
/** | ||
* the Additional Injection Mode | ||
*/ | ||
mode?: number; | ||
/** | ||
* the list of required methods | ||
*/ | ||
required?: string[]; | ||
/** | ||
* Returns the additional ability options if they exist | ||
* | ||
* @param options the ability Options | ||
* @returns the Additional Ability options if exists | ||
*/ | ||
getOpts: (options?: AbilityOptions) => AbilityOptions|undefined; | ||
} | ||
/** | ||
* An object mapping target ability names to additional abilities | ||
* | ||
*/ | ||
export interface AdditionalAbilities { | ||
// the key is the target ability name | ||
[key: string]: AdditionalAbility|Array<AdditionalAbility> | ||
} | ||
/** | ||
* The ability injector options | ||
* | ||
*/ | ||
export interface AbilityInjectorOptions { | ||
/** | ||
* The optional depends abilities which can work together | ||
*/ | ||
depends?: AdditionalAbilities; | ||
} | ||
/** | ||
* A function that adds(injects) the ability of a specified ability class to a target class. | ||
* | ||
* Note: Maybe the ability will be injected into the inheritance class. | ||
* | ||
* @param {Function} targetClass - The target class to which the ability will be added. | ||
@@ -112,2 +188,3 @@ * @param {AbilityOptions} [options] - An optional ability configuration object. | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -117,4 +194,6 @@ * properties and methods. | ||
*/ | ||
export function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean): AbilityFn; | ||
export function createAbilityInjector(abilityClass: Function, aCoreMethod?: string|string[], isGetClassFunc?: boolean): AbilityFn; | ||
export function createAbilityInjector(abilityClass: Function, isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export function createAbilityInjector(abilityClass: Function, aCoreMethod?: string|string[], isGetClassFunc?: boolean, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export function createAbilityInjector(abilityClass: Function, aCoreMethod?: string|string[], injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
export function createAbilityInjector(abilityClass: Function, injectorOpts?: AbilityInjectorOptions): AbilityFn; | ||
/** | ||
@@ -130,2 +209,3 @@ * Creates a function that adds(injects) the ability to the target class based on the ability class. | ||
* with aClass and aOptions to get the actual ability class. defaults to false | ||
* @param injectorOpts An optional injector options object | ||
* @returns Another function that accepts the target class and options to include or exclude specific | ||
@@ -135,8 +215,18 @@ * properties and methods. | ||
*/ | ||
export function createAbilityInjector(abilityClass: Function, aCoreMethod?: string|string[]|boolean, isGetClassFunc?: boolean): AbilityFn { | ||
export function createAbilityInjector(abilityClass: Function, aCoreMethod?: string|string[]|boolean|AbilityInjectorOptions, isGetClassFunc?: boolean|AbilityInjectorOptions, injectorOpts?: AbilityInjectorOptions): AbilityFn { | ||
if (typeof aCoreMethod === 'boolean') { | ||
injectorOpts = isGetClassFunc as AbilityInjectorOptions; | ||
isGetClassFunc = aCoreMethod; | ||
aCoreMethod = undefined; | ||
} else if (typeof aCoreMethod === 'object') { | ||
injectorOpts = aCoreMethod as AbilityInjectorOptions; | ||
aCoreMethod = undefined; | ||
} | ||
if (isGetClassFunc !== undefined && typeof isGetClassFunc !== 'boolean') { | ||
injectorOpts = isGetClassFunc; | ||
isGetClassFunc = undefined; | ||
} | ||
const vDepends = injectorOpts && injectorOpts.depends; | ||
function abilityFn(aClass, aOptions?) { | ||
@@ -153,10 +243,12 @@ let AbilityClass = abilityClass; | ||
if (aClass != null) { | ||
let $abilities, vAdditionalAbilityInjected, vExcludes, vIncludes, vInjectedOnParent; | ||
let $abilities, vAdditionalAbilityInjected; | ||
const vTargetClass = aClass; | ||
let aClassPrototype = aClass.prototype; | ||
let vClassPrototype = aClass.prototype; | ||
let vHasCoreMethod = isArray(aCoreMethod) ? aCoreMethod[0] : aCoreMethod as string; | ||
// TODO: Check the core method on the target class or the inheritance? | ||
if (vHasCoreMethod) { | ||
if (vHasCoreMethod[0] !== '@') { | ||
vHasCoreMethod = aClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
vHasCoreMethod = vClassPrototype.hasOwnProperty(vHasCoreMethod); | ||
} else { | ||
@@ -167,102 +259,83 @@ vHasCoreMethod = vHasCoreMethod.substring(1); | ||
} | ||
if (vName) { | ||
$abilities = aClass.prototype.$abilities; | ||
vInjectedOnParent = isInjectedOnParent(aClass, vName); | ||
if (!aClass.prototype.hasOwnProperty('$abilities')) { | ||
$abilities = null; | ||
} | ||
} | ||
if (!(vHasCoreMethod || ($abilities && $abilities['$' + vName]))) { | ||
if ((aOptions == null) || !(aOptions.include || aOptions.exclude)) { | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName); | ||
} | ||
vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
extendFilter(aClass, AbilityClass, function(k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
} | ||
if (!vInjectedOnParent) { | ||
vExcludes = injectMembersFromNonEnum(aClassPrototype, AbilityClass.prototype); | ||
extendFilter(aClassPrototype, AbilityClass.prototype, function(k) { | ||
return !(vExcludes.indexOf(k) >= 0); | ||
}); | ||
} | ||
} else { | ||
vIncludes = aOptions.include; | ||
if (vIncludes) { | ||
if (!isArray(vIncludes)) { | ||
vIncludes = [vIncludes]; | ||
} | ||
let vIncludeMembers!: Array<string> | ||
let vFilterMembers!: (name: string) => boolean | ||
const vHasIncludeOptions = aOptions && (aOptions.include || aOptions.exclude) | ||
if (vHasIncludeOptions) { | ||
let arr = aOptions.include | ||
const hasExclude = typeof aOptions.exclude === 'string' || (aOptions.exclude && aOptions.exclude.length) | ||
if (typeof arr === 'string') {arr = [arr]} | ||
if (!(arr && arr.length) || hasExclude) { | ||
vIncludeMembers = getMembers(AbilityClass) | ||
} else { | ||
vIncludes = []; | ||
vIncludeMembers = arr | ||
} | ||
if (aCoreMethod) { | ||
if (isArray(aCoreMethod)) { | ||
vIncludes = vIncludes.concat(aCoreMethod); | ||
} else { | ||
vIncludes.push(aCoreMethod); | ||
} | ||
arr = aOptions.exclude | ||
if (arr) { | ||
if (!isArray(arr)) {arr = [arr]} | ||
vIncludeMembers = vIncludeMembers.filter(item => arr.indexOf(item)=== -1); | ||
} | ||
vExcludes = aOptions.exclude; | ||
if (vExcludes) { | ||
if (!isArray(vExcludes)) { | ||
vExcludes = [vExcludes]; | ||
arrayPushOnly(vIncludeMembers, aCoreMethod) | ||
if (vIncludeMembers.length) { | ||
vFilterMembers = function filterMembers(name) { | ||
return vIncludeMembers.includes(name); | ||
} | ||
} else { | ||
vExcludes = []; | ||
} | ||
} | ||
const vGenFilter = function (isStatic?: boolean) { | ||
return function(k) { | ||
return filter(k, vIncludes, vExcludes, isStatic); | ||
}; | ||
}; | ||
if (vName) { | ||
const vInjectedOnParent = isInjectedOnParent(aClass, vName); | ||
// return the injected class if it has already been injected on parent | ||
if (vInjectedOnParent) {return vInjectedOnParent} | ||
let vAbilities = injectMembersFromNonEnum(aClass, AbilityClass, vGenFilter(true), true); | ||
vAbilities = vAbilities.concat(injectMembersFromNonEnum(aClass.prototype, AbilityClass.prototype, vGenFilter())); | ||
vExcludes = vExcludes.concat(vAbilities); | ||
vAbilities = undefined; | ||
const filterMethods = function (methods, isStatic?: boolean) { | ||
if (methods instanceof Object) { | ||
const vFilter = vGenFilter(isStatic); | ||
for (const k in methods) { | ||
if (!vFilter(k)) { | ||
delete methods[k]; | ||
} | ||
} | ||
} | ||
}; | ||
if (vName) { | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, filterMethods); | ||
} | ||
extendFilter(aClass, AbilityClass, vGenFilter(true)); | ||
vAdditionalAbilityInjected = injectAdditionalAbility(aClass, vName, aOptions); | ||
if (vAdditionalAbilityInjected) { | ||
aClassPrototype = vAdditionalAbilityInjected.prototype; | ||
aClass = vAdditionalAbilityInjected | ||
vClassPrototype = vAdditionalAbilityInjected.prototype; | ||
} | ||
if (!vInjectedOnParent) { | ||
extendFilter(aClassPrototype, AbilityClass.prototype, vGenFilter()); | ||
$abilities = aClass.prototype[abilitiesSym]; | ||
if (!vClassPrototype.hasOwnProperty(abilitiesSym)) { | ||
$abilities = null; | ||
} | ||
filterMethods(aOptions.methods); | ||
filterMethods(aOptions.classMethods); | ||
} | ||
if (!vHasIncludeOptions) { | ||
// inject the static methods | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, null, true); | ||
// inject the enumerable members | ||
extendFilter(aClass, AbilityClass, function(k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
// inject the methods | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype); | ||
extendFilter(vClassPrototype, AbilityClass.prototype, function(k) { | ||
return (vExcludes.indexOf(k) === -1); | ||
}); | ||
} else { | ||
let vExcludes = injectMembersFromNonEnum(aClass, AbilityClass, vFilterMembers, true); | ||
// inject the enumerable members | ||
extendFilter(aClass, AbilityClass, function(k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers('@' + k)); | ||
}); | ||
vExcludes = injectMembersFromNonEnum(vClassPrototype, AbilityClass.prototype, vFilterMembers); | ||
extendFilter(vClassPrototype, AbilityClass.prototype, function(k) { | ||
return (vExcludes.indexOf(k) === -1 && vFilterMembers(k)); | ||
}); | ||
} | ||
if (aOptions != null) { | ||
if (!vInjectedOnParent && aOptions.methods instanceof Object) { | ||
injectMethods(aClassPrototype, aOptions.methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
injectMethods(aClass, aOptions.classMethods, aOptions); | ||
} | ||
_applyAdditionalAbility(aClass, aOptions) | ||
} | ||
if (vName) { | ||
if (aClassPrototype !== aClass.prototype) { | ||
aClassPrototype = aClass.prototype; | ||
} | ||
if (!aClassPrototype.hasOwnProperty('$abilities')) { | ||
if (vClassPrototype.hasOwnProperty(abilitiesSym)) { | ||
$abilities = vClassPrototype[abilitiesSym]; | ||
} else { | ||
$abilities = {}; | ||
defineProperty(aClassPrototype, '$abilities', $abilities); | ||
defineProperty(vClassPrototype, abilitiesSym, $abilities); | ||
} | ||
@@ -272,2 +345,25 @@ $abilities['$' + vName] = abilityFn; | ||
} | ||
// Apply optional dependencies | ||
if (vDepends) { | ||
Object.keys(vDepends).forEach(function (name) { | ||
let vDepend = vDepends[name] | ||
if (vDepend) { | ||
if (!Array.isArray(vDepend)) {vDepend = [vDepend]} | ||
vDepend.forEach(item => !item.id && (item.id = vName)) | ||
let vDependAbility = $abilities[name] | ||
if (vDependAbility) { | ||
if (!Array.isArray(vDependAbility)) {$abilities[name] = vDependAbility = [vDependAbility]} | ||
vDependAbility.push(vDepends[name]) | ||
} else { | ||
$abilities[name] = vDepends[name] | ||
} | ||
const vInjectedOnParent = isInjectedOnParent(vTargetClass, name); | ||
if (vInjectedOnParent) { | ||
injectAdditionalAbility(vTargetClass, name, aOptions) | ||
} | ||
} | ||
}) | ||
} | ||
} else { | ||
@@ -279,60 +375,51 @@ aClass = AbilityClass; | ||
function filter(k, aIncludes, aExcludes, aIsStatic) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
let result = aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} else if (aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} else { | ||
result = true; | ||
} | ||
return result; | ||
}; | ||
abilityFn.filter = filter; | ||
/* // eslint-disable-next-line unused-imports/no-unused-vars | ||
function getAdditionalAbility(aClass, aName) { | ||
const result = []; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities') && !aClass.prototype.$abilities['$' + aName] && aClass.prototype.$abilities[aName]) { | ||
result.push(aClass); | ||
} | ||
aClass = aClass.super_; | ||
} | ||
return result; | ||
}; | ||
*/ | ||
function injectAdditionalAbility(aClass, aName, filterMethods?) { | ||
function injectAdditionalAbility(aClass, aName, aOptions) { | ||
let result; | ||
while (aClass && aClass.prototype) { | ||
if (aClass.prototype.hasOwnProperty('$abilities')) { | ||
const $abilities = aClass.prototype.$abilities; | ||
let vAbility; | ||
if (!$abilities['$' + aName] && (vAbility = $abilities[aName])) { | ||
const vOptions = vAbility(); | ||
result = aClass; | ||
if (vOptions != null) { | ||
if (filterMethods) { | ||
filterMethods(vOptions.methods); | ||
filterMethods(vOptions.classMethods); | ||
let vClass = aClass; | ||
let vOnTarget = true; | ||
const vTargets = [] | ||
while (vClass && vClass.prototype) { | ||
if (vClass.prototype.hasOwnProperty(abilitiesSym)) { | ||
let vAbility = getAdditionalAbilityOptions(vClass, aName); | ||
if (vAbility) { | ||
if (!Array.isArray(vAbility)) {vAbility = [vAbility]} | ||
for (const item of vAbility) { | ||
if (item && typeof item.getOpts === 'function') { | ||
const vOptions = item.getOpts(aOptions); | ||
if (vOptions != null) { | ||
if (item.required && item.required.length && aOptions && (aOptions.include || aOptions.exclude)) { | ||
let vMissingMethod | ||
for (const n of item.required) { | ||
if (!filter(n, aOptions.include, aOptions.exclude)) { | ||
vMissingMethod = true | ||
break | ||
} | ||
} | ||
if (vMissingMethod) {continue} | ||
} | ||
if (vOptions.id === undefined) {vOptions.id = item.id} | ||
if (item.mode === AdditionalInjectionMode.target) { | ||
vTargets.push([aClass, vOptions, vClass]) | ||
} else { | ||
applyAdditionalAbility(vClass, aName, vOptions) | ||
vOnTarget = false | ||
} | ||
} | ||
} | ||
if (vOptions.methods instanceof Object) { | ||
injectMethods(aClass.prototype, vOptions.methods, vOptions); | ||
} | ||
if (vOptions.classMethods instanceof Object) { | ||
injectMethods(aClass, vOptions.classMethods, vOptions); | ||
} | ||
} | ||
result = vClass; | ||
} | ||
} | ||
aClass = aClass.super_; | ||
vClass = getParentClass(vClass); | ||
} | ||
if (vTargets.length) { | ||
// apply additional ability from parent to child | ||
for (let i = vTargets.length - 1; i>=0; i--) { | ||
const item = vTargets[i] | ||
applyAdditionalAbility(item[0], aName, item[1], item[2]) | ||
} | ||
} | ||
if (vOnTarget) {result = aClass} | ||
return result; | ||
@@ -343,1 +430,151 @@ }; | ||
}; | ||
/** | ||
* Returns the additional ability options for a specified ability class | ||
* | ||
* @param aClass - The class to which the additional ability options belong | ||
* @param aName - The name of the ability | ||
* @returns The additional ability options | ||
*/ | ||
function getAdditionalAbilityOptions(aClass: Function, aName: string) { | ||
const $abilities = aClass.prototype[abilitiesSym]; | ||
const result: AdditionalAbility|Array<AdditionalAbility> = $abilities && $abilities[aName]; | ||
return result; | ||
} | ||
function _applyAdditionalAbility(aClass, aOptions) { | ||
if (aOptions.methods instanceof Object) { | ||
const methods = getFilteredMembers(aOptions.methods, aOptions) | ||
injectMethods(aClass.prototype, methods, aOptions); | ||
} | ||
if (aOptions.classMethods instanceof Object) { | ||
const classMethods = getFilteredMembers(aOptions.classMethods, aOptions, true) | ||
injectMethods(aClass, classMethods, aOptions); | ||
} | ||
} | ||
/** | ||
* Adds an additional ability to a class based on the provided options | ||
* | ||
* @param {Function} aClass - The target class to which the additional ability will be added | ||
* @param {string} aName - The name of the additional ability | ||
* @param {AbilityOptions} aOptions - The options that describe which methods to inject | ||
* @param {Function} [fromClass] - The class from which the additional ability is being applied | ||
*/ | ||
function applyAdditionalAbility(aClass, aName, aOptions, fromClass?) { | ||
if (aOptions != null) { | ||
const fromId = fromClass && aClass !== fromClass ? '_' + fromClass.name : '' | ||
const id = aName + (aOptions.id ? '_' + aOptions.id : fromId) | ||
let $abilitiesOpt = aClass.prototype[abilitiesOptSym] | ||
if (!$abilitiesOpt || !$abilitiesOpt[id]) { | ||
_applyAdditionalAbility(aClass, aOptions) | ||
if (!$abilitiesOpt) { | ||
$abilitiesOpt = {} | ||
defineProperty(aClass.prototype, abilitiesOptSym, $abilitiesOpt); | ||
} | ||
$abilitiesOpt[id] = true; | ||
} | ||
} | ||
} | ||
/** | ||
* Pushes an array of items into a destination array, but only if the items are not already in the destination array | ||
* | ||
* @param dest - The destination array | ||
* @param src - The source array or item | ||
* @returns The destination array with the new items added | ||
*/ | ||
function arrayPushOnly(dest: Array<any>, src: Array<any>|any) { | ||
if (src !== undefined) { | ||
if (!Array.isArray(src)) {src = [src]} | ||
src.forEach(item => { | ||
dest.indexOf(item) === -1 && dest.push(item) | ||
}); | ||
} | ||
return dest; | ||
} | ||
/** | ||
* Returns an array of all members of a class, including static and prototype members | ||
* | ||
* @param aClass - The class to get the members from | ||
* @returns An array of member names | ||
*/ | ||
function getMembers(aClass: Function) { | ||
let result: Array<string> = getNonEnumNames(aClass).filter(n => !skipStaticNames.includes(n)).map(name => '@' + name) | ||
result = result.concat(Object.keys(aClass).map(name => '@' + name)) | ||
result = result.concat(getNonEnumNames(aClass.prototype).filter(n => !skipProtoNames.includes(n))) | ||
result = result.concat(Object.keys(aClass.prototype)) | ||
return result | ||
} | ||
/** | ||
* Determines whether to include a member based on the provided options | ||
* | ||
* @private | ||
* @param {string} k - The name of the member | ||
* @param {string|string[]} aIncludes - The names of members to include | ||
* @param {string|string[]} aExcludes - The names of members to exclude | ||
* @param {boolean} [aIsStatic] - Whether the member is a static member | ||
* @returns {boolean} - Whether to include the member | ||
*/ | ||
function filter(k, aIncludes, aExcludes, aIsStatic?: boolean) { | ||
if (aIsStatic) { | ||
k = '@' + k; | ||
} | ||
if (typeof aIncludes === 'string') {aIncludes = [aIncludes]} | ||
if (typeof aExcludes === 'string') {aExcludes = [aExcludes]} | ||
let result = aIncludes && aIncludes.length; | ||
if (result) { | ||
result = aIncludes.indexOf(k) >= 0; | ||
if (!result && aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} | ||
} else if (aExcludes && aExcludes.length) { | ||
result = !(aExcludes.indexOf(k) >= 0); | ||
} else { | ||
result = true; | ||
} | ||
return result; | ||
}; | ||
/** | ||
* Returns an object containing only the members that pass the filter | ||
* | ||
* @function | ||
* @private | ||
* @param {Object} obj - The object to filter | ||
* @param {AbilityOptions} aOptions - The options that describe which members to include | ||
* @param {boolean} [isStatic] - Whether the members are static members | ||
* @returns {Object} - An object containing only the members that pass the filter | ||
*/ | ||
function getFilteredMembers(obj, aOptions, isStatic?: boolean) { | ||
const result = {} | ||
Object.keys(obj).forEach(name => { | ||
if (filter(name, aOptions.include, aOptions.exclude, isStatic)) { | ||
result[name] = obj[name] | ||
} | ||
}) | ||
return result | ||
} | ||
/* | ||
function cloneObj(src: object, maxDeep = 5) { | ||
if (!src) {return src}; | ||
const result = {} | ||
Object.keys(src).forEach(key => { | ||
const value = src[key] | ||
if (Array.isArray(value)) { | ||
result[key] = value.slice() | ||
} else if (maxDeep > 0 && value instanceof Object) { | ||
--maxDeep | ||
result[key] = cloneObj(value, maxDeep) | ||
} else { | ||
result[key] = src[key] | ||
} | ||
}) | ||
return result | ||
} | ||
*/ |
@@ -1,2 +0,2 @@ | ||
import getPrototypeOf from 'inherits-ex/lib/getPrototypeOf'; | ||
import {getOwnPropValue, getPrototypeOf} from 'inherits-ex'; | ||
@@ -11,3 +11,3 @@ export function hasAbilityOnParent(aClass: Function, aName: string) { | ||
if (vPrototype.hasOwnProperty('$abilities') && vPrototype.$abilities.hasOwnProperty(aName)) { | ||
result = true; | ||
result = getOwnPropValue(vPrototype, 'Class') || vPrototype.constructor; | ||
break; | ||
@@ -14,0 +14,0 @@ } |
@@ -10,3 +10,3 @@ import chai, { expect } from 'chai'; | ||
import {inherits, defineProperty} from 'inherits-ex' | ||
import {createAbilityInjector as customAbility} from '../src/custom-ability'; | ||
import {AdditionalInjectionMode, abilitiesSym, abilitiesOptSym, createAbilityInjector} from '../src/custom-ability'; | ||
@@ -46,3 +46,3 @@ var setImmediate = setImmediate || process.nextTick; | ||
const testable = customAbility(MyAbility, 'emit'); | ||
const testable = createAbilityInjector(MyAbility, 'emit'); | ||
beforeEach(function() { | ||
@@ -76,3 +76,3 @@ all_stub_reset(MyAbility); | ||
const addFeatureTo = customAbility(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
const addFeatureTo = createAbilityInjector(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
@@ -100,3 +100,3 @@ class MyClass { | ||
const addFeatureTo = customAbility(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
const addFeatureTo = createAbilityInjector(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
@@ -127,3 +127,3 @@ class MyClass { | ||
MyFeature.additionalClassMethod = function() {} | ||
const addFeatureTo = customAbility(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
const addFeatureTo = createAbilityInjector(MyFeature, ['coreAbilityMethod', '@coreAbilityClassMethod']); | ||
@@ -146,3 +146,3 @@ function emptyMethod(){console.log("MyClass")} | ||
}; | ||
testable1 = customAbility(getAbilityClass, true); | ||
testable1 = createAbilityInjector(getAbilityClass, true); | ||
result = testable1(My); | ||
@@ -154,3 +154,3 @@ result.should.be.equal(My); | ||
var My, testable1; | ||
testable1 = customAbility(MyAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
My = testable1(); | ||
@@ -161,3 +161,3 @@ return My.should.be.equal(MyAbility); | ||
var My, testable1; | ||
testable1 = customAbility(MyAbility, '@cone'); | ||
testable1 = createAbilityInjector(MyAbility, '@cone'); | ||
My = (function() { | ||
@@ -178,3 +178,3 @@ class My { | ||
var k, result, results, testable1; | ||
testable1 = customAbility(MyAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
class Root {}; | ||
@@ -189,11 +189,10 @@ class My {}; | ||
result = testable1(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); | ||
for (k in MyAbility) { | ||
should.exist(result[k], k); | ||
} | ||
results = []; | ||
for (k in MyAbility.prototype) { | ||
results.push(result.prototype.should.not.have.ownProperty(k)); | ||
My.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.have.property(k); | ||
} | ||
return results; | ||
}); | ||
@@ -208,4 +207,4 @@ it('should add multi abilities on same class', function() { | ||
}; | ||
testable1 = customAbility(MyAbility); | ||
testable2 = customAbility(OtherAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
testable2 = createAbilityInjector(OtherAbility); | ||
Root = class Root {}; | ||
@@ -232,3 +231,3 @@ My = (function() { | ||
result = testable1(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); | ||
for (k in MyAbility) { | ||
@@ -238,3 +237,4 @@ should.exist(result[k], k); | ||
for (k in MyAbility.prototype) { | ||
result.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.have.property(k); | ||
} | ||
@@ -253,11 +253,10 @@ result = testable2(Root); | ||
result = testable2(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); | ||
for (k in OtherAbility) { | ||
should.exist(result[k], k); | ||
} | ||
results = []; | ||
for (k in OtherAbility.prototype) { | ||
results.push(result.prototype.should.not.have.ownProperty(k)); | ||
My.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.have.property(k); | ||
} | ||
return results; | ||
}); | ||
@@ -286,3 +285,3 @@ it('should add methods and class methods to a class', function() { | ||
}; | ||
testable1 = customAbility(fn, 'emit', true); | ||
testable1 = createAbilityInjector(fn, 'emit', true); | ||
My = function() {}; | ||
@@ -328,3 +327,3 @@ testable1(My).should.be.equal(My); | ||
}; | ||
testable1 = customAbility(fn, 'emit', true); | ||
testable1 = createAbilityInjector(fn, 'emit', true); | ||
My = function() {}; | ||
@@ -471,182 +470,335 @@ testable1(My).should.be.equal(My); | ||
}); | ||
it('should use additional abilities', function() { | ||
var opt, testableOpts; | ||
testableOpts = function() { | ||
return { | ||
methods: { | ||
addtional: function() {} | ||
} | ||
describe('AdditionalAbility', () => { | ||
it('should use additional abilities', function() { | ||
var opt, testableOpts; | ||
testableOpts = function() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
}; | ||
class My { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
class My { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
My.prototype.$abilities = { | ||
MyAbility: testableOpts | ||
}; | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: testableOpts} | ||
}; | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('addtional'); | ||
myAbilityCheck(My); | ||
}); | ||
it('should use inherited additional abilities', function() { | ||
var my, myOpts, opt, overMy, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
overMy = sinon.spy(function() { | ||
return My.__super__.over.apply(this, arguments); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
myAbilityCheck(My); | ||
}); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
it('should use inherited additional abilities', function() { | ||
var my, myOpts, opt, overMy, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
overMy = sinon.spy(function() { | ||
return My.__super__.over.apply(this, arguments); | ||
}); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
}; | ||
myOpts = function() { | ||
return { | ||
methods: { | ||
addtional: function() {}, | ||
over: overMy | ||
} | ||
myOpts = function() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
over: overMy | ||
} | ||
}; | ||
}; | ||
}; | ||
class Root { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
class Root { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
Root.prototype.$abilities = { | ||
MyAbility: rootOpts | ||
}; | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts} | ||
}; | ||
class My { | ||
static __super__: any; | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
class My { | ||
static __super__: any; | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
inherits(My, Root); | ||
inherits(My, Root); | ||
My.prototype.$abilities = { | ||
MyAbility: myOpts | ||
}; | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: myOpts} | ||
}; | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('addtional'); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
my = new My(); | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
it('should use additional ability via multi classes', function() { | ||
var opt, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.not.ownProperty('root'); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
Root.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
my = new My(); | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
it('should use additional ability via multi classes', function() { | ||
var opt, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
}; | ||
class Root { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
class Root { | ||
$abilities: { MyAbility: any; }; | ||
}; | ||
Root.prototype.$abilities = { | ||
MyAbility: rootOpts | ||
}; | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts} | ||
}; | ||
class My {}; | ||
class My {}; | ||
inherits(My, Root); | ||
inherits(My, Root); | ||
class My1 {}; | ||
class My1 {}; | ||
inherits(My1, Root); | ||
inherits(My1, Root); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.property('over'); | ||
myAbilityCheck(My); | ||
testable(My1); | ||
My1.prototype.should.have.property('root'); | ||
My1.prototype.should.have.property('over'); | ||
myAbilityCheck(My1); | ||
}); | ||
it('should use additional ability via multi inherited classes', function() { | ||
var k, ref, v; | ||
class Root { | ||
$abilities: { MyAbility: () => { methods: { additional: () => void; two: () => void; }; }; }; | ||
}; | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.not.have.ownProperty('root'); | ||
My.prototype.should.have.property('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
Root.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
testable(My1); | ||
My1.prototype.should.have.property('root'); | ||
My1.prototype.should.have.property('over'); | ||
myAbilityCheck(My1); | ||
}); | ||
it('should use additional ability via multi inherited classes', function() { | ||
var k, ref, v; | ||
class Root { | ||
// $abilities: { MyAbility: () => { methods: { additional: () => void; two: () => void; }; }; }; | ||
}; | ||
Root.prototype.$abilities = { | ||
MyAbility: function() { // additinal ability to MyAbility | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts() { // additional ability to MyAbility | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
two: function() {} | ||
} | ||
}; | ||
}} | ||
}; | ||
class Mid { | ||
// $abilities: { MyAbility: () => { methods: { additional: () => any; iok: () => void; }; }; }; | ||
static __super__: any; | ||
}; | ||
inherits(Mid, Root); | ||
Mid.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: function() { | ||
// additional ability to MyAbility | ||
return { | ||
methods: { | ||
additional: function() { | ||
return Mid.__super__.additional.apply(this, arguments); | ||
}, | ||
iok: function() {} | ||
} | ||
}; | ||
}} | ||
}; | ||
class A {}; | ||
inherits(A, Mid); | ||
testable(A); // make the class A testable. | ||
myAbilityCheck(A); | ||
// A should have all static methods of Test | ||
// Mid,Root should have no any methods of Test | ||
for (k in MyAbility) { | ||
v = MyAbility[k]; | ||
Mid.should.not.have.ownProperty(k); | ||
Root.should.have.ownProperty(k); | ||
A.should.not.have.ownProperty(k); | ||
v.should.be.equal(A[k]); | ||
} | ||
ref = MyAbility.prototype; | ||
// A and Mid should have no any methods of Test | ||
// the Root should have all methods of Test | ||
for (k in ref) { | ||
v = ref[k]; | ||
A.prototype.should.not.have.ownProperty(k); | ||
Mid.prototype.should.not.have.ownProperty(k); | ||
Root.prototype.should.have.ownProperty(k); | ||
} | ||
// Root should have additional methods: | ||
Root.prototype.should.have.ownProperty('additional'); | ||
Root.prototype.should.have.ownProperty('two'); | ||
Mid.prototype.should.have.ownProperty('additional'); | ||
Mid.prototype.should.have.ownProperty('iok'); | ||
}); | ||
it('should not inject additional ability if no all required methods exists', function() { | ||
function testableOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
two: function() {} | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
class My {} | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: testableOpts, required: ['one', 'two']} | ||
}; | ||
const opt = {exclude: 'one'}; | ||
testable(My, opt); | ||
My.prototype.should.not.have.ownProperty('additional'); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
}); | ||
it('should inject additional ability via depends in injectorOptions', function() { | ||
function testableOpts() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
class MyAbility1 { | ||
additional2() {} | ||
} | ||
}; | ||
class Mid { | ||
$abilities: { MyAbility: () => { methods: { additional: () => any; iok: () => void; }; }; }; | ||
static __super__: any; | ||
}; | ||
const testable1 = createAbilityInjector(MyAbility1, {depends: { | ||
MyAbility: { | ||
getOpts: testableOpts | ||
} | ||
}}); | ||
inherits(Mid, Root); | ||
function My() {} | ||
Mid.prototype.$abilities = { | ||
MyAbility: function() { // additinal ability to MyAbility | ||
let opt = {}; | ||
testable1(My, opt); | ||
My.prototype.should.have.ownProperty('additional2'); | ||
My.prototype.should.have.ownProperty(abilitiesSym); | ||
expect(My.prototype[abilitiesSym]).to.have.ownProperty('MyAbility') | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility_MyAbility1', true) | ||
My.prototype.should.have.ownProperty('additional'); | ||
}); | ||
it('should inject additional ability via depends in injectorOptions after already injected ability', function() { | ||
function testableOpts() { | ||
return { | ||
methods: { | ||
additional: function() { | ||
return Mid.__super__.additional.apply(this, arguments); | ||
}, | ||
iok: function() {} | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
class MyAbility1 { | ||
additional2() {} | ||
} | ||
}; | ||
class A {}; | ||
const testable1 = createAbilityInjector(MyAbility1, {depends: { | ||
MyAbility: { | ||
getOpts: testableOpts | ||
} | ||
}}); | ||
inherits(A, Mid); | ||
function My() {} | ||
testable(A); // make the class A testable. | ||
let opt = {}; | ||
testable(My, opt); | ||
myAbilityCheck(My) | ||
My.prototype.should.have.ownProperty(abilitiesSym); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
myAbilityCheck(A); | ||
// A should have all static methods of Test | ||
// Mid,Root should have no any methods of Test | ||
for (k in MyAbility) { | ||
v = MyAbility[k]; | ||
Mid.should.not.have.ownProperty(k); | ||
Root.should.not.have.ownProperty(k); | ||
A.should.have.ownProperty(k); | ||
v.should.be.equal(A[k]); | ||
} | ||
ref = MyAbility.prototype; | ||
// A and Mid should have no any methods of Test | ||
// the Root should have all methods of Test | ||
for (k in ref) { | ||
v = ref[k]; | ||
A.prototype.should.not.have.ownProperty(k); | ||
Mid.prototype.should.not.have.ownProperty(k); | ||
Root.prototype.should.have.ownProperty(k); | ||
} | ||
// Root should have additional methods: | ||
Root.prototype.should.have.ownProperty('additional'); | ||
Root.prototype.should.have.ownProperty('two'); | ||
Mid.prototype.should.have.ownProperty('additional'); | ||
Mid.prototype.should.have.ownProperty('iok'); | ||
testable1(My, opt); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility_MyAbility1', true) | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.ownProperty('additional2'); | ||
}); | ||
it('should inject additional abilities on target only after mode is target', function() { | ||
// all additional ability on inheritance classes are injected to target class together. | ||
// and can be super call. | ||
const overRoot = sinon.spy(); | ||
const overMy = sinon.spy(function() { | ||
return this.super(...arguments) | ||
}); | ||
function rootOpts() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
function myOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
over: overMy | ||
} | ||
}; | ||
}; | ||
class Root { | ||
root: Function | ||
over: Function | ||
} | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts, mode: AdditionalInjectionMode.target} | ||
}; | ||
class My extends Root {} | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: myOpts, mode: AdditionalInjectionMode.target} | ||
}; | ||
const opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.ownProperty('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
Root.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
// expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
let my = new My; | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
}); | ||
@@ -684,3 +836,3 @@ | ||
oneTestable = customAbility(OneAbility, 'emit'); | ||
oneTestable = createAbilityInjector(OneAbility, 'emit'); | ||
beforeEach(function() { | ||
@@ -794,3 +946,3 @@ OneAbility.prototype.$init.resetHistory(); | ||
oneTestable = customAbility(OneAbility, 'emit'); | ||
oneTestable = createAbilityInjector(OneAbility, 'emit'); | ||
beforeEach(function() { | ||
@@ -896,3 +1048,3 @@ var k, ref, v; | ||
} | ||
var testable = customAbility(TheAbility, 'emit'); | ||
var testable = createAbilityInjector(TheAbility, 'emit'); | ||
it('should call init correctly', () => { | ||
@@ -899,0 +1051,0 @@ var oInit = sinon.spy() |
@@ -5,3 +5,3 @@ import chai from 'chai'; | ||
const should = chai.should(); | ||
import { assert } from 'chai'; | ||
import { assert, expect } from 'chai'; | ||
chai.use(sinonChai); | ||
@@ -11,3 +11,3 @@ import path from 'path'; | ||
import defineProperty from 'util-ex/lib/defineProperty'; | ||
import {createAbilityInjector as customAbility} from '../src/custom-ability'; | ||
import {AdditionalInjectionMode, abilitiesSym, abilitiesOptSym, createAbilityInjector} from '../src/custom-ability'; | ||
@@ -40,3 +40,3 @@ | ||
const testable = customAbility(MyAbility, 'emit'); | ||
const testable = createAbilityInjector(MyAbility, 'emit'); | ||
@@ -74,3 +74,3 @@ beforeEach(function() { | ||
}; | ||
testable1 = customAbility(getAbilityClass, true); | ||
testable1 = createAbilityInjector(getAbilityClass, true); | ||
result = testable1(My); | ||
@@ -82,3 +82,3 @@ result.should.be.equal(My); | ||
let My, testable1; | ||
testable1 = customAbility(MyAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
My = testable1(); | ||
@@ -89,3 +89,3 @@ return My.should.be.equal(MyAbility); | ||
var My, testable1; | ||
testable1 = customAbility(MyAbility, '@cone'); | ||
testable1 = createAbilityInjector(MyAbility, '@cone'); | ||
My = (function() { | ||
@@ -104,3 +104,3 @@ function My() {} | ||
var My, Root, k, result, results, testable1; | ||
testable1 = customAbility(MyAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
Root = (function() { | ||
@@ -124,11 +124,12 @@ function Root() {} | ||
result = testable1(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); // already injected on Root | ||
for (k in MyAbility) { | ||
should.exist(result[k], k); | ||
} | ||
results = []; | ||
// results = []; | ||
for (k in MyAbility.prototype) { | ||
results.push(result.prototype.should.not.have.ownProperty(k)); | ||
My.prototype.should.have.property(k); | ||
My.prototype.should.not.have.ownProperty(k); | ||
} | ||
return results; | ||
// return results; | ||
}); | ||
@@ -147,4 +148,4 @@ it('should add multi abilities on same class', function() { | ||
})(); | ||
testable1 = customAbility(MyAbility); | ||
testable2 = customAbility(OtherAbility); | ||
testable1 = createAbilityInjector(MyAbility); | ||
testable2 = createAbilityInjector(OtherAbility); | ||
Root = (function() { | ||
@@ -176,3 +177,3 @@ function Root() {} | ||
result = testable1(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); | ||
for (k in MyAbility) { | ||
@@ -182,3 +183,4 @@ should.exist(result[k], k); | ||
for (k in MyAbility.prototype) { | ||
result.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.have.property(k); | ||
} | ||
@@ -197,11 +199,10 @@ result = testable2(Root); | ||
result = testable2(My); | ||
result.should.be.equal(My); | ||
result.should.be.equal(Root); | ||
for (k in OtherAbility) { | ||
should.exist(result[k], k); | ||
} | ||
results = []; | ||
for (k in OtherAbility.prototype) { | ||
results.push(result.prototype.should.not.have.ownProperty(k)); | ||
My.prototype.should.not.have.ownProperty(k); | ||
My.prototype.should.have.property(k); | ||
} | ||
return results; | ||
}); | ||
@@ -230,3 +231,3 @@ it('should add methods and class methods to a class', function() { | ||
}; | ||
testable1 = customAbility(fn, 'emit', true); | ||
testable1 = createAbilityInjector(fn, 'emit', true); | ||
My = function() {}; | ||
@@ -268,3 +269,3 @@ testable1(My).should.be.equal(My); | ||
}; | ||
testable1 = customAbility(fn, 'emit', true); | ||
testable1 = createAbilityInjector(fn, 'emit', true); | ||
My = function() {}; | ||
@@ -409,105 +410,107 @@ testable1(My).should.be.equal(My); | ||
}); | ||
it('should use additional abilities', function() { | ||
let My, opt, testableOpts; | ||
testableOpts = function() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
describe('AdditionalAbility', () => { | ||
it('should use additional abilities', function() { | ||
let My, opt, testableOpts; | ||
testableOpts = function() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
}; | ||
My = (function() { | ||
function My() {} | ||
My = (function() { | ||
function My() {} | ||
My.prototype.$abilities = { | ||
MyAbility: testableOpts | ||
}; | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: testableOpts} | ||
}; | ||
return My; | ||
return My; | ||
})(); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
myAbilityCheck(My); | ||
}); | ||
it('should use inherited additional abilities', function() { | ||
var My, Root, my, myOpts, opt, overMy, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
overMy = sinon.spy(function() { | ||
return My.__super__.over.apply(this, arguments); | ||
})(); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
myAbilityCheck(My); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
}); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
it('should not duplicate inject additional abilities on base class', function() { | ||
const overRoot = sinon.spy(function(){ | ||
expect(this).not.ownProperty('self') | ||
}); | ||
const overMy = sinon.spy(function() { | ||
return (My as any).__super__.over.apply(this, arguments); | ||
}); | ||
function rootOpts() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
}; | ||
myOpts = function() { | ||
return { | ||
methods: { | ||
addtional: function() {}, | ||
over: overMy | ||
} | ||
function myOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
over: overMy | ||
} | ||
}; | ||
}; | ||
}; | ||
Root = (function() { | ||
function Root() {} | ||
// function Root() {} | ||
class Root {} | ||
class My extends Root {} | ||
Root.prototype.$abilities = { | ||
MyAbility: rootOpts | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts} | ||
}; | ||
return Root; | ||
// function My() {} | ||
})(); | ||
My = (function() { | ||
function My() {} | ||
// inherits(My, Root); | ||
inherits(My, Root); | ||
My.prototype.$abilities = { | ||
MyAbility: myOpts | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: myOpts} | ||
}; | ||
return My; | ||
const opt = {}; | ||
testable(My, opt); | ||
})(); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('addtional'); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
my = new My; | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
it('should use additional ability via multi classes', function() { | ||
var My, My1, Root, opt, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
// test inject duplication | ||
// function My1() {} | ||
// inherits(My1, Root); | ||
class My1 extends Root {} | ||
testable(My1, opt); | ||
const my:any = new My1; | ||
my.over(3, 1, 2); | ||
overRoot.should.have.been.calledWith(3, 1, 2); | ||
}); | ||
it('should use inherited additional abilities', function() { | ||
const overRoot = sinon.spy(); | ||
const overMy = sinon.spy(function() { | ||
return (My as any).__super__.over.apply(this, arguments); | ||
}); | ||
function rootOpts() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
}; | ||
Root = (function() { | ||
function myOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
over: overMy | ||
} | ||
}; | ||
}; | ||
function Root() {} | ||
Root.prototype.$abilities = { | ||
MyAbility: rootOpts | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts} | ||
}; | ||
return Root; | ||
})(); | ||
My = (function() { | ||
function My() {} | ||
@@ -517,93 +520,287 @@ | ||
return My; | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: myOpts} | ||
}; | ||
})(); | ||
My1 = (function() { | ||
function My1() {} | ||
const opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.not.have.ownProperty('root'); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
Root.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
let my = new My; | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
it('should use additional ability via multi classes', function() { | ||
var My, My1, Root, opt, overRoot, rootOpts; | ||
overRoot = sinon.spy(); | ||
rootOpts = function() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
Root = (function() { | ||
function Root() {} | ||
inherits(My1, Root); | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts} | ||
}; | ||
return My1; | ||
return Root; | ||
})(); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.have.property('over'); | ||
myAbilityCheck(My); | ||
testable(My1); | ||
My1.prototype.should.have.property('root'); | ||
My1.prototype.should.have.property('over'); | ||
myAbilityCheck(My1); | ||
}); | ||
it('should use additional ability via multi inherited classes', function() { | ||
var A, Mid, Root, k, ref, v; | ||
Root = (function() { | ||
function Root() {} | ||
})(); | ||
My = (function() { | ||
function My() {} | ||
Root.prototype.$abilities = { | ||
MyAbility: function() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
two: function() {} | ||
} | ||
}; | ||
} | ||
inherits(My, Root); | ||
return My; | ||
})(); | ||
My1 = (function() { | ||
function My1() {} | ||
inherits(My1, Root); | ||
return My1; | ||
})(); | ||
opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.property('root'); | ||
My.prototype.should.not.have.ownProperty('root'); | ||
My.prototype.should.have.property('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
Root.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
testable(My1); | ||
My1.prototype.should.have.property('root'); | ||
My1.prototype.should.have.property('over'); | ||
myAbilityCheck(My1); | ||
}); | ||
it('should use additional ability via multi inherited classes', function() { | ||
var A, Mid, Root, k, ref, v; | ||
Root = (function() { | ||
function Root() {} | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
two: function() {} | ||
} | ||
}; | ||
}} | ||
}; | ||
return Root; | ||
})(); | ||
Mid = (function() { | ||
function Mid() {} | ||
inherits(Mid, Root); | ||
Mid.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts() { | ||
return { | ||
methods: { | ||
additional: function() { | ||
return (Mid as any).__super__.additional.apply(this, arguments); | ||
}, | ||
iok: function() {} | ||
} | ||
}; | ||
}} | ||
}; | ||
return Mid; | ||
})(); | ||
A = (function() { | ||
function A() {} | ||
inherits(A, Mid); | ||
testable(A); | ||
return A; | ||
})(); | ||
myAbilityCheck(A); | ||
for (k in MyAbility) { | ||
v = MyAbility[k]; | ||
Mid.should.not.have.ownProperty(k); | ||
Root.should.have.ownProperty(k); | ||
A.should.not.have.ownProperty(k); | ||
v.should.be.equal(A[k]); | ||
} | ||
ref = MyAbility.prototype; | ||
for (k in ref) { | ||
v = ref[k]; | ||
A.prototype.should.not.have.ownProperty(k); | ||
Mid.prototype.should.not.have.ownProperty(k); | ||
Root.prototype.should.have.ownProperty(k); | ||
} | ||
Root.prototype.should.have.ownProperty('additional'); | ||
Root.prototype.should.have.ownProperty('two'); | ||
Mid.prototype.should.have.ownProperty('additional'); | ||
Mid.prototype.should.have.ownProperty('iok'); | ||
}); | ||
it('should not inject additional ability if no all required methods exists', function() { | ||
let My, opt, testableOpts; | ||
testableOpts = function() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
My = (function() { | ||
function My() {} | ||
return Root; | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: testableOpts, required: ['one', 'two']} | ||
}; | ||
})(); | ||
Mid = (function() { | ||
function Mid() {} | ||
return My; | ||
inherits(Mid, Root); | ||
})(); | ||
opt = {exclude: 'one'}; | ||
testable(My, opt); | ||
My.prototype.should.not.have.ownProperty('additional'); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
}); | ||
it('should inject additional ability via depends in injectorOptions', function() { | ||
function testableOpts() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
Mid.prototype.$abilities = { | ||
MyAbility: function() { | ||
return { | ||
methods: { | ||
additional: function() { | ||
return (Mid as any).__super__.additional.apply(this, arguments); | ||
}, | ||
iok: function() {} | ||
} | ||
}; | ||
function MyAbility1() {} | ||
MyAbility1.prototype.additional2 = sinon.spy() | ||
const testable1 = createAbilityInjector(MyAbility1, {depends: { | ||
MyAbility: { | ||
getOpts: testableOpts | ||
} | ||
}}); | ||
function My() {} | ||
let opt = {}; | ||
testable1(My, opt); | ||
My.prototype.should.have.ownProperty('additional2'); | ||
My.prototype.should.have.ownProperty(abilitiesSym); | ||
expect(My.prototype[abilitiesSym]).to.have.ownProperty('MyAbility') | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility_MyAbility1', true) | ||
My.prototype.should.have.ownProperty('additional'); | ||
}); | ||
it('should inject additional ability via depends in injectorOptions after already injected ability', function() { | ||
function testableOpts() { | ||
return { | ||
methods: { | ||
additional: function() {} | ||
} | ||
}; | ||
}; | ||
return Mid; | ||
function MyAbility1() {} | ||
MyAbility1.prototype.additional2 = sinon.spy() | ||
})(); | ||
A = (function() { | ||
function A() {} | ||
const testable1 = createAbilityInjector(MyAbility1, {depends: { | ||
MyAbility: { | ||
getOpts: testableOpts | ||
} | ||
}}); | ||
inherits(A, Mid); | ||
function My() {} | ||
testable(A); | ||
let opt = {}; | ||
testable(My, opt); | ||
myAbilityCheck(My) | ||
My.prototype.should.have.ownProperty(abilitiesSym); | ||
My.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
return A; | ||
testable1(My, opt); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility_MyAbility1', true) | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.ownProperty('additional2'); | ||
}); | ||
it('should inject additional abilities on target only after mode is target', function() { | ||
// all additional ability on inheritance classes are injected to target class together. | ||
// and can be super call. | ||
const overRoot = sinon.spy(); | ||
const overMy = sinon.spy(function() { | ||
return this.super(...arguments) | ||
}); | ||
function rootOpts() { | ||
return { | ||
methods: { | ||
root: function() {}, | ||
over: overRoot | ||
} | ||
}; | ||
}; | ||
function myOpts() { | ||
return { | ||
methods: { | ||
additional: function() {}, | ||
over: overMy | ||
} | ||
}; | ||
}; | ||
function Root() {} | ||
})(); | ||
myAbilityCheck(A); | ||
for (k in MyAbility) { | ||
v = MyAbility[k]; | ||
Mid.should.not.have.ownProperty(k); | ||
Root.should.not.have.ownProperty(k); | ||
A.should.have.ownProperty(k); | ||
v.should.be.equal(A[k]); | ||
} | ||
ref = MyAbility.prototype; | ||
for (k in ref) { | ||
v = ref[k]; | ||
A.prototype.should.not.have.ownProperty(k); | ||
Mid.prototype.should.not.have.ownProperty(k); | ||
Root.prototype.should.have.ownProperty(k); | ||
} | ||
Root.prototype.should.have.ownProperty('additional'); | ||
Root.prototype.should.have.ownProperty('two'); | ||
Mid.prototype.should.have.ownProperty('additional'); | ||
return Mid.prototype.should.have.ownProperty('iok'); | ||
Root.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: rootOpts, mode: AdditionalInjectionMode.target} | ||
}; | ||
function My() {} | ||
inherits(My, Root); | ||
My.prototype[abilitiesSym] = { | ||
MyAbility: {getOpts: myOpts, mode: AdditionalInjectionMode.target} | ||
}; | ||
const opt = {}; | ||
testable(My, opt); | ||
My.prototype.should.have.ownProperty('additional'); | ||
My.prototype.should.have.ownProperty('root'); | ||
My.prototype.should.have.ownProperty('over'); | ||
myAbilityCheck(My); | ||
My.prototype.should.have.ownProperty(abilitiesOptSym); | ||
expect(My.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
Root.prototype.should.not.have.ownProperty(abilitiesOptSym); | ||
// expect(Root.prototype[abilitiesOptSym]).to.have.ownProperty('MyAbility', true) | ||
let my = new My; | ||
my.over(1, 2, 3); | ||
overMy.should.have.been.calledOnce; | ||
overMy.should.have.been.calledWith(1, 2, 3); | ||
overRoot.should.have.been.calledOnce; | ||
overRoot.should.have.been.calledWith(1, 2, 3); | ||
}); | ||
}); | ||
describe('use the injectMethods(AOP) to hook', function() { | ||
@@ -635,3 +832,3 @@ var OneAbility, oneTestable; | ||
})(); | ||
oneTestable = customAbility(OneAbility, 'emit'); | ||
oneTestable = createAbilityInjector(OneAbility, 'emit'); | ||
beforeEach(function() { | ||
@@ -684,5 +881,5 @@ OneAbility.prototype.$init.resetHistory(); | ||
t = OneAbility.prototype.$init.thisValues[0]; | ||
return t.should.be.equal(a); | ||
t.should.be.equal(a); | ||
}); | ||
return it('should ignore some injectMethod', function() { | ||
it('should ignore some injectMethod', function() { | ||
var A, a, oldInit; | ||
@@ -708,3 +905,3 @@ oldInit = sinon.spy(); | ||
oldInit.should.be.calledOnce; | ||
return oldInit.should.be.calledWith(123); | ||
oldInit.should.be.calledWith(123); | ||
}); | ||
@@ -711,0 +908,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
170136
3721
683
37
Updatedinherits-ex@^2.1.0-alpha.11