Skip to content

Commit

Permalink
refactor: improve parseBody function and docs (#2628)
Browse files Browse the repository at this point in the history
* refactor: improve parsebody function and docs

* refactor: Improve test and function from color function

* chore: remove comments

* chore: use the previous function

* refactor: remove the if operator
  • Loading branch information
mvares authored May 7, 2024
1 parent c95e135 commit 413c936
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 100 deletions.
50 changes: 16 additions & 34 deletions deno_dist/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import { HonoRequest } from '../request.ts'
export type BodyData = Record<string, string | File | (string | File)[]>
export type ParseBodyOptions = {
/**
* Parse all fields with multiple values should be parsed as an array.
* Determines whether all fields with multiple values should be parsed as arrays.
* @default false
* @example
* ```ts
* const data = new FormData()
* data.append('file', 'aaa')
* data.append('file', 'bbb')
* data.append('message', 'hello')
* ```
*
* If `all` is `false`:
* parseBody should return `{ file: 'bbb', message: 'hello' }`
* If all is false:
* parseBody should return { file: 'bbb', message: 'hello' }
*
* If `all` is `true`:
* parseBody should return `{ file: ['aaa', 'bbb'], message: 'hello' }`
* If all is true:
* parseBody should return { file: ['aaa', 'bbb'], message: 'hello' }
*/
all?: boolean
}
Expand All @@ -29,24 +27,16 @@ export const parseBody = async <T extends BodyData = BodyData>(
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
const contentType = headers.get('Content-Type')

if (isFormDataContent(contentType)) {
if (
(contentType !== null && contentType.startsWith('multipart/form-data')) ||
(contentType !== null && contentType.startsWith('application/x-www-form-urlencoded'))
) {
return parseFormData<T>(request, options)
}

return {} as T
}

function isFormDataContent(contentType: string | null): boolean {
if (contentType === null) {
return false
}

return (
contentType.startsWith('multipart/form-data') ||
contentType.startsWith('application/x-www-form-urlencoded')
)
}

async function parseFormData<T extends BodyData = BodyData>(
request: HonoRequest | Request,
options: ParseBodyOptions
Expand Down Expand Up @@ -80,23 +70,15 @@ function convertFormDataToBodyData<T extends BodyData = BodyData>(
}

const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
if (form[key] && isArrayField(form[key])) {
appendToExistingArray(form[key] as (string | File)[], value)
const formKey = form[key] as (string | File)[]

if (form[key] && Array.isArray(form[key])) {
formKey.push(value)
} else if (form[key]) {
convertToNewArray(form, key, value)
const parsedKey = [...formKey].join('').replace(',', '')

form[key] = [parsedKey, value]
} else {
form[key] = value
}
}

function isArrayField(field: unknown): field is (string | File)[] {
return Array.isArray(field)
}

const appendToExistingArray = (arr: (string | File)[], value: FormDataEntryValue): void => {
arr.push(value)
}

const convertToNewArray = (form: BodyData, key: string, value: FormDataEntryValue): void => {
form[key] = [form[key] as string | File, value]
}
2 changes: 2 additions & 0 deletions deno_dist/utils/color.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export function getColorEnabled() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { process, Deno } = globalThis as any

const isNoColor =
typeof process !== 'undefined'
? // eslint-disable-next-line no-unsafe-optional-chaining
'NO_COLOR' in process?.env
: typeof Deno?.noColor === 'boolean'
? (Deno.noColor as boolean)
: false

return !isNoColor
}
64 changes: 36 additions & 28 deletions src/utils/body.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
import { parseBody } from './body'

describe('Parse Body Util', () => {
const FORM_URL = 'https://localhost/form'
const SEARCH_URL = 'https://localhost/search'

const createRequest = (
url: string,
method: 'POST',
body: BodyInit,
headers?: { [key: string]: string }
) => {
return new Request(url, {
method,
body,
headers,
})
}

it('should parse `multipart/form-data`', async () => {
const data = new FormData()
data.append('message', 'hello')
const req = new Request('https://localhost/form', {
method: 'POST',
body: data,
// `Content-Type` header must not be set.
})

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req)).toEqual({ message: 'hello' })
})

it('should parse `x-www-form-urlencoded`', async () => {
const searchParams = new URLSearchParams()
searchParams.append('message', 'hello')
const req = new Request('https://localhost/search', {
method: 'POST',
body: searchParams,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},

const req = createRequest(SEARCH_URL, 'POST', searchParams, {
'Content-Type': 'application/x-www-form-urlencoded',
})

expect(await parseBody(req)).toEqual({ message: 'hello' })
})

it('should not parse multiple values in default', async () => {
const data = new FormData()
data.append('file', 'aaa')
data.append('file', 'bbb')
data.append('message', 'hello')
const req = new Request('https://localhost/form', {
method: 'POST',
body: data,
})

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req)).toEqual({
file: 'bbb',
message: 'hello',
Expand All @@ -45,10 +55,9 @@ describe('Parse Body Util', () => {
data.append('file', 'aaa')
data.append('file', 'bbb')
data.append('message', 'hello')
const req = new Request('https://localhost/form', {
method: 'POST',
body: data,
})

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req, { all: true })).toEqual({
file: ['aaa', 'bbb'],
message: 'hello',
Expand All @@ -60,10 +69,9 @@ describe('Parse Body Util', () => {
data.append('file[]', 'aaa')
data.append('file[]', 'bbb')
data.append('message', 'hello')
const req = new Request('https://localhost/form', {
method: 'POST',
body: data,
})

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req, { all: true })).toEqual({
'file[]': ['aaa', 'bbb'],
message: 'hello',
Expand All @@ -72,11 +80,11 @@ describe('Parse Body Util', () => {

it('should return blank object if body is JSON', async () => {
const payload = { message: 'hello hono' }
const req = new Request('http://localhost/json', {
method: 'POST',
body: JSON.stringify(payload),
headers: new Headers({ 'Content-Type': 'application/json' }),

const req = createRequest('http://localhost/json', 'POST', JSON.stringify(payload), {
'Content-Type': 'application/json',
})

expect(await parseBody(req)).toEqual({})
})
})
50 changes: 16 additions & 34 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import { HonoRequest } from '../request'
export type BodyData = Record<string, string | File | (string | File)[]>
export type ParseBodyOptions = {
/**
* Parse all fields with multiple values should be parsed as an array.
* Determines whether all fields with multiple values should be parsed as arrays.
* @default false
* @example
* ```ts
* const data = new FormData()
* data.append('file', 'aaa')
* data.append('file', 'bbb')
* data.append('message', 'hello')
* ```
*
* If `all` is `false`:
* parseBody should return `{ file: 'bbb', message: 'hello' }`
* If all is false:
* parseBody should return { file: 'bbb', message: 'hello' }
*
* If `all` is `true`:
* parseBody should return `{ file: ['aaa', 'bbb'], message: 'hello' }`
* If all is true:
* parseBody should return { file: ['aaa', 'bbb'], message: 'hello' }
*/
all?: boolean
}
Expand All @@ -29,24 +27,16 @@ export const parseBody = async <T extends BodyData = BodyData>(
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
const contentType = headers.get('Content-Type')

if (isFormDataContent(contentType)) {
if (
(contentType !== null && contentType.startsWith('multipart/form-data')) ||
(contentType !== null && contentType.startsWith('application/x-www-form-urlencoded'))
) {
return parseFormData<T>(request, options)
}

return {} as T
}

function isFormDataContent(contentType: string | null): boolean {
if (contentType === null) {
return false
}

return (
contentType.startsWith('multipart/form-data') ||
contentType.startsWith('application/x-www-form-urlencoded')
)
}

async function parseFormData<T extends BodyData = BodyData>(
request: HonoRequest | Request,
options: ParseBodyOptions
Expand Down Expand Up @@ -80,23 +70,15 @@ function convertFormDataToBodyData<T extends BodyData = BodyData>(
}

const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
if (form[key] && isArrayField(form[key])) {
appendToExistingArray(form[key] as (string | File)[], value)
const formKey = form[key] as (string | File)[]

if (form[key] && Array.isArray(form[key])) {
formKey.push(value)
} else if (form[key]) {
convertToNewArray(form, key, value)
const parsedKey = [...formKey].join('').replace(',', '')

form[key] = [parsedKey, value]
} else {
form[key] = value
}
}

function isArrayField(field: unknown): field is (string | File)[] {
return Array.isArray(field)
}

const appendToExistingArray = (arr: (string | File)[], value: FormDataEntryValue): void => {
arr.push(value)
}

const convertToNewArray = (form: BodyData, key: string, value: FormDataEntryValue): void => {
form[key] = [form[key] as string | File, value]
}
11 changes: 7 additions & 4 deletions src/utils/color.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { getColorEnabled } from './color'

describe('getColorEnabled()', () => {
it('getColorEnabled() is true', async () => {
describe('getColorEnabled() - With colors enabled', () => {
it('should return true', async () => {
expect(getColorEnabled()).toBe(true)
})
})
describe('getColorEnabled() in NO_COLOR', () => {

describe('getColorEnabled() - With NO_COLOR environment variable set', () => {
beforeAll(() => {
vi.stubEnv('NO_COLOR', '1')
})

afterAll(() => {
vi.unstubAllEnvs()
})
it('getColorEnabled() is false', async () => {

it('should return false', async () => {
expect(getColorEnabled()).toBe(false)
})
})
2 changes: 2 additions & 0 deletions src/utils/color.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export function getColorEnabled() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { process, Deno } = globalThis as any

const isNoColor =
typeof process !== 'undefined'
? // eslint-disable-next-line no-unsafe-optional-chaining
'NO_COLOR' in process?.env
: typeof Deno?.noColor === 'boolean'
? (Deno.noColor as boolean)
: false

return !isNoColor
}

0 comments on commit 413c936

Please sign in to comment.