Skip to content

Commit

Permalink
feat(hooks): Refactor .params, .props and .defaults into hooks (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored Jun 1, 2020
1 parent 732b5c9 commit 9b13b7d
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 502 deletions.
92 changes: 7 additions & 85 deletions packages/hooks/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Middleware } from './compose';
import { copyToSelf } from './utils';

export const HOOKS: string = Symbol('@feathersjs/hooks') as any;

export type HookContextData = { [key: string]: any };

/**
* The base hook context.
*/
export class HookContext<T = any, C = any> {
result?: T;
method?: string;
Expand All @@ -22,14 +18,11 @@ export class HookContext<T = any, C = any> {

export type HookContextConstructor = new (data?: { [key: string]: any }) => HookContext;

export type HookDefaultsInitializer = (self?: any, args?: any[], context?: HookContext) => HookContextData;
export type HookDefaultsInitializer = (context: HookContext) => HookContextData;

export class HookManager {
_parent?: this|null = null;
_params: string[] = [];
_middleware: Middleware[] = [];
_props: HookContextData = {};
_defaults: HookDefaultsInitializer;

parent (parent: this) {
this._parent = parent;
Expand All @@ -43,103 +36,32 @@ export class HookManager {
return this;
}

getContextClass (): HookContextConstructor {
return HookContext;
}

getMiddleware (): Middleware[] {
const previous = this._parent ? this._parent.getMiddleware() : [];

return previous.concat(this._middleware);
}

collectMiddleware (self: any, _args: any[]): Middleware[] {
collectMiddleware (self: any, _args: any[]): Middleware[] {
const otherMiddleware = getMiddleware(self);

return otherMiddleware.concat(this.getMiddleware().reverse());
}

props (props: HookContextData) {
Object.assign(this._props, props);

return this;
}

getProps (): HookContextData {
const previous = this._parent ? this._parent.getProps() : {};

return Object.assign({}, previous, this._props);
}

params (...params: string[]) {
this._params = params;

return this;
}

getParams (): string[] {
const previous = this._parent ? this._parent.getParams() : [];

return previous.concat(this._params);
}

defaults (defaults: HookDefaultsInitializer) {
this._defaults = defaults;

return this;
return otherMiddleware.concat(this.getMiddleware());
}

getDefaults (self: any, args: any[], context: HookContext): HookContextData {
const previous = this._parent ? this._parent.getDefaults(self, args, context) : {};
const defaults = typeof this._defaults === 'function' ? this._defaults(self, args, context) : {};

return Object.assign({}, previous, defaults);
}

getContextClass (Base: HookContextConstructor = HookContext): HookContextConstructor {
const ContextClass = class ContextClass extends Base {
constructor (data: any) {
super(data);

copyToSelf(this);
}
};
const params = this.getParams();
const props = this.getProps();

params.forEach((name, index) => {
if (props[name]) {
throw new Error(`Hooks can not have a property and param named '${name}'. Use .defaults instead.`);
}

Object.defineProperty(ContextClass.prototype, name, {
enumerable: true,
get () {
return this?.arguments[index];
},
set (value: any) {
this.arguments[index] = value;
}
});
});

Object.assign(ContextClass.prototype, props);

return ContextClass;
}

initializeContext (self: any, args: any[], context: HookContext): HookContext {
const ctx = this._parent ? this._parent.initializeContext(self, args, context) : context;
const defaults = this.getDefaults(self, args, ctx);

if (self) {
ctx.self = self;
}

ctx.arguments = args;

for (const name of Object.keys(defaults)) {
if (ctx[name] === undefined) {
ctx[name] = defaults[name];
}
}

return ctx;
}
}
Expand Down
63 changes: 63 additions & 0 deletions packages/hooks/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { HookContext, HookContextData, HookDefaultsInitializer } from './base'
import { NextFunction } from './compose'

/**
* Returns a hook that initializes named function parameters on the
* hook context.
*
* @param names A list of parameter names
*/
export function params (...names: string[]) {
const descriptors = names.reduce((result, name, index) => {
result[name] = {
enumerable: true,
get (this: any) {
return this.arguments[index];
},

set (this: any, value) {
this.arguments[index] = value;
}
}

return result;
}, {} as PropertyDescriptorMap);

return async function contextParams (context: HookContext, next: NextFunction) {
Object.defineProperties(context, descriptors);
await next();
};
}

/**
* Returns a hook that sets the given properties on the hook context.
*
* @param properties The properties to set.
*/
export function properties (properties: HookContextData) {
return async function contextProperties (context: HookContext, next: NextFunction) {
Object.assign(context, properties);
await next();
}
}

/**
* Returns a hook that calls a `callback(context)` function that
* returns default values which will be set on the context if they
* are currently `undefined`.
*
* @param initializer The initialization callback.
*/
export function defaults (initializer: HookDefaultsInitializer) {
return async function contextDefaults (context: HookContext, next: NextFunction) {
const defaults = await initializer(context);

for (const name of Object.keys(defaults)) {
if (context[name] === undefined) {
context[name] = defaults[name];
}
}

await next();
}
}
4 changes: 3 additions & 1 deletion packages/hooks/src/decorator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { functionHooks } from './function';
import { setManager, HookOptions, convertOptions } from './base';
import { properties } from './context';

export const hookDecorator = (managerOrMiddleware?: HookOptions) => {
const wrapper: any = (_target: any, method: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> => {
Expand All @@ -17,7 +18,8 @@ export const hookDecorator = (managerOrMiddleware?: HookOptions) => {
throw new Error(`Can not apply hooks. '${method}' is not a function`);
}

descriptor.value = functionHooks(fn, manager.props({ method}));
manager._middleware.unshift(properties({ method }));
descriptor.value = functionHooks(fn, manager);

return descriptor;
};
Expand Down
20 changes: 19 additions & 1 deletion packages/hooks/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@ import { compose, Middleware } from './compose';
import {
HookContext, setManager, HookContextData, HookOptions, convertOptions
} from './base';
import { getOriginal, copyProperties } from './utils';

export function getOriginal (fn: any): any {
return typeof fn.original === 'function' ? getOriginal(fn.original) : fn;
}

function copyProperties <F> (target: F, original: any) {
const originalProps = (Object.keys(original) as any)
.concat(Object.getOwnPropertySymbols(original));

for (const prop of originalProps) {
const propDescriptor = Object.getOwnPropertyDescriptor(original, prop);

if (!target.hasOwnProperty(prop)) {
Object.defineProperty(target, prop, propDescriptor);
}
}

return target;
}

/**
* Returns a new function that is wrapped in the given hooks.
Expand Down
1 change: 1 addition & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { objectHooks, HookMap } from './object';
import { hookDecorator } from './decorator';
import { HookManager, HookContextData, HookContext, HookContextConstructor, HookOptions } from './base';

export * as setContext from './context';
export * from './function';
export * from './compose';
export * from './base';
Expand Down
5 changes: 4 additions & 1 deletion packages/hooks/src/object.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Middleware } from './compose';
import { functionHooks } from './function';
import { setMiddleware, convertOptions, HookOptions } from './base';
import { properties } from './context';

export type HookMap<O = any> = {
[L in keyof O]?: HookOptions;
Expand All @@ -22,7 +23,9 @@ export function objectHooks (_obj: any, hooks: HookMap|Middleware[]) {

const manager = convertOptions(hooks[method]);

result[method] = functionHooks(fn, manager.props({ method }));
manager._middleware.unshift(properties({ method }));

result[method] = functionHooks(fn, manager);

return result;
}, obj);
Expand Down
44 changes: 0 additions & 44 deletions packages/hooks/src/utils.ts

This file was deleted.

12 changes: 10 additions & 2 deletions packages/hooks/test/benchmark.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { strict as assert } from 'assert';
import { hooks, HookContext, NextFunction, middleware } from '../src/';
import {
hooks,
HookContext,
NextFunction,
middleware,
setContext
} from '../src/';

const CYCLES = 100000;
const getRuntime = async (callback: () => Promise<any>) => {
Expand Down Expand Up @@ -44,10 +50,12 @@ describe('hook benchmark', () => {

it('single hook, withParams and props', async () => {
const hookHello = hooks(hello, middleware([
setContext.params('name'),
setContext.properties({ dave: true }),
async (_ctx: HookContext, next: NextFunction) => {
await next();
}
]).params('name').props({ dave: true }));
]));

const runtime = await getRuntime(() => hookHello('Dave'));

Expand Down
Loading

0 comments on commit 9b13b7d

Please sign in to comment.