UNPKG

okam-core

Version:

The extension for small program framework

349 lines (316 loc) 10.5 kB
/** * @file Native api utilities * @author sparklewhy@gmail.com */ import {isFunction, isObject, isPromise, definePropertyValue} from '../util/index'; /** * Crate API done hook * * @inner * @param {Function} done the done hook to execute * @param {Object} ctx the extra call context for the hook * @return {Object} */ function createAPIDoneHook(done, ctx) { let hookInfo = { resData: null, resException: false, hook: null }; hookInfo.hook = (err, res, complete, shouldCatchException) => { let result; if (err != null) { result = err; if (shouldCatchException) { try { let data = done(result, null, ctx); data == null || (result = data); } catch (ex) { // cache exception info for promise hookInfo.resException = true; result = ex; } } else { let data = done(result, null, ctx); data == null || (result = data); } } else { let data = done(null, res, ctx); data == null || (res = data); result = res; } // cache response data for promise hookInfo.resData = result; complete && complete(result); // call complete callback return result; }; return hookInfo; } /** * Intercept async API done based on the call arguments: `success`/`fail` * * @inner * @param {boolean} sync whether is sync API, if true, it will not hook the * API done result based the call arguments * @param {Array} args the API call arguments * @param {Function} doneHook the done hook to execute * @return {boolean} whether intercept the API done result by the arguments */ function interceptAsyncAPIDone(sync, args, doneHook) { let callOpts = args[0]; let {success, fail, complete} = callOpts || {}; let hasIntercepted = false; if (!sync && (isFunction(success) || isFunction(fail) || isFunction(complete)) ) { hasIntercepted = true; let newCallOpts = Object.assign({}, callOpts); newCallOpts.success = res => { let data = doneHook(null, res, complete, true); success && success(data); }; newCallOpts.fail = err => { err = doneHook(err || /* istanbul ignore next */ 'err', null, complete, true); fail && fail(err); }; // remove complete callback // using success and fail callback instead of using complete callback complete && (newCallOpts.complete = undefined); args[0] = newCallOpts; } return hasIntercepted; } /** * Intercept the API promise response * * @inner * @param {boolean} hasIntercepted whether intercepted by the API call arguments * @param {Promise} promise the promise response * @param {Object} doneHookInfo the done hook info * @return {Promise} */ function interceptPromiseResponse(hasIntercepted, promise, doneHookInfo) { let {hook} = doneHookInfo; return promise.then( res => (hasIntercepted ? doneHookInfo.resData : hook(null, res)), err => { // check whether is intercepted to avoid repeated interception if (hasIntercepted) { let {resException, resData} = doneHookInfo; if (resException) { throw resData; } return resData; } return hook(err || /* istanbul ignore next */ 'err'); } ); } /** * Execute API initialization hook * * @inner * @param {Function} init the initialization hook * @param {Array} args the API call arguments * @param {Object} ctx the extra call context for the hook * @return {*} */ function hookAPIInit(init, args, ctx) { // API init interception if (isFunction(init)) { if (args.length > 1 || !isObject(args[0])) { // convert as one array type argument to make the developer // can modify the call arguments directly args = [args]; } return init.apply(this, [...args, ctx]); } } /** * Execute API done hook * * @inner * @param {Function} done the done hook * @param {boolean} sync whether is sync API, if true, it will not hook the * API done result based the call arguments * @param {Array} args the API call arguments * @param {Object} ctx the extra call context for the hook * @return {Object} return the api done hook info */ function hookAPIDone(done, sync, args, ctx) { // API done interception let hasDoneHook = isFunction(done); let hasIntercepted = false; let doneHookInfo; if (hasDoneHook) { doneHookInfo = createAPIDoneHook(done, ctx); // intercept async API based the args: `success`/`fail`/`complete` callback hasIntercepted = interceptAsyncAPIDone(sync, args, doneHookInfo.hook); } return { hasDoneHook, hasIntercepted, doneHookInfo }; } /** * Execute api * * @inner * @param {Object} hookInfo the api done hook info * @param {Function} rawApi the raw api definition * @param {Array} args the API call arguments * @return {*} */ function executeAPI(hookInfo, rawApi, args) { // call API let {hasDoneHook, hasIntercepted, doneHookInfo} = hookInfo; let result = rawApi.apply(this, args); if (hasDoneHook) { // intercept the API call result if (isPromise(result)) { result = interceptPromiseResponse(hasIntercepted, result, doneHookInfo); } else if (!hasIntercepted) { result = doneHookInfo.hook(null, result); } } return result; } /** * Proxy API * * @inner * @param {Object} ctx the extra context information for the hook * @param {Function} rawApi the original API to call * @param {Object} apiOpts the API hook options * @param {...any} args the arguments to call the API * @return {*} */ function proxyAPI(ctx, rawApi, apiOpts, ...args) { let {init, done, sync} = apiOpts; // API init interception let initResult = hookAPIInit(init, args, ctx); if (isPromise(initResult)) { return initResult.then( () => { let hookInfo = hookAPIDone(done, sync, args, ctx); return executeAPI(hookInfo, rawApi, args); } ); } else { let hookInfo = hookAPIDone(done, sync, args, ctx); return executeAPI(hookInfo, rawApi, args); } } /** * Promisify the given function. * Notice: the function must be async function and the the first param passed to * the function must be like the below: * <code> * func({ * fail() {}, // execute async function fail callback * success() {} // execute async function success callback * }); * </code> * * @param {Function} func the function to been promisify * @param {Object=} context the context to execute the promisified function * @return {Function} */ export function promisify(func, context = null) { return function (...args) { return new Promise((resolve, reject) => { let opts = Object.assign({}, args[0]); args[0] = opts; let oldFail = opts.fail; opts.fail = err => { typeof oldFail === 'function' && oldFail(err); reject(err); }; let oldSuccess = opts.success; opts.success = res => { typeof oldSuccess === 'function' && oldSuccess(res); resolve(res); }; func.apply(context || null, args); }); }; } /** * promisifyApi * * @param {Array|string} apis apis which need promisifyApi * @param {Object=} context the context to execute the promisified function */ export function promisifyApis(apis, context) { if (!Array.isArray(apis)) { apis = [apis]; } let allApis = context.$api; apis.forEach(key => { let api = allApis[key]; if (!api) { return; } definePropertyValue(allApis, key, promisify(api, context.$api)); }); } /** * Intercept native api to change the call arguments or the call result or * log something when calling. * * @param {Object} apis the api to intercept, the structure is like below: * { * request: { // the key `request` is the API name to intercept * * // Intercept the call, in this hook you can change the call arguments * init(options) { * // modify request options * options.dataType = 'json'; * }, * * // Intercept the API return result, in this hook you can * // change the return result. * // The first argument means whether the call has error happen, * // if the `err` is null, the call is successful, `res` the result * // of this call. * // NOTICE: this hook is used for the sync API or async API that * // must return promise, if you wanna to modify call result. * // For async API that returns promise, if you wanna the promise * // reject happen, you need throw `err` in this hook. * done(err, res) { * if (err) { * // log something * throw err; // ensure the reject can trigger like before. * } * res.data = res.data.data; // modify the return data structure. * * // return 333; // of course, you can return the new result, * // if the `res` is not a reference, you cannot change it directly. * } * } * } * @param {string} key the key of all raw api definition * @param {Object} ctx the app instance context */ export function interceptApis(apis, key, ctx) { if (!apis) { return; } let allApis = ctx[key]; Object.keys(apis).forEach(apiName => { let rawApi = allApis[apiName]; if (rawApi) { definePropertyValue( allApis, apiName, proxyAPI.bind(null, ctx, rawApi, apis[apiName]) ); } }); }