wrapme v1.1.0
wrapme
Functions to wrap other functions and fields/methods and to change/enhance their behavior, functionality or usage.
Can be used for Aspect-oriented programming.
Features
- Wrap a single function/field/method (by
wrap) or several fields and methods at once (byintercept). - Wrap only field's get operation (
getoption) or set operation (setoption), or both (by default). - Provide special getter and/or setter for wrapped field if it is necessary.
- Call original function/method or field's operation before (use
beforeorlistenoption), after (useafteroption) and/or insidehandler(userun()orrunApply()). - Totally control calling of original function/method or field's operation inside
handler: call depending on condition, filter/validate/convert passed arguments and/or provide another arguments. - Return result of original function/method or field's operation, or any other value from
handler. - Save necessary data between
handlercalls. - Restore original fields/methods when it is needed.
- Does not have dependencies and can be used in ECMAScript 5+ environment.
- Small size.
import { intercept } from 'wrapme';
const api = {
sum(...numList) {
let result = 0;
for (let value of numList) {
result += value;
}
return result;
},
// Other methods
// ...
};
// Logging
const log = [];
function logger(callData) {
log.push({
name: callData.field,
args: callData.arg,
result: callData.result,
callNum: callData.number,
time: new Date().getTime()
});
}
const unwrap = intercept(api, 'sum', logger, {listen: true});
api.sum(1, 2, 3, 4); // Returns 10, adds item to log
api.sum(1, -1, 2, -2, 3); // Returns 3, adds item to log
// Restore original method
unwrap();Table of contents
Installation ↑
Node
npm install wrapmeAMD, <script>
Use dist/wrapme.umd.development.js or dist/wrapme.umd.production.min.js (minified version).
Usage ↑
ECMAScript 6+
import { intercept, wrap } from 'wrapme';Node
const wrapme = require('wrapme');
const { intercept, wrap } = wrapme;AMD
define(['path/to/dist/wrapme.umd.production.min.js'], function(wrapme) {
const intercept = wrapme.intercept;
const wrap = wrapme.wrap;
});<script>
<script type="text/javascript" src="path/to/dist/wrapme.umd.production.min.js"></script>
<script type="text/javascript">
// wrapme is available via wrapme field of window object
const intercept = wrapme.intercept;
const wrap = wrapme.wrap;
</script>Examples ↑
import { intercept, wrap } from 'wrapme';
const api = {
value: 1,
sum(...numList) {
let result = 0;
for (let value of numList) {
result += value;
}
return result;
},
positive(...numList) {
let result = [];
for (let value of numList) {
if (value > 0) {
result.push(value);
}
}
return result;
},
factorial(num) {
let result = 1;
while (num > 1) {
result *= num--;
}
return result;
},
binomCoeff(n, k) {
const { factorial } = api;
return factorial(n) / (factorial(k) * factorial(n - k));
}
};
// Logging
const log = [];
function logger(callData) {
if (! callData.byUnwrap) {
callData.settings.log.push({
name: callData.field,
args: callData.arg,
result: callData.result,
callNum: callData.number,
time: new Date().getTime()
});
}
}
const unwrap = intercept(api, ['sum', 'positive', 'value'], logger, {listen: true, log});
api.sum(1, 2, 3, 4); // Returns 10, adds item to log
api.positive(1, 2, -3, 0, 10, -7); // Returns [1, 2, 10], adds item to log
api.value += api.sum(1, -1, 2, -2, 3); // Changes value to 4, adds items to log
// Restore original fields
unwrap();
api.positive(-1, 5, 0, api.value, -8); // Returns [5, 4], doesn't add items to log
console.log("call log:\n", JSON.stringify(log, null, 4));
/* log looks like:
[
{
"name": "sum",
"args": [
1,
2,
3,
4
],
"result": 10,
"callNum": 1,
"time": 1586602348174
},
{
"name": "positive",
"args": [
1,
2,
-3,
0,
10,
-7
],
"result": [
1,
2,
10
],
"callNum": 1,
"time": 1586602348174
},
{
"name": "value",
"args": [],
"result": 1,
"callNum": 1,
"time": 1586602348174
},
{
"name": "sum",
"args": [
1,
-1,
2,
-2,
3
],
"result": 3,
"callNum": 2,
"time": 1586602348174
},
{
"name": "value",
"args": [
4
],
"result": 4,
"callNum": 2,
"time": 1586602348175
}
]
*/
// Simple memoization
function memoize(callData) {
const { save } = callData;
const key = callData.arg.join(' ');
return (key in save)
? save[key]
: (save[key] = callData.run());
}
intercept(api, ['factorial', 'binomCoeff'], memoize);
api.factorial(10);
api.factorial(5);
api.binomCoeff(10, 5); // Uses already calculated factorials
api.binomCoeff(10, 5); // Uses already calculated value
// Side effects
function saveToLocalStorage(callData) {
if (callData.bySet) {
const { save } = callData;
if ('id' in save) {
clearTimeout(save.id);
}
save.id = setTimeout(
() => localStorage.setItem(
`wrap:${callData.field}`,
typeof callData.result === 'undefined'
? callData.arg0
: callData.result
),
callData.settings.timeout || 0
);
}
}
wrap(api, 'value', saveToLocalStorage, {listen: true, timeout: 50});
// Validation, filtering or conversion
function filter(callData) {
const { arg, bySet } = callData;
const argList = [];
for (let item of arg) {
const itemType = typeof item;
if ( (itemType === 'number' && ! isNaN(item))
|| (bySet && itemType === 'string' && item && (item = Number(item))) ) {
argList.push(item);
}
}
if (argList.length || ! bySet) {
return callData.runApply(argList);
}
}
wrap(api, 'value', filter);
api.value = 'some data'; // value isn't changed, saveToLocalStorage isn't called
api.value = 9; // value is changed, saveToLocalStorage is called
api.value = '-53'; // string is converted to number and value is changed, saveToLocalStorage is called
const sum = wrap(api.sum, filter);
const positive = wrap(api.positive, filter);
sum(false, 3, NaN, new Date(), 8, {}, 'sum', '2'); // Returns 11
positive(true, -5, NaN, 4, new Date(), 1, {a: 5}, 0, 'positive', -1); // Returns [4, 1]See additional examples in tests.
API ↑
wrap(target, field, handler?, settings?): Function
Wraps specified object's field/method or standalone function into new (wrapping) function that calls passed handler which eventually may run wrapped function or get/set field's value.
Arguments:
target: Function | object- Function that should be wrapped or an object whose field/method will be wrapped and replaced.field: Function | string- Name of field/method that should be wrapped or a handler when function is passed fortargetparameter.handler: Function | object- A function (interceptor) that should be executed when newly created function is called or get/set operation for the field is applied, or optional settings when function is passed fortargetparameter.settings: object- Optional settings that will be available inhandler.settings.after: boolean(optional) - Whether original function, method or field's operation should be called afterhandler.settings.before: boolean(optional) - Whether original function, method or field's operation should be called beforehandler.settings.bind: boolean(optional) - Whether wrapping function should be bound totargetobject.settings.context: object(optional) - Context (this) that should be used forhandlercall.settings.data: any(optional) - Any data that should be available inhandler.settings.get: boolean | Function(optional) - Whether field's get operation should be intercepted and whether created wrapping function should be used as field's getter (by defaulttruefor usual (non-functional) field andfalsefor method).settings.listen: boolean(optional) - Whether original function, method or field's operation should be called beforehandlerand whether original's result should be returned.settings.set: boolean | Function(optional) - Whether field's set operation should be intercepted and whether created wrapping function should be used as field's setter (by defaulttruefor usual (non-functional) field andfalsefor method).
Returns wrapping function when target is a function,
or a function that restores original field/method when target is an object.
An object with the following fields will be passed into handler:
arg: any[]- Array of arguments that were passed to the wrapping function.arg0: any- Value ofarg[0].byCall: boolean- Whether wrapping function is called as object's method or as usual function (by a call operation).byGet: boolean- Whether wrapping function is called to get field's value (by get operation, as field's getter).bySet: boolean- Whether wrapping function is called to set field's value (by set operation, as field's setter).byUnwrap: boolean- Whether wrapping function (andhandler) is called during unwrapping.context: object- Context (this) with which wrapping function is called.data: any- Value ofsettings.dataoption.field: string | undefined- Name of the field or method that was wrapped.fieldWrap: boolean- Whether field's get and/or set operation was wrapped.funcWrap: boolean- Whether standalone function (not object's field/method) was wrapped.get: (() => any) | undefined- Function that returns field's current value if field was wrapped.method: string- Name of the method or function that was wrapped.methodWrap: boolean- Whether method was wrapped.number: number- Number ofhandler's call (starting from 1).result: any- Result of original function/method when it is called beforehandler.run: (...args?) => any- Method that calls original function/method or field's getter/setter; by default values fromargwill be used as arguments; but you may pass arguments torunand they will be used instead of the original arguments.runApply: (any[]?) => any- Similar torunbut accepts an array of new arguments, e.g.runApply([1, 2, 3])is equivalent torun(1, 2, 3); if the first argument ofrunApplyis not an array it will be wrapped into array (i.e.[arguments[0]]); only the first argument ofrunApplyis used.save: object- An object that can be used to preserve some values betweenhandlercalls.set: ((value: any) => any) | undefined- Function that changes field's current value if field was wrapped.settings: object- Value ofsettingsparameter; except forsettings.bindandsettings.context, it is possible to change any setting to alter following execution; so be careful when you change a field's value ofsettingsobject.target: ((...args) => any) | string- Original function or method that was wrapped, or name of wrapped field.targetObj: object | null- An object whose field/method was wrapped and replaced.value: any- Previous value returned by wrapping function.
When settings.after and settings.listen are false, result of handler will be returned from wrapping function.
intercept(target, field, handler?, settings?): Function
Wraps specified object's field(s)/method(s) or standalone function into new (wrapping) function that calls passed handler which eventually may run wrapped function or get/set field's value.
Arguments:
target: Function | object- Function that should be wrapped or an object whose field(s)/method(s) will be wrapped and replaced.field: Function | string | string[]- Name of field/method (or list of field/method names) that should be wrapped or a handler when function is passed fortargetparameter.handler: Function | object- A function (interceptor) that should be executed when newly created function is called or get/set operation for the field is applied, or settings when function is passed fortargetparameter.settings: object- Optional settings that will be available inhandler. Seewrapfor details.
Returns wrapping function when target is a function,
or a function that restores original field(s)/method(s) when target is an object.
See doc folder for details.
Related projects ↑
Inspiration ↑
This library is inspired by meld.
Contributing ↑
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code.
License ↑
Copyright (c) 2020 Denis Sikuler
Licensed under the MIT license.
