Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hooks): Finalize default initializer functionality #35

Merged
merged 1 commit into from
May 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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