Skip to content

Commit

Permalink
feat: Refactoring to pass an option object to initialize hooks more e…
Browse files Browse the repository at this point in the history
…xplicitly (#7)
  • Loading branch information
daffl committed Jan 13, 2020
1 parent f2b5697 commit 8f2453f
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 351 deletions.
86 changes: 43 additions & 43 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion packages/hooks/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function registerMiddleware<T> (target: T, middleware: Middleware[]) {
return target;
}

export function getMiddleware (target: any): Middleware[] {
export function getMiddleware<T> (target: any): Array<Middleware<T>> {
return (target && target[HOOKS]) || [];
}

Expand All @@ -39,6 +39,39 @@ export class HookContext<T = any, C = any> {
* arguments of the function call.
*/
export type ContextUpdater<T = any> = (self: any, args: any[], context: HookContext<T>) => HookContext<T>;
/**
* A function that for a given function, calling context and arguments returns the list of hooks
*/
export type MiddlewareCollector<T = any> = (self: any, fn: any, args: any[]) => Array<Middleware<T>>;

/**
* Available options when initializing hooks with more than just an array of middleware
*/
export interface FunctionHookOptions<T = any> {
middleware: Array<Middleware<T>>;
context: ContextUpdater<T>;
collect: MiddlewareCollector<T>;
}

export type HookSettings<T = any> = Array<Middleware<T>>|Partial<FunctionHookOptions>;

export function defaultCollectMiddleware<T = any> (self: any, fn: any, _args: any[]) {
return [
...getMiddleware<T>(self),
...getMiddleware(fn)
];
}

export function normalizeOptions<T = any> (opts: HookSettings): FunctionHookOptions<T> {
const options: Partial<FunctionHookOptions> = Array.isArray(opts) ? { middleware: opts } : opts;
const {
middleware = [],
context = withParams(),
collect = defaultCollectMiddleware
} = options;

return { middleware, context, collect };
}

/**
* Returns a ContextUpdater function that turns function arguments like
Expand Down
24 changes: 12 additions & 12 deletions packages/hooks/src/decorator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { Middleware } from './compose';
import { functionHooks } from './function';
import { ContextUpdater, withParams, HookContext, registerMiddleware } from './base';
import { HookContext, registerMiddleware, normalizeOptions, HookSettings } from './base';

export const hookDecorator = <T> (hooks: Array<Middleware<T>>, _updateContext?: ContextUpdater<T>) => {
export const hookDecorator = <T> (hooks: HookSettings<T> = []) => {
return (_target: any, method: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> => {
if (!descriptor) {
if (_updateContext) {
throw new Error('Context can not be updated at the class decorator level. Remove updateContext parameter.');
}
const options = normalizeOptions(hooks);

registerMiddleware(_target.prototype, hooks);
if (!descriptor) {
registerMiddleware(_target.prototype, options.middleware);

return _target;
}
Expand All @@ -20,16 +17,19 @@ export const hookDecorator = <T> (hooks: Array<Middleware<T>>, _updateContext?:
throw new Error(`Can not apply hooks. '${method}' is not a function`);
}

const originalUpdateContext = _updateContext || withParams();
const updateContext = (self: any, args: any[], context: HookContext<any>) => {
const ctx = originalUpdateContext(self, args, context);
const originalContext = options.context;
const context = (self: any, args: any[], context: HookContext<any>) => {
const ctx = originalContext(self, args, context);

ctx.method = method;

return ctx;
};

descriptor.value = functionHooks(fn, hooks, updateContext);
descriptor.value = functionHooks(fn, {
...options,
context
});

return descriptor;
};
Expand Down
35 changes: 14 additions & 21 deletions packages/hooks/src/function.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { compose, Middleware } from './compose';
import {
HookContext, ContextUpdater, withParams,
registerMiddleware, getMiddleware
} from './base';
import { HookContext, registerMiddleware, normalizeOptions, HookSettings } from './base';

/**
* Returns a new function that is wrapped in the given hooks.
Expand All @@ -11,20 +8,16 @@ import {
* just set `context.arguments` to the function call arguments
* and `context.self` to the function call `this` reference.
*
* @param fn The function to wrap
* @param hooks The list of hooks (middleware)
* @param updateContext A ContextUpdate method
* @param original The function to wrap
* @param options A list of hooks (middleware) or options for more detailed hook processing
*/
export const functionHooks = <T = any>(
fn: any,
hooks: Array<Middleware<T>>,
updateContext: ContextUpdater = withParams()
) => {
if (typeof fn !== 'function') {
export const functionHooks = <F, T = any>(original: F, opts: HookSettings<T>) => {
if (typeof original !== 'function') {
throw new Error('Can not apply hooks to non-function');
}

const result = registerMiddleware(function (this: any, ...args: any[]) {
const { context: updateContext, collect, middleware } = normalizeOptions(opts);
const wrapper = function (this: any, ...args: any[]) {
// If we got passed an existing HookContext instance, we want to return it as well
const returnContext = args[args.length - 1] instanceof HookContext;
// Initialize the context. Either the default context or the one that was passed
Expand All @@ -35,14 +28,12 @@ export const functionHooks = <T = any>(
const hookChain: Middleware[] = [
// Return `ctx.result` or the context
(ctx, next) => next().then(() => returnContext ? ctx : ctx.result),
// The hooks attached to the `this` object
...getMiddleware(this),
// The hook chain attached to this function
...getMiddleware(result),
// Create the hook chain by calling the `collectMiddleware function
...collect(this, wrapper, args),
// Runs the actual original method if `ctx.result` is not already set
(ctx, next) => {
if (ctx.result === undefined) {
return Promise.resolve(fn.apply(this, ctx.arguments)).then(result => {
return Promise.resolve(original.apply(this, ctx.arguments)).then(result => {
ctx.result = result;

return next();
Expand All @@ -54,7 +45,9 @@ export const functionHooks = <T = any>(
];

return compose(hookChain).call(this, context);
}, hooks);
};

return Object.assign(result, { original: fn });
registerMiddleware(wrapper, middleware);

return Object.assign(wrapper, { original });
};
Loading

0 comments on commit 8f2453f

Please sign in to comment.