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(fetch): add safeParse function #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/fetch/src/addons/parse/_types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
export interface FfetchTypeProvider {
readonly schema: unknown
readonly parsed: unknown
readonly safeParsed: unknown
}

export interface FfetchParser<TypeProvider extends FfetchTypeProvider> {
readonly _provider: TypeProvider
parse: (schema: unknown, value: unknown) => unknown | Promise<unknown>
safeParse: (schema: unknown, value: unknown) => unknown | Promise<unknown>
}

export type CallTypeProvider<TypeProvider extends FfetchTypeProvider, Schema> = (TypeProvider & { schema: Schema })['parsed']
export type CallSafeTypeProvider<TypeProvider extends FfetchTypeProvider, Schema> = (TypeProvider & { schema: Schema })['safeParsed']
14 changes: 14 additions & 0 deletions packages/fetch/src/addons/parse/adapters/valibot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ describe('zod', () => {
expect(res).toEqual('hello')
})

it('should safely parse a type (pass)', async () => {
const parser = ffetchValibotAdapter()
const res = await parser.safeParse(v.string(), 'hello')

expect(res).toMatchObject({ success: true, output: 'hello' })
})

it('should safely parse a type (fail)', async () => {
const parser = ffetchValibotAdapter()
const res = await parser.safeParse(v.string(), 42)

expect(res).toMatchObject({ success: false })
})

it('should work with ffetch (pass)', async () => {
const fetch_ = vi.fn<typeof fetch>()
.mockImplementation(async () => new Response('{"a": 42}'))
Expand Down
16 changes: 14 additions & 2 deletions packages/fetch/src/addons/parse/adapters/valibot.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { AnySchema, BaseIssue, BaseSchema, BaseSchemaAsync, Config, InferOutput } from 'valibot'
import type { AnySchema, BaseIssue, BaseSchema, BaseSchemaAsync, Config, InferOutput, SafeParseResult } from 'valibot'

import type { FfetchParser, FfetchTypeProvider } from '../_types.js'
import { parse, parseAsync } from 'valibot'
import { parse, parseAsync, safeParse, safeParseAsync } from 'valibot'

export interface ValibotTypeProvider extends FfetchTypeProvider {
readonly parsed: this['schema'] extends (
BaseSchema<unknown, unknown, BaseIssue<unknown>> |
BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
) ? InferOutput<this['schema']> : never
readonly safeParsed: this['schema'] extends (
BaseSchema<unknown, unknown, BaseIssue<unknown>> |
BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
) ? SafeParseResult<this['schema']> : never
}

export function ffetchValibotAdapter(
Expand All @@ -21,9 +25,17 @@ export function ffetchValibotAdapter(
: function (schema: unknown, value: unknown): unknown {
return parse(schema as AnySchema, value, rest)
}
const safeParser = async
? async function (schema: unknown, value: unknown): Promise<unknown> {
return safeParseAsync(schema as AnySchema, value, rest)
}
: function (schema: unknown, value: unknown): unknown {
return safeParse(schema as AnySchema, value, rest)
}

return {
_provider,
parse: parser,
safeParse: safeParser,
}
}
14 changes: 14 additions & 0 deletions packages/fetch/src/addons/parse/adapters/valita.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ describe('valita', () => {
expect(res).toEqual('hello')
})

it('should safely parse a type (pass)', async () => {
const parser = ffetchValitaAdapter()
const res = await parser.safeParse(v.string(), 'hello')

expect(res).toMatchObject({ ok: true, value: 'hello' })
})

it('should safely parse a type (fail)', async () => {
const parser = ffetchValitaAdapter()
const res = await parser.safeParse(v.string(), 42)

expect(res).toMatchObject({ ok: false })
})

it('should work with ffetch (pass)', async () => {
const fetch_ = vi.fn<typeof fetch>()
.mockImplementation(async () => new Response('{"a": 42}'))
Expand Down
4 changes: 4 additions & 0 deletions packages/fetch/src/addons/parse/adapters/valita.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { FfetchParser, FfetchTypeProvider } from '../_types.js'

export interface ValitaTypeProvider extends FfetchTypeProvider {
readonly parsed: this['schema'] extends v.Type<any> ? v.Infer<this['schema']> : never
readonly safeParsed: this['schema'] extends v.Type<any> ? v.ValitaResult<v.Infer<this['schema']>> : never
}

type ParseOptions = NonNullable<Parameters<v.Type<any>['parse']>[1]>
Expand All @@ -16,5 +17,8 @@ export function ffetchValitaAdapter(options?: ParseOptions): FfetchParser<Valita
parse(schema: unknown, value: unknown): unknown {
return (schema as v.Type<any>).parse(value, options)
},
safeParse(schema: unknown, value: unknown): unknown {
return (schema as v.Type<any>).try(value, options)
},
}
}
14 changes: 14 additions & 0 deletions packages/fetch/src/addons/parse/adapters/yup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ describe('zod', () => {
expect(res).toEqual('hello')
})

it('should safely parse a type (pass)', async () => {
const parser = ffetchYupAdapter()
const res = await parser.safeParse(y.string(), 'hello')

expect(res).toMatchObject({ success: true, data: 'hello' })
})

it('should safely parse a type (fail)', async () => {
const parser = ffetchYupAdapter({ action: 'validate', options: { strict: true } })
const res = await parser.safeParse(y.string(), 42)

expect(res).toMatchObject({ success: false })
})

it('should work with ffetch (pass)', async () => {
const fetch_ = vi.fn<typeof fetch>()
.mockImplementation(async () => new Response('{"a": 42}'))
Expand Down
29 changes: 26 additions & 3 deletions packages/fetch/src/addons/parse/adapters/yup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { CastOptions, InferType, ISchema, ValidateOptions } from 'yup'
import type { CastOptions, InferType, ISchema, ValidateOptions, ValidationError } from 'yup'

import type { FfetchParser, FfetchTypeProvider } from '../_types.js'

type YupSafeParseResult<R> =
| { success: true, data: R }
| { success: false, error: ValidationError }

export interface YupTypeProvider extends FfetchTypeProvider {
readonly parsed: this['schema'] extends ISchema<any, any> ? InferType<this['schema']> : never
readonly safeParsed: this['schema'] extends ISchema<any, any> ? YupSafeParseResult<InferType<this['schema']>> : never
}

export type FfetchYupAdapterOptions =
Expand All @@ -16,15 +21,33 @@ export function ffetchYupAdapter({
}: FfetchYupAdapterOptions = { action: 'validate' }): FfetchParser<YupTypeProvider> {
const _provider = null as unknown as YupTypeProvider
const parse = action === 'cast'
? async function (schema: unknown, value: unknown): Promise<unknown> {
? function (schema: unknown, value: unknown): unknown {
return (schema as ISchema<any, any>).cast(value, options)
}
: function (schema: unknown, value: unknown): unknown {
: async function (schema: unknown, value: unknown): Promise<unknown> {
return (schema as ISchema<any, any>).validate(value, options)
}
const safeParse = action === 'cast'
? function (schema: unknown, value: unknown): unknown {
try {
const data: unknown = (schema as ISchema<any, any>).cast(value, options)
return { success: true, data }
} catch (e) {
return { success: false, error: e }
}
}
: async function (schema: unknown, value: unknown): Promise<unknown> {
try {
const data: unknown = await (schema as ISchema<any, any>).validate(value, options)
return { success: true, data }
} catch (e) {
return { success: false, error: e }
}
}

return {
_provider,
parse,
safeParse,
}
}
14 changes: 14 additions & 0 deletions packages/fetch/src/addons/parse/adapters/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ describe('zod', () => {
expect(res).toEqual('hello')
})

it('should safely parse a type (pass)', async () => {
const parser = ffetchZodAdapter()
const res = await parser.safeParse(z.string(), 'hello')

expect(res).toMatchObject({ success: true, data: 'hello' })
})

it('should safely parse a type (fail)', async () => {
const parser = ffetchZodAdapter()
const res = await parser.safeParse(z.string(), 42)

expect(res).toMatchObject({ success: false })
})

it('should work with ffetch (pass)', async () => {
const fetch_ = vi.fn<typeof fetch>()
.mockImplementation(async () => new Response('{"a": 42}'))
Expand Down
11 changes: 10 additions & 1 deletion packages/fetch/src/addons/parse/adapters/zod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ParseParams, z } from 'zod'
import type { ParseParams, SafeParseReturnType, z } from 'zod'

import type { FfetchParser, FfetchTypeProvider } from '../_types.js'

export interface ZodTypeProvider extends FfetchTypeProvider {
readonly parsed: this['schema'] extends z.ZodTypeAny ? z.infer<this['schema']> : never
readonly safeParsed: this['schema'] extends z.ZodTypeAny ? SafeParseReturnType<this['schema'], z.infer<this['schema']>> : never
}

export function ffetchZodAdapter({ async, ...rest }: Partial<ParseParams> = {}): FfetchParser<ZodTypeProvider> {
Expand All @@ -15,9 +16,17 @@ export function ffetchZodAdapter({ async, ...rest }: Partial<ParseParams> = {}):
: function (schema: unknown, value: unknown): unknown {
return (schema as z.ZodTypeAny).parse(value, rest)
}
const safeParse = async
? async function (schema: unknown, value: unknown): Promise<z.SafeParseReturnType<unknown, unknown>> {
return (schema as z.ZodTypeAny).safeParseAsync(value, rest)
}
: function (schema: unknown, value: unknown): z.SafeParseReturnType<unknown, unknown> {
return (schema as z.ZodTypeAny).safeParse(value, rest)
}

return {
_provider,
parse,
safeParse,
}
}
6 changes: 5 additions & 1 deletion packages/fetch/src/addons/parse/addon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FfetchResult } from '../../ffetch.js'
import type { FfetchAddon } from '../types.js'

import type { CallTypeProvider, FfetchParser, FfetchTypeProvider } from './_types.js'
import type { CallSafeTypeProvider, CallTypeProvider, FfetchParser, FfetchTypeProvider } from './_types.js'

export { FfetchParser, FfetchTypeProvider }

Expand All @@ -11,13 +11,17 @@ export function parser<TypeProvider extends FfetchTypeProvider>(
object,
{
parsedJson: <Schema>(schema: Schema) => Promise<CallTypeProvider<TypeProvider, Schema>>
safelyParsedJson: <Schema>(schema: Schema) => Promise<CallSafeTypeProvider<TypeProvider, Schema>>
}
> {
return {
response: {
async parsedJson(this: FfetchResult, schema: unknown) {
return parser.parse(schema, await this.json())
},
async safelyParsedJson(this: FfetchResult, schema: unknown) {
return parser.safeParse(schema, await this.json())
},
},
}
}
Loading