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 (
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();
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 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.
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 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.