Skip to content

Commit

Permalink
Feat(renderer): support handlebar renderer engine (#18)
Browse files Browse the repository at this point in the history
* feat(renderer): add interfance and @Render decorator

* feat(renderer): implement hbs renderer

* docs(renderer): add small tutorial

* fix(renderer): add file to partials folder so it is pushed

* lint(*): deno fmt

* refactor(view-rendere-test): remove console.log

* docs(renderer): add small info

* refactor(app): change actualRendererToRenderer
  • Loading branch information
Sorikairox authored May 23, 2022
1 parent d32526f commit 5251830
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 85 deletions.
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@ Join [our discord](https://discord.gg/Q7ZHuDPgjA)

## Feature

| Task | State |
|----------------------------------------------------------------------------------|---------------------------------|
| Controllers and routes creation with Decorators (GET, PUT, DELETE, POST, PATCH) | Complete ✅ |
| Module with dependency injection | Complete ✅ |
| Request, Response and Body injection in Routes via decorators | Complete ✅ |
| Route parameter decorator factory to easily create param decorators (even async) | Complete ✅ |
| Url params injection in Controllers with Decorators | Complete ✅ |
| Guards implementations for controllers and methods | Complete ✅ |
| Global guard | Complete ✅ |
| `@Inject('token')` to use with interfaces | Complete ✅ |
| Exceptions Filters that catch all errors and `@UseFilters` decorator | Complete ✅ |
| Exceptions Filters that catch only a specific type of error | Complete ✅ |
| `OnAppClose` and `OnAppBootstrap` Lifecycle hook for injectables and controllers | Complete ✅ |
| Create documentation | Help wanted 🆘 |
| Logger | Complete ✅ |
| Starter repo | Help wanted 🆘 |
| Support Handlebars engine | Help wanted 🆘 |
| Anything we have in mind | Waiting |
| Task | State |
| -------------------------------------------------------------------------------- | ---------------------------------- |
| Controllers and routes creation with Decorators (GET, PUT, DELETE, POST, PATCH) | Complete ✅ |
| Module with dependency injection | Complete ✅ |
| Request, Response and Body injection in Routes via decorators | Complete ✅ |
| Route parameter decorator factory to easily create param decorators (even async) | Complete ✅ |
| Url params injection in Controllers with Decorators | Complete ✅ |
| Guards implementations for controllers and methods | Complete ✅ |
| Global guard | Complete ✅ |
| `@Inject('token')` to use with interfaces | Complete ✅ |
| Exceptions Filters that catch all errors and `@UseFilters` decorator | Complete ✅ |
| Exceptions Filters that catch only a specific type of error | Complete ✅ |
| `OnAppClose` and `OnAppBootstrap` Lifecycle hook for injectables and controllers | Complete ✅ |
| Create documentation | In progress ⌛ (does it even end ?) |
| Logger | Complete ✅ |
| Starter repo | Help wanted 🆘 |
| Support Handlebars engine | Help wanted 🆘 |
| Anything we have in mind | Waiting |

## Docs

Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
"emitDecoratorMetadata": true
},
"tasks": {
"test": "NO_LOG=true deno test --allow-env --allow-net -A --unstable --coverage=coverage spec"
"test": "NO_LOG=true deno test --allow-env --allow-net --allow-read -A --unstable --coverage=coverage spec"
}
}
63 changes: 63 additions & 0 deletions doc/renderer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Rendering HTML

Building API is cool, but sometime, we want to build a simple MVC app that will
render HTML.

For this, Danet integrate the [Handlebars](https://handlebarsjs.com/) templating
engine.

## Before writing any code

#### Create the following directory at your project's root

```
/views
/views/layouts
/views/partials
```

!!!info Info
If you want to put these directory elsewhere, you can provide the path to `views` at runtime with `app.setViewEngineDir('my/path/to/views);`
!!!

#### Create a default layout called `main.hbs` with the following content:

```handlebars
{{{body}}}
```

## Let's render things now !

First, let's create your first template called `hello.hbs` in the `views`
directory. It will print 2 variables passed from your controller.

```handlebars
<html>
<head>
<meta charset="utf-8" />
<title>{{ title }}</title>
</head>
<body>
Hello {{ name }}!
</body>
</html>
```

Now, let's tell your controller it has to render this view on a specific route:

```ts
@Controller('nice-controller')
class MyController {
@Render('hello')
@Get('/')
renderANiceHTML() {
return { title: 'the page title', name: 'world' };
}
}
```

We specify the template to use with the `@Render()` decorator, and the return
value of the route handler is passed to the template for rendering.

Notice that the return value is an object with `title` and `name` properties,
matching `title` and `name` placeholders we used in the template.
134 changes: 70 additions & 64 deletions spec/exception-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,99 +7,105 @@ import { Controller, Get } from '../src/router/controller/decorator.ts';
import { HttpContext } from '../src/router/router.ts';

class CustomException extends Error {
public customField = 'i am a custom field';
constructor(text: string) {
super(text);
}
public customField = 'i am a custom field';
constructor(text: string) {
super(text);
}
}

class ErrorFilter implements ExceptionFilter {
catch(exception: any, context: HttpContext) {
context.response.body = {
wePassedInFilterCatchingAllErrors: true,
};
}
catch(exception: any, context: HttpContext) {
context.response.body = {
wePassedInFilterCatchingAllErrors: true,
};
}
}

@Catch(CustomException)
class CustomErrorFilter implements ExceptionFilter {
catch(exception: any, context: HttpContext) {
context.response.body = {
wePassedInFilterCatchingOnlySomeError: true,
};
}
catch(exception: any, context: HttpContext) {
context.response.body = {
wePassedInFilterCatchingOnlySomeError: true,
};
}
}

@UseFilter(ErrorFilter)
@Controller('')
class ControllerWithFilter {

@Get('/')
simpleGet() {
throw Error('an error');
}
@Get('/')
simpleGet() {
throw Error('an error');
}
}

@Controller('custom-error')
class ControllerWithCustomFilter {
@UseFilter(CustomErrorFilter)
@Get('')
customError() {
throw new CustomException('an error');
}
@UseFilter(CustomErrorFilter)
@Get('')
customError() {
throw new CustomException('an error');
}

@Get('unexpected-error')
unexpectedError() {
throw Error('unexpected');
}
@Get('unexpected-error')
unexpectedError() {
throw Error('unexpected');
}
}
@Module({
controllers: [ControllerWithFilter, ControllerWithCustomFilter]
controllers: [ControllerWithFilter, ControllerWithCustomFilter],
})
class ModuleWithFilter {}

for (const testName of ['Exception Filter with @Catch catch related errors', 'Method exception filter works']) {
Deno.test(testName, async () => {
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;
for (
const testName of [
'Exception Filter with @Catch catch related errors',
'Method exception filter works',
]
) {
Deno.test(testName, async () => {
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;

const res = await fetch(`http://localhost:${port}/custom-error`, {
method: 'GET',
});
const json = await res.json();
assertEquals(json, {
wePassedInFilterCatchingOnlySomeError: true,
});
await app.close();
});
const res = await fetch(`http://localhost:${port}/custom-error`, {
method: 'GET',
});
const json = await res.json();
assertEquals(json, {
wePassedInFilterCatchingOnlySomeError: true,
});
await app.close();
});
}

Deno.test('Controller filter works', async () => {
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;

const res = await fetch(`http://localhost:${port}`, {
method: 'GET',
});
const json = await res.json();
assertEquals(json, {
wePassedInFilterCatchingAllErrors: true,
});
await app.close();
const res = await fetch(`http://localhost:${port}`, {
method: 'GET',
});
const json = await res.json();
assertEquals(json, {
wePassedInFilterCatchingAllErrors: true,
});
await app.close();
});

Deno.test('throw 500 on unexpected error', async () => {
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;
const app = new DanetApplication();
await app.init(ModuleWithFilter);
const port = (await app.listen(0)).port;

const res = await fetch(`http://localhost:${port}/custom-error/unexpected-error`, {
method: 'GET',
});
assertEquals(500, res.status);
await res.json();
await app.close();
const res = await fetch(
`http://localhost:${port}/custom-error/unexpected-error`,
{
method: 'GET',
},
);
assertEquals(500, res.status);
await res.json();
await app.close();
});

35 changes: 35 additions & 0 deletions spec/view-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { assertEquals } from 'https://deno.land/[email protected]/testing/asserts.ts';
import * as path from 'https://deno.land/[email protected]/path/mod.ts';
import { DanetApplication } from '../src/app.ts';
import { Module } from '../src/module/decorator.ts';
import { Render } from '../src/renderer/decorator.ts';
import { Controller, Get } from '../src/router/controller/decorator.ts';

@Controller('nice-controller')
class SimpleController {
@Render('index')
@Get('/')
simpleGet() {
return { title: 'my title' };
}
}

@Module({
controllers: [SimpleController],
})
class MyModule {}

Deno.test('Hbs renderer', async () => {
const app = new DanetApplication();
await app.init(MyModule);
const viewPath = path.dirname(path.fromFileUrl(import.meta.url)) + '/views';
app.setViewEngineDir(viewPath);
const port = (await app.listen(0)).port;

const res = await fetch(`http://localhost:${port}/nice-controller`, {
method: 'GET',
});
const text = await res.text();
assertEquals(text.includes('my title'), true);
await app.close();
});
1 change: 1 addition & 0 deletions spec/views/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{title}}
1 change: 1 addition & 0 deletions spec/views/layouts/main.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{{body}}}
Empty file.
15 changes: 14 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { ApplicationListenEvent } from 'https://deno.land/x/[email protected]/application.ts';
import { Application, Router } from 'https://deno.land/x/[email protected]/mod.ts';
import { FilterExecutor } from './exception/filter/executor.ts';
import { GuardExecutor } from './guard/executor.ts';
import { HookExecutor } from './hook/executor.ts';
import { hookName } from './hook/interfaces.ts';

import { Injector } from './injector/injector.ts';
import { Logger } from './logger.ts';
import { MetadataHelper } from './metadata/helper.ts';
import { moduleMetadataKey, ModuleOptions } from './module/decorator.ts';
import { HandlebarRenderer } from './renderer/handlebar.ts';
import { DanetRouter } from './router/router.ts';
import { Constructor } from './utils/constructor.ts';

export class DanetApplication {
private app = new Application();
private injector = new Injector();
private hookExecutor = new HookExecutor(this.injector);
public danetRouter = new DanetRouter(this.injector);
private renderer = new HandlebarRenderer();
public danetRouter = new DanetRouter(
this.injector,
new GuardExecutor(this.injector),
new FilterExecutor(),
this.renderer,
);
private controller: AbortController = new AbortController();
private logger: Logger = new Logger('DanetApplication');

Expand Down Expand Up @@ -69,4 +78,8 @@ export class DanetApplication {
get router(): Router {
return this.danetRouter.router;
}

setViewEngineDir(path: string) {
this.renderer.setRootDir(path);
}
}
20 changes: 20 additions & 0 deletions src/renderer/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MetadataHelper } from '../metadata/helper.ts';
import { ControllerConstructor } from '../router/controller/constructor.ts';

export const rendererViewFile = 'rendererViewFile';
export const Render = (fileName: string) =>
(
// deno-lint-ignore ban-types
target: ControllerConstructor | Object,
propertyKey?: string | symbol,
// deno-lint-ignore no-explicit-any
descriptor?: TypedPropertyDescriptor<any>,
) => {
if (propertyKey && descriptor) {
MetadataHelper.setMetadata(
rendererViewFile,
fileName,
descriptor.value,
);
}
};
Loading

0 comments on commit 5251830

Please sign in to comment.