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(utils): specify detailed return type for parseBody #2771

Merged
merged 13 commits into from
May 24, 2024
102 changes: 102 additions & 0 deletions src/utils/body.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { parseBody } from './body'
import type { BodyData } from './body'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool PR!

I think you can just merge the imports in one import like this

import { parseBody, type BodyData } from './body'

I think we need to setup eslint to avoid imports to the same file @usualoma @yusukebe

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! That's certainly better.
I made the change in eecfc43.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fzn0x

I think we need to setup eslint to avoid imports to the same file @usualoma @yusukebe

That's great! I'll work on it soon.


type RecursiveRecord<K extends string, T> = {
[key in K]: T | RecursiveRecord<K, T>
}

describe('Parse Body Util', () => {
const FORM_URL = 'https://localhost/form'
Expand Down Expand Up @@ -240,4 +245,101 @@ describe('Parse Body Util', () => {

expect(await parseBody(req)).toEqual({})
})

describe('Return type', () => {
let req: Request
beforeEach(() => {
req = createRequest(FORM_URL, 'POST', new FormData())
})

it('without options', async () => {
expectTypeOf((await parseBody(req))['key']).toEqualTypeOf<string | File>()
})

it('{all: true}', async () => {
expectTypeOf((await parseBody(req, { all: true }))['key']).toEqualTypeOf<
string | File | (string | File)[]
>()
})

it('{all: boolean}', async () => {
expectTypeOf((await parseBody(req, { all: !!Math.random() }))['key']).toEqualTypeOf<
string | File | (string | File)[]
>()
})

it('{dot: true}', async () => {
expectTypeOf((await parseBody(req, { dot: true }))['key']).toEqualTypeOf<
string | File | RecursiveRecord<string, string | File>
>()
})

it('{dot: boolean}', async () => {
expectTypeOf((await parseBody(req, { dot: !!Math.random() }))['key']).toEqualTypeOf<
string | File | RecursiveRecord<string, string | File>
>()
})

it('{all: true, dot: true}', async () => {
expectTypeOf((await parseBody(req, { all: true, dot: true }))['key']).toEqualTypeOf<
| string
| File
| (string | File)[]
| RecursiveRecord<string, string | File | (string | File)[]>
>()
})

it('{all: boolean, dot: boolean}', async () => {
expectTypeOf(
(await parseBody(req, { all: !!Math.random(), dot: !!Math.random() }))['key']
).toEqualTypeOf<
| string
| File
| (string | File)[]
| RecursiveRecord<string, string | File | (string | File)[]>
>()
})
})
})

describe('BodyData', () => {
it('without options', async () => {
expectTypeOf(({} as BodyData)['key']).toEqualTypeOf<string | File>()
})

it('{all: true}', async () => {
expectTypeOf(({} as BodyData<{ all: true }>)['key']).toEqualTypeOf<
string | File | (string | File)[]
>()
})

it('{all: boolean}', async () => {
expectTypeOf(({} as BodyData<{ all: boolean }>)['key']).toEqualTypeOf<
string | File | (string | File)[]
>()
})

it('{dot: true}', async () => {
expectTypeOf(({} as BodyData<{ dot: true }>)['key']).toEqualTypeOf<
string | File | RecursiveRecord<string, string | File>
>()
})

it('{dot: boolean}', async () => {
expectTypeOf(({} as BodyData<{ dot: boolean }>)['key']).toEqualTypeOf<
string | File | RecursiveRecord<string, string | File>
>()
})

it('{all: true, dot: true}', async () => {
expectTypeOf(({} as BodyData<{ all: true; dot: true }>)['key']).toEqualTypeOf<
string | File | (string | File)[] | RecursiveRecord<string, string | File | (string | File)[]>
>()
})

it('{all: boolean, dot: boolean}', async () => {
expectTypeOf(({} as BodyData<{ all: boolean; dot: boolean }>)['key']).toEqualTypeOf<
string | File | (string | File)[] | RecursiveRecord<string, string | File | (string | File)[]>
>()
})
})
45 changes: 36 additions & 9 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { HonoRequest } from '../request'

type BodyDataValue = string | File | (string | File)[] | { [key: string]: BodyDataValue }
export type BodyData = Record<string, BodyDataValue>
type BodyDataValueComponent<T> =
| string
| File
| (T extends { all: false }
? never // explicitly set to false
: T extends { all: true } | { all: boolean }
? (string | File)[] // use all option
: never) // without options
type BodyDataValueObject<T> = { [key: string]: BodyDataValueComponent<T> | BodyDataValueObject<T> }
type BodyDataValue<T> =
| BodyDataValueComponent<T>
| (T extends { dot: false }
? never // explicitly set to false
: T extends { dot: true } | { dot: boolean }
? BodyDataValueObject<T> // use dot option
: never) // without options
export type BodyData<T extends Partial<ParseBodyOptions> = {}> = Record<string, BodyDataValue<T>>

export type ParseBodyOptions = {
/**
* Determines whether all fields with multiple values should be parsed as arrays.
Expand Down Expand Up @@ -44,9 +60,12 @@ export type ParseBodyOptions = {
* @param {Partial<ParseBodyOptions>} [options] - Options for parsing the body.
* @returns {Promise<T>} The parsed body data.
*/
export const parseBody = async <T extends BodyData = BodyData>(
export const parseBody = async <
Options extends Partial<ParseBodyOptions>,
T extends BodyData<Options> = BodyData<Options>
>(
request: HonoRequest | Request,
options: Partial<ParseBodyOptions> = Object.create(null)
options: Options = Object.create(null)
): Promise<T> => {
const { all = false, dot = false } = options

Expand All @@ -71,7 +90,7 @@ export const parseBody = async <T extends BodyData = BodyData>(
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {Promise<T>} The parsed body data.
*/
async function parseFormData<T extends BodyData = BodyData>(
async function parseFormData<T extends BodyData>(
request: HonoRequest | Request,
options: ParseBodyOptions
): Promise<T> {
Expand All @@ -92,7 +111,7 @@ async function parseFormData<T extends BodyData = BodyData>(
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {T} The converted body data.
*/
function convertFormDataToBodyData<T extends BodyData = BodyData>(
function convertFormDataToBodyData<T extends BodyData>(
formData: FormData,
options: ParseBodyOptions
): T {
Expand Down Expand Up @@ -134,7 +153,11 @@ function convertFormDataToBodyData<T extends BodyData = BodyData>(
* @param {string} key - The key to parse.
* @param {FormDataEntryValue} value - The value to assign.
*/
const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
const handleParsingAllValues = (
form: BodyData<{ all: true }>,
key: string,
value: FormDataEntryValue
): void => {
if (form[key] !== undefined) {
if (Array.isArray(form[key])) {
;(form[key] as (string | File)[]).push(value)
Expand All @@ -153,7 +176,11 @@ const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntr
* @param {string} key - The dot notation key.
* @param {BodyDataValue} value - The value to assign.
*/
const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataValue): void => {
const handleParsingNestedValues = (
form: BodyData,
key: string,
value: BodyDataValue<Partial<ParseBodyOptions>>
): void => {
let nestedForm = form
const keys = key.split('.')

Expand All @@ -169,7 +196,7 @@ const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataV
) {
nestedForm[key] = Object.create(null)
}
nestedForm = nestedForm[key] as BodyData
nestedForm = nestedForm[key] as unknown as BodyData
}
})
}