0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-24 19:26:56 +01:00

feat: Add new shortcuts for request/response (#62)

* Add new shortcuts for request/response

* We have only `param`, we can not use `params`

* Update readme

* tweak
This commit is contained in:
Yusuke Wada 2022-01-26 12:38:20 +09:00
parent ff375c6b69
commit 68444b6932
11 changed files with 184 additions and 67 deletions

View File

@ -35,7 +35,7 @@ Fastest is hono
Below is a demonstration to create an application of Cloudflare Workers with Hono.
![Demo](https://user-images.githubusercontent.com/10682/148223268-2484a891-57c1-472f-9df3-936a5586f002.gif)
![Demo](https://user-images.githubusercontent.com/10682/151102477-be0f950e-8d23-49c5-b6d8-d8ecb6b7484e.gif)
## Install
@ -90,7 +90,7 @@ app.all('/hello', (c) => c.text('Any Method /hello'))
```js
app.get('/user/:name', (c) => {
const name = c.req.params('name')
const name = c.req.param('name')
...
})
```
@ -99,8 +99,8 @@ app.get('/user/:name', (c) => {
```js
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
const date = c.req.params('date')
const title = c.req.params('title')
const date = c.req.param('date')
const title = c.req.param('title')
...
```
@ -219,13 +219,18 @@ To handle Request and Reponse easily, you can use Context object:
### c.req
```js
// Get Request object
app.get('/hello', (c) => {
const userAgent = c.req.headers.get('User-Agent')
...
})
// Shortcut to get a header value
app.get('/shortcut', (c) => {
const userAgent = c.req.header('User-Agent')
...
})
// Query params
app.get('/search', (c) => {
const query = c.req.query('q')
@ -234,40 +239,31 @@ app.get('/search', (c) => {
// Captured params
app.get('/entry/:id', (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
...
})
```
### c.res
### Shortcuts for Response
```js
// Response object
app.use('/', (c, next) => {
next()
c.res.headers.append('X-Debug', 'Debug message')
})
```
### c.event
```js
// FetchEvent object
app.use('*', async (c, next) => {
c.event.waitUntil(
...
)
await next()
})
```
### c.env
```js
// Environment object for Cloudflare Workers
app.get('*', async c => {
const counter = c.env.COUNTER
...
app.get('/welcome', (c) => {
c.header('X-Message', 'Hello!')
c.header('Content-Type', 'text/plain')
c.status(201)
c.statusText('201 Content Created')
return c.body('Thank you for comming')
/*
Same as:
return new Response('Thank you for comming', {
status: 201,
statusText: '201 Content Created',
headers: {
'X-Message': 'Hello',
'Content-Type': 'text/plain'
}
})
*/
})
```
@ -310,6 +306,38 @@ app.get('/redirect', (c) => c.redirect('/'))
app.get('/redirect-permanently', (c) => c.redirect('/', 301))
```
### c.res
```js
// Response object
app.use('/', (c, next) => {
next()
c.res.headers.append('X-Debug', 'Debug message')
})
```
### c.event
```js
// FetchEvent object
app.use('*', async (c, next) => {
c.event.waitUntil(
...
)
await next()
})
```
### c.env
```js
// Environment object for Cloudflare Workers
app.get('*', async c => {
const counter = c.env.COUNTER
...
})
```
## fire
`app.fire()` do:

View File

@ -55,7 +55,7 @@ app.get('/hello', () => new Response('This is /hello'))
// Named parameter
app.get('/entry/:id', (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
return c.text(`Your ID is ${id}`)
})
// Redirect
@ -71,7 +71,7 @@ app.get('/fetch-url', async (c) => {
// Request headers
app.get('/user-agent', (c) => {
const userAgent = c.req.headers.get('User-Agent')
const userAgent = c.req.header('User-Agent')
return c.text(`Your UserAgent is ${userAgent}`)
})
@ -90,8 +90,8 @@ app.post('/api/posts', (c) => c.json({ message: 'Created!' }, 201))
// default route
app.get('/api/*', (c) => c.text('API endpoint is not found', 404))
app.post('/form', async (ctx) => {
return ctx.json(ctx.req.parsedBody || {})
app.post('/form', async (c) => {
return c.json(c.req.parsedBody || {})
//return new Response('ok /form')
})

View File

@ -24,7 +24,7 @@ export const create: Handler = async (c) => {
}
export const show: Handler = async (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
const post = await Model.getPost(id)
if (!post) {
return c.json({ error: 'Not Found', ok: false }, 404)
@ -33,7 +33,7 @@ export const show: Handler = async (c) => {
}
export const update: Handler = async (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
if (!(await Model.getPost(id))) {
// 204 No Content
return c.json({ ok: false }, 204)
@ -44,7 +44,7 @@ export const update: Handler = async (c) => {
}
export const destroy: Handler = async (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
if (!(await Model.getPost(id))) {
// 204 No Content
return c.json({ ok: false }, 204)

View File

@ -10,7 +10,7 @@ app.use('*', async (c, next) => {
app.get('/', (c) => c.text('Hono!! Compute@Edge!!'))
app.get('/hello/:name', (c) => {
return c.text(`Hello ${c.req.params('name')}!!!!!`)
return c.text(`Hello ${c.req.param('name')}!!!!!`)
})
app.fire()

View File

@ -48,7 +48,7 @@ app.get('/', (c) => {
})
app.get('/post/:id{[0-9]+}', (c) => {
const id = c.req.params('id')
const id = c.req.param('id')
const post = getPost(id)
if (!post) {
return c.text('Not Found', 404)

View File

@ -9,6 +9,10 @@ export class Context {
res: Response
env: Env
event: FetchEvent
private _headers: Headers
private _status: number
private _statusText: string
body: (body: BodyInit, init?: ResponseInit) => Response
constructor(req: Request, opts?: { res: Response; env: Env; event: FetchEvent }) {
this.req = req
@ -17,9 +21,26 @@ export class Context {
this.env = opts.env
this.event = opts.event
}
this._headers = {}
this.body = this.newResponse
}
newResponse(body?: BodyInit | null | undefined, init?: ResponseInit | undefined): Response {
header(name: string, value: string): void {
this._headers[name] = value
}
status(number: number): void {
this._status = number
}
statusText(text: string): void {
this._statusText = text
}
newResponse(body: BodyInit, init: ResponseInit = {}): Response {
init.status = this._status || init.status
init.statusText = this._statusText || init.statusText
init.headers = { ...init.headers, ...this._headers }
return new Response(body, init)
}

View File

@ -10,8 +10,9 @@ const METHOD_NAME_OF_ALL = 'ALL'
declare global {
interface Request {
params: (key: string) => string
param: (key: string) => string
query: (key: string) => string | null
header: (name: string) => string
parsedBody: any
}
}
@ -126,7 +127,7 @@ export class Hono {
const result = await this.matchRoute(method, path)
request.params = (key: string): string => {
request.param = (key: string): string => {
if (result) {
return result.params[key]
}

View File

@ -6,6 +6,9 @@ export const defaultMiddleware = async (c: Context, next: Function) => {
const url = new URL(c.req.url)
return url.searchParams.get(key)
}
c.req.header = (name: string): string => {
return c.req.headers.get(name)
}
await next()

View File

@ -18,7 +18,7 @@ describe('Logger by Middleware', () => {
app.get('/empty', () => new Response(''))
it('Log status 200 with empty body', async () => {
const req = new Request('http://localhost/short')
const req = new Request('http://localhost/empty')
const res = await app.dispatch(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
@ -44,7 +44,7 @@ describe('Logger by Middleware', () => {
expect(log.endsWith(` ${longRandomString.length / 1024}kB`)).toBe(true)
})
it.only('Log status 404', async () => {
it('Log status 404', async () => {
const msg = 'Default 404 Nout Found'
app.notFound = () => {
return new Response(msg, { status: 404 })

View File

@ -4,7 +4,7 @@ describe('Context', () => {
const req = new Request('http://localhost/')
const c = new Context(req)
it('c.text', async () => {
it('c.text()', async () => {
const res = c.text('text in c', 201, { 'X-Custom': 'Message' })
expect(res.status).toBe(201)
expect(res.headers.get('Content-Type')).toBe('text/plain')
@ -12,7 +12,7 @@ describe('Context', () => {
expect(res.headers.get('X-Custom')).toBe('Message')
})
it('c.json', async () => {
it('c.json()', async () => {
const res = c.json({ message: 'Hello' }, 201, { 'X-Custom': 'Message' })
expect(res.status).toBe(201)
expect(res.headers.get('Content-Type')).toMatch('application/json')
@ -22,7 +22,7 @@ describe('Context', () => {
expect(res.headers.get('X-Custom')).toBe('Message')
})
it('c.html', async () => {
it('c.html()', async () => {
const res = c.html('<h1>Hello! Hono!</h1>', 201, { 'X-Custom': 'Message' })
expect(res.status).toBe(201)
expect(res.headers.get('Content-Type')).toMatch('text/html')
@ -30,7 +30,7 @@ describe('Context', () => {
expect(res.headers.get('X-Custom')).toBe('Message')
})
it('c.redirect', async () => {
it('c.redirect()', async () => {
let res = c.redirect('/destination')
expect(res.status).toBe(302)
expect(res.headers.get('Location')).toMatch(/^https?:\/\/.+\/destination$/)
@ -38,4 +38,30 @@ describe('Context', () => {
expect(res.status).toBe(302)
expect(res.headers.get('Location')).toBe('https://example.com/destination')
})
it('c.header()', async () => {
c.header('X-Foo', 'Bar')
const res = c.body('Hi')
const foo = res.headers.get('X-Foo')
expect(foo).toBe('Bar')
})
it('c.status() and c.statusText()', async () => {
c.status(201)
c.statusText('Created!!!!')
const res = c.body('Hi')
expect(res.status).toBe(201)
expect(res.statusText).toBe('Created!!!!')
})
it('Complext pattern', async () => {
c.status(404)
c.statusText('Hono is Not Found')
const res = c.json({ hono: 'great app' })
expect(res.status).toBe(404)
expect(res.statusText).toBe('Hono is Not Found')
expect(res.headers.get('Content-Type')).toMatch(/json/)
const obj: { [key: string]: string } = await res.json()
expect(obj['hono']).toBe('great app')
})
})

View File

@ -2,16 +2,26 @@ import { Hono } from '../src/hono'
describe('GET Request', () => {
const app = new Hono()
app.get('/hello', () => {
return new Response('hello', {
status: 200,
})
})
app.get('/hello-with-shortcuts', (c) => {
c.header('X-Custom', 'This is Hono')
c.statusText('Hono is created')
c.status(201)
return c.html('<h1>Hono!!!</h1>')
})
app.notFound = () => {
return new Response('not found', {
status: 404,
})
}
it('GET /hello is ok', async () => {
const req = new Request('http://localhost/hello')
const res = await app.dispatch(req)
@ -19,6 +29,18 @@ describe('GET Request', () => {
expect(res.status).toBe(200)
expect(await res.text()).toBe('hello')
})
it('GET /hell-with-shortcuts is ok', async () => {
const req = new Request('http://localhost/hello-with-shortcuts')
const res = await app.dispatch(req)
expect(res).not.toBeNull()
expect(res.status).toBe(201)
expect(res.statusText).toBe('Hono is created')
expect(res.headers.get('X-Custom')).toBe('This is Hono')
expect(res.headers.get('Content-Type')).toMatch(/text\/html/)
expect(await res.text()).toBe('<h1>Hono!!!</h1>')
})
it('GET / is not found', async () => {
const req = new Request('http://localhost/')
const res = await app.dispatch(req)
@ -84,35 +106,51 @@ describe('Routing', () => {
})
})
describe('params and query', () => {
describe('param and query', () => {
const app = new Hono()
app.get('/entry/:id', async (c) => {
const id = await c.req.params('id')
app.get('/entry/:id', (c) => {
const id = c.req.param('id')
return new Response(`id is ${id}`, {
status: 200,
})
})
app.get('/search', async (c) => {
const name = await c.req.query('name')
app.get('/search', (c) => {
const name = c.req.query('name')
return new Response(`name is ${name}`, {
status: 200,
})
})
it('params of /entry/:id is found', async () => {
app.get('/add-header', (c) => {
const bar = c.req.header('X-Foo')
return new Response(`foo is ${bar}`, {
status: 200,
})
})
it('param of /entry/:id is found', async () => {
const req = new Request('http://localhost/entry/123')
const res = await app.dispatch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('id is 123')
})
it('query of /search?name=sam is found', async () => {
const req = new Request('http://localhost/search?name=sam')
const res = await app.dispatch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('name is sam')
})
it('/add-header header - X-Foo is Bar', async () => {
const req = new Request('http://localhost/add-header')
req.headers.append('X-Foo', 'Bar')
const res = await app.dispatch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('foo is Bar')
})
})
describe('Middleware', () => {
@ -127,24 +165,24 @@ describe('Middleware', () => {
// Apeend Custom Header
app.use('*', async (c, next) => {
await next()
await c.res.headers.append('x-custom', 'root')
c.res.headers.append('x-custom', 'root')
})
app.use('/hello', async (c, next) => {
await next()
await c.res.headers.append('x-message', 'custom-header')
c.res.headers.append('x-message', 'custom-header')
})
app.use('/hello/*', async (c, next) => {
await next()
await c.res.headers.append('x-message-2', 'custom-header-2')
c.res.headers.append('x-message-2', 'custom-header-2')
})
app.get('/hello', () => {
return new Response('hello')
})
app.get('/hello/:message', (c) => {
const message = c.req.params('message')
const message = c.req.param('message')
return new Response(`${message}`)
})
@ -153,18 +191,18 @@ describe('Middleware', () => {
const res = await app.dispatch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('hello')
expect(await res.headers.get('x-custom')).toBe('root')
expect(await res.headers.get('x-message')).toBe('custom-header')
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
expect(res.headers.get('x-custom')).toBe('root')
expect(res.headers.get('x-message')).toBe('custom-header')
expect(res.headers.get('x-message-2')).toBe('custom-header-2')
})
it('logging and custom header with named params', async () => {
it('logging and custom header with named param', async () => {
const req = new Request('http://localhost/hello/message')
const res = await app.dispatch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('message')
expect(await res.headers.get('x-custom')).toBe('root')
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
expect(res.headers.get('x-custom')).toBe('root')
expect(res.headers.get('x-message-2')).toBe('custom-header-2')
})
})