Skip to content

Commit

Permalink
feat(hooks): Finalize default initializer functionality (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed May 28, 2020
1 parent 699f2fd commit d380d76
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 178 deletions.
26 changes: 23 additions & 3 deletions packages/hooks/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ 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 class HookManager {
_parent?: this|null = null;
_params: string[] = [];
_middleware: Middleware[] = [];
_props: HookContextData = {};
_defaults: HookContextData|(() => HookContextData) = {};
_defaults: HookDefaultsInitializer;

parent (parent: this) {
this._parent = parent;
Expand Down Expand Up @@ -77,12 +79,19 @@ export class HookManager {
return previous.concat(this._params);
}

defaults (defaults: HookContextData|(() => HookContextData)) {
defaults (defaults: HookDefaultsInitializer) {
this._defaults = defaults;

return this;
}

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) {
Expand All @@ -95,10 +104,14 @@ export class HookManager {
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];
return this?.arguments[index];
},
set (value: any) {
this.arguments[index] = value;
Expand All @@ -113,13 +126,20 @@ export class HookManager {

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
2 changes: 1 addition & 1 deletion packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function middleware (mw: Middleware[] = []) {
/**
* Returns a new function that wraps an existing async function
* with hooks.
*
*
* @param fn The async function to add hooks to.
* @param manager An array of middleware or hook settings
* (`middleware([]).params()` etc.)
Expand Down
128 changes: 128 additions & 0 deletions packages/hooks/test/class.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { strict as assert } from 'assert';
import { hooks, middleware, HookContext, NextFunction } from '../src';

interface Dummy {
sayHi (name: string): Promise<string>;
addOne (number: number): Promise<number>;
}

describe('class objectHooks', () => {
let DummyClass: new () => Dummy;

beforeEach(() => {
DummyClass = class DummyClass implements Dummy {
async sayHi (name: string) {
return `Hi ${name}`;
}

async addOne (number: number) {
return number + 1;
}
};
});


it('hooking object on class adds to the prototype', async () => {
hooks(DummyClass, {
sayHi: middleware([async (ctx: HookContext, next: NextFunction) => {
assert.deepStrictEqual(ctx, new DummyClass.prototype.sayHi.Context({
arguments: ['David'],
method: 'sayHi',
name: 'David',
self: instance
}));

await next();

ctx.result += '?';
}]).params('name'),

addOne: middleware([async (ctx: HookContext, next: NextFunction) => {
ctx.arguments[0] += 1;

await next();
}])
});

const instance = new DummyClass();

assert.strictEqual(await instance.sayHi('David'), 'Hi David?');
assert.strictEqual(await instance.addOne(1), 3);
});

it('works with inheritance', async () => {
hooks(DummyClass, {
sayHi: middleware([async (ctx: HookContext, next: NextFunction) => {
assert.deepStrictEqual(ctx, new (OtherDummy.prototype.sayHi as any).Context({
arguments: [ 'David' ],
method: 'sayHi',
self: instance
}));

await next();

ctx.result += '?';
}])
});

class OtherDummy extends DummyClass {}

hooks(OtherDummy, {
sayHi: middleware([async (ctx: HookContext, next: NextFunction) => {
await next();

ctx.result += '!';
}])
});

const instance = new OtherDummy();

assert.strictEqual(await instance.sayHi('David'), 'Hi David?!');
});

it('works with multiple context updaters', async () => {
hooks(DummyClass, {
sayHi: middleware([
async (ctx, next) => {
assert.equal(ctx.name, 'Dave');
assert.equal(ctx.gna, 42);
assert.equal(ctx.app, 'ok');

ctx.name = 'Changed';

await next();
}
]).params('name')
});

class OtherDummy extends DummyClass {}

hooks(OtherDummy, {
sayHi: middleware([
async (ctx, next) => {
assert.equal(ctx.name, 'Dave');
assert.equal(ctx.gna, 42);
assert.equal(ctx.app, 'ok');

await next();
}
]).props({ gna: 42 })
});

const instance = new OtherDummy();

hooks(instance, {
sayHi: middleware([
async (ctx, next) => {
assert.equal(ctx.name, 'Dave');
assert.equal(ctx.gna, 42);
assert.equal(ctx.app, 'ok');

await next();
}
]).props({ app: 'ok' })
});

assert.equal(await instance.sayHi('Dave'), 'Hi Changed');
});
});
77 changes: 27 additions & 50 deletions packages/hooks/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../src';

describe('functionHooks', () => {
const hello = async (name: string, _params: any = {}) => {
const hello = async (name?: string, _params: any = {}) => {
return `Hello ${name}`;
};

Expand Down Expand Up @@ -309,53 +309,30 @@ describe('functionHooks', () => {
assert.deepEqual(Object.keys(resultContext), ['message', 'name', 'arguments', 'result']);
});

// it('creates context with default params', async () => {
// const fn = hooks(hello, {
// middleware: [
// async (ctx, next) => {
// assert.equal(ctx.name, 'Dave');
// assert.deepEqual(ctx.params, {});

// ctx.name = 'Changed';

// await next();
// }
// ],
// context: withParams('name', ['params', {}])
// });

// assert.equal(await fn('Dave'), 'Hello Changed');
// });

// it('is chainable with .params on function', async () => {
// const hook = async function (this: any, context: HookContext, next: NextFunction) {
// await next();
// context.result += '!';
// };
// const exclamation = hooks(hello, middleware([hook]).params(['name', 'Dave']));

// const result = await exclamation();

// assert.equal(result, 'Hello Dave!');
// });

// it('is chainable with .params on object', async () => {
// const hook = async function (this: any, context: HookContext, next: NextFunction) {
// await next();
// context.result += '!';
// };
// const obj = {
// sayHi (name: any) {
// return `Hi ${name}`;
// }
// };

// hooks(obj, {
// sayHi: hooks([hook]).params('name')
// });

// const result = await obj.sayHi('Dave');

// assert.equal(result, 'Hi Dave!');
// });
it('same params and props throw an error', async () => {
const hello = async (name?: string) => {
return `Hello ${name}`;
};
assert.throws(() => hooks(hello, middleware([]).params('name').props({ name: 'David' })), {
message: `Hooks can not have a property and param named 'name'. Use .defaults instead.`
});
});

it('creates context with default params', async () => {
const fn = hooks(hello, middleware([
async (ctx, next) => {
assert.deepEqual(ctx.params, {});

await next();
}]).params('name', 'params').defaults(() => {
return {
name: 'Bertho',
params: {}
}
})
);

assert.equal(await fn('Dave'), 'Hello Dave');
assert.equal(await fn(), 'Hello Bertho');
});
});
Loading

0 comments on commit d380d76

Please sign in to comment.