Functions to wrap other functions and fields/methods and to change/enhance their behavior, functionality or usage.
Can be used for Aspect-oriented programming.
- Wrap a single function/field/method (by
wrap
) or several fields and methods at once (byintercept
). - Wrap only field's get operation (
get
option) or set operation (set
option), 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
before
orlisten
option), after (useafter
option) 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
handler
calls. - 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();
Installation ↑
npm install wrapme
Use dist/wrapme.umd.development.js
or dist/wrapme.umd.production.min.js
(minified version).
Usage ↑
import { intercept, wrap } from 'wrapme';
const wrapme = require('wrapme');
const { intercept, wrap } = wrapme;
define(['path/to/dist/wrapme.umd.production.min.js'], function(wrapme) {
const intercept = wrapme.intercept;
const wrap = wrapme.wrap;
});
<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 ↑
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 fortarget
parameter.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 fortarget
parameter.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 totarget
object.settings.context: object
(optional) - Context (this
) that should be used forhandler
call.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 defaulttrue
for usual (non-functional) field andfalse
for method).settings.listen: boolean
(optional) - Whether original function, method or field's operation should be called beforehandler
and 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 defaulttrue
for usual (non-functional) field andfalse
for 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.data
option.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 fromarg
will be used as arguments; but you may pass arguments torun
and they will be used instead of the original arguments.runApply: (any[]?) => any
- Similar torun
but accepts an array of new arguments, e.g.runApply([1, 2, 3])
is equivalent torun(1, 2, 3)
; if the first argument ofrunApply
is not an array it will be wrapped into array (i.e.[arguments[0]]
); only the first argument ofrunApply
is used.save: object
- An object that can be used to preserve some values betweenhandler
calls.set: ((value: any) => any) | undefined
- Function that changes field's current value if field was wrapped.settings: object
- Value ofsettings
parameter; except forsettings.bind
andsettings.context
, it is possible to change any setting to alter following execution; so be careful when you change a field's value ofsettings
object.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.
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 fortarget
parameter.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 fortarget
parameter.settings: object
- Optional settings that will be available inhandler
. Seewrap
for 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 docs
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.