Skip to content

Commit

Permalink
Merge c6686c8 into 832043b
Browse files Browse the repository at this point in the history
  • Loading branch information
dmanto committed Jun 20, 2023
2 parents 832043b + c6686c8 commit accd118
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 5 deletions.
38 changes: 34 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import Path from '@mojojs/path';
type AppHook = (app: App, ...args: any[]) => any;
type ContextHook = (app: MojoContext, ...args: any[]) => any;

type Decoration = ((...args: any[]) => any) | {get?: () => any; set?: (value: any) => any};
type Decoration = ((...args: any[]) => any) | {get?: () => any; set?: (value: any) => any; configurable?: boolean};

const ContextWrapper = class extends Context {};

Expand Down Expand Up @@ -185,6 +185,7 @@ export class App {
validator = new Validator();

_contextClass: any = class extends ContextWrapper {};
_nestedFunctions: Record<string, Record<string, MojoAction>> = {};

constructor(options: AppOptions = {}) {
this.config = options.config ?? {};
Expand Down Expand Up @@ -236,11 +237,40 @@ export class App {
* ctx.res.set('X-Mojo', 'I <3 mojo.js!');
* await ctx.render(...args);
* });
*
* // Render response with header using nested helper
* app.addHelper('renderWith.header', async (ctx, ...args) => {
* ctx.res.set('X-Mojo', 'I <3 mojo.js!');
* await ctx.render(...args);
* });
*/
addHelper(name: string, fn: MojoAction): this {
return this.decorateContext(name, function (this: MojoContext, ...args: any[]) {
return fn(this, ...args);
});
const nestedNames = name.split('.');
if (nestedNames.length === 1) {
return this.decorateContext(name, function (this: MojoContext, ...args: any[]) {
return fn(this, ...args);
});
} else if (nestedNames.length === 2) {
const [getterName, methodName] = nestedNames;
this._nestedFunctions[getterName] ??= {};
this._nestedFunctions[getterName][methodName] = fn;
const fnEntries = Object.entries<MojoAction>(this._nestedFunctions[getterName]);
return this.decorateContext(getterName, {
get: function (this: MojoContext) {
if (this._cachedFunctions[getterName] === undefined) {
const ctxScopedFunctions: Record<string, MojoAction> = {};
for (const [key, fn] of fnEntries) {
ctxScopedFunctions[key] = (...args: any[]) => fn(this as MojoContext, ...args);
}
this._cachedFunctions[getterName] = ctxScopedFunctions;
}
return this._cachedFunctions[getterName];
},
configurable: true
});
} else {
throw new Error(`The name "${name}" exceeds maximum depth (2) for nested helpers`);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Context extends EventEmitter {
*/
stash: Record<string, any> = {};

_cachedFunctions: Record<string, Record<string, MojoAction>> = {};
_flash: SessionData | undefined = undefined;
_params: Params | undefined = undefined;
_session: Record<string, any> | undefined = undefined;
Expand Down
14 changes: 14 additions & 0 deletions test/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1092,5 +1092,19 @@ t.test('App', async t => {
t.end();
});

t.test('Nested helper depth limit', t => {
let result;
try {
app.addHelper('too.many.indents', function () {
throw new Error('Fail!');
});
} catch (error) {
result = error;
}
t.match(result, /The name "too\.many\.indents" exceeds maximum depth \(2\) for nested helpers/);

t.end();
});

await ua.stop();
});
51 changes: 50 additions & 1 deletion test/plugin-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ t.test('Plugin app', async t => {
app.get('/form_helpers', ctx => ctx.render({inline: formTagHelpers}));

app.get('/helper', ctx => ctx.render({text: ctx.testHelper('test')}));
app.get('/nested/helper', ctx => ctx.render({text: ctx.nested.testHelper('test')}));
app.get('/nested/helper_with_stash/:test', ctx => ctx.render({text: ctx.nested.testStashHelper('test')}));
app.get('/nested/other_helper_with_stash/:test', ctx => ctx.render({text: ctx.other.otherStashHelper('test')}));
app.get('/nested/inexistent_helper_with_stash/:test', ctx => ctx.render({text: ctx.other.testStashHelper('test')}));

app.get('/getter/setter', ctx => {
const before = ctx.testProp;
Expand Down Expand Up @@ -113,6 +117,49 @@ t.test('Plugin app', async t => {
(await ua.getOk('/helper')).statusIs(200).bodyIs('works');
});

await t.test('Nested helper', async () => {
(await ua.getOk('/nested/helper')).statusIs(200).bodyIs('works (nested)');
});

await t.test('Nested helper using stash', async () => {
(await ua.getOk('/nested/helper_with_stash/works')).statusIs(200).bodyIs('works (nested)');
});

await t.test('Nested helper with different root using stash', async () => {
(await ua.getOk('/nested/other_helper_with_stash/works')).statusIs(200).bodyIs('works (nested other)');
});

await t.test('Nested helper with wrong root name', async () => {
const logLevel = app.log.level;
app.log.level = 'fatal';
(await ua.getOk('/nested/inexistent_helper_with_stash/works'))
.statusIs(500)
.bodyLike(/TypeError: ctx\.other\.testStashHelper is not a function/);
app.log.level = logLevel;
});

await t.test('Cached getter object return', t => {
const ctx = app.newMockContext();
ctx.stash.test = 'something';
t.equal(ctx.nested.testStashHelper('test'), 'something (nested)');
const nested1 = ctx.nested;
t.equal(ctx.nested.testStashHelper('test'), 'something (nested)');
t.equal(ctx.nested, nested1);
t.end();
});

await t.test('Nested helpers concurrency', t => {
const ctx1 = app.newMockContext();
const ctx2 = app.newMockContext();
ctx1.stash.test = 'One';
ctx2.stash.test = 'Two';
t.equal(ctx1.nested.testStashHelper('test'), 'One (nested)');
const nested1 = ctx1.nested;
t.equal(ctx2.nested.testStashHelper('test'), 'Two (nested)');
t.equal(nested1.testStashHelper('test'), 'One (nested)');
t.end();
});

await t.test('Decorate with getter and setter', async () => {
(await ua.getOk('/getter/setter')).statusIs(200).bodyIs('before: works, after: also works');
});
Expand Down Expand Up @@ -393,7 +440,9 @@ function mixedPlugin(app) {
app.config.test = 'works';

app.addHelper('testHelper', (ctx, name) => ctx.config[name]);

app.addHelper('nested.testHelper', (ctx, name) => `${ctx.config[name]} (nested)`);
app.addHelper('nested.testStashHelper', (ctx, name) => `${ctx.stash[name]} (nested)`);
app.addHelper('other.otherStashHelper', (ctx, name) => `${ctx.stash[name]} (nested other)`);
app.decorateContext('testMethod', function (name) {
return this.config[name];
});
Expand Down

0 comments on commit accd118

Please sign in to comment.