diff --git a/examples/simple/spec.ts b/examples/simple/spec.ts index 2d34494..9478f76 100644 --- a/examples/simple/spec.ts +++ b/examples/simple/spec.ts @@ -3,12 +3,17 @@ import { DefineApiEndpoints, FetchT } from "../../src"; export type PathMap = DefineApiEndpoints<{ "/users": { get: { - resBody: { - 200: { userNames: string[] }; - 400: { errorMessage: string }; - }; headers: { "Content-Type": "application/json" }; - resHeaders: { "Content-Type": "application/json" }; + responses: { + 200: { + body: { userNames: string[] }; + headers: { "Content-Type": "application/json" }; + }; + 400: { + body: { errorMessage: string }; + headers: { "Content-Type": "application/json" }; + }; + }; }; }; }>; diff --git a/examples/spec/valibot.ts b/examples/spec/valibot.ts index 7a45f33..2cd6069 100644 --- a/examples/spec/valibot.ts +++ b/examples/spec/valibot.ts @@ -10,29 +10,34 @@ export const pathMap = { query: v.object({ page: v.string(), }), - resBody: { - 200: v.object({ userNames: v.array(v.string()) }), - 400: v.object({ errorMessage: v.string() }), + responses: { + 200: { body: v.object({ userNames: v.array(v.string()) }) }, + 400: { body: v.object({ errorMessage: v.string() }) }, }, }, post: { headers: JsonHeader, - resHeaders: JsonHeader, - resBody: { - 200: v.object({ userId: v.string() }), - 400: v.object({ errorMessage: v.string() }), - }, body: v.object({ userName: v.string(), }), + responses: { + 200: { + headers: JsonHeader, + body: v.object({ userId: v.string() }), + }, + 400: { + headers: JsonHeader, + body: v.object({ errorMessage: v.string() }), + }, + }, }, }, "/users/:userId": { get: { params: v.object({ userId: v.string() }), - resBody: { - 200: v.object({ userName: v.string() }), - 400: v.object({ errorMessage: v.string() }), + responses: { + 200: { body: v.object({ userName: v.string() }) }, + 400: { body: v.object({ errorMessage: v.string() }) }, }, }, }, diff --git a/examples/spec/zod.ts b/examples/spec/zod.ts index 26f17ed..00a41fa 100644 --- a/examples/spec/zod.ts +++ b/examples/spec/zod.ts @@ -11,17 +11,22 @@ export const pathMap = { query: z.object({ page: z.string(), }), - resBody: { - 200: z.object({ userNames: z.string().array() }), - 400: z.object({ errorMessage: z.string() }), + responses: { + 200: { body: z.object({ userNames: z.string().array() }) }, + 400: { body: z.object({ errorMessage: z.string() }) }, }, }, post: { headers: JsonHeader, - resHeaders: JsonHeader, - resBody: { - 200: z.object({ userId: z.string() }), - 400: z.object({ errorMessage: z.string() }), + responses: { + 200: { + headers: JsonHeader, + body: z.object({ userId: z.string() }), + }, + 400: { + headers: JsonHeader, + body: z.object({ errorMessage: z.string() }), + }, }, body: z.object({ userName: z.string(), @@ -31,9 +36,9 @@ export const pathMap = { "/users/:userId": { get: { params: z.object({ userId: z.string() }), - resBody: { - 200: z.object({ userName: z.string() }), - 400: z.object({ errorMessage: z.string() }), + responses: { + 200: { body: z.object({ userName: z.string() }) }, + 400: { body: z.object({ errorMessage: z.string() }) }, }, }, }, diff --git a/src/common/spec.ts b/src/common/spec.ts index 53d1a49..c35f5ba 100644 --- a/src/common/spec.ts +++ b/src/common/spec.ts @@ -42,16 +42,14 @@ export interface BaseApiSpec< Query, Body, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ResBody, RequestHeaders, - ResponseHeaders, + Responses extends AnyApiResponses, > { query?: Query; params?: Params; body?: Body; - resBody: ResBody; + responses: Responses; headers?: RequestHeaders; - resHeaders?: ResponseHeaders; } export type ApiSpec< ParamKeys extends string = string, @@ -62,16 +60,11 @@ export type ApiSpec< // eslint-disable-next-line @typescript-eslint/no-explicit-any Query extends Record = Record, Body extends object = object, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ResBody extends Partial> = Partial< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Record - >, RequestHeaders extends Record = Record, - ResponseHeaders extends Record = Record, -> = BaseApiSpec; + Responses extends AnyApiResponses = AnyApiResponses, +> = BaseApiSpec; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyApiSpec = BaseApiSpec; +export type AnyApiSpec = BaseApiSpec; type JsonHeader = { "Content-Type": "application/json"; @@ -82,7 +75,10 @@ type WithJsonHeader | undefined> = type AsJsonApiSpec = Omit & { headers: WithJsonHeader; - resHeaders: WithJsonHeader; + // FIXME: いい感じにマージが必要 + // response: { + // headers: WithJsonHeader; + // }; }; export type ApiP< @@ -115,19 +111,24 @@ export type ApiHasP< : never; export type ApiRes< - AResponses extends ApiResponses, + AResponses extends AnyApiResponses, SC extends keyof AResponses & StatusCode, - Res = object, -> = AResponses[SC] extends Res ? AResponses[SC] : never; -export type ApiResponses = Partial>; -export type ApiClientResponses = { +> = AResponses[SC] extends AnyResponse ? AResponses[SC]["body"] : undefined; +export type AnyApiResponses = DefineApiResponses; +export type DefineApiResponses = Partial< + Record +>; +export type DefineResponse = { body: Body; headers?: Headers }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyResponse = DefineResponse; +export type ApiClientResponses = { [SC in keyof AResponses & StatusCode]: ClientResponse< ApiRes, SC, "json" >; }; -export type MergeApiResponses = +export type MergeApiResponseBodies = ApiClientResponses[keyof ApiClientResponses]; /** diff --git a/src/express/index.ts b/src/express/index.ts index 3562af5..8bc4fab 100644 --- a/src/express/index.ts +++ b/src/express/index.ts @@ -1,5 +1,12 @@ import { IRouter, RequestHandler } from "express"; -import { Method, ApiResponses, ApiRes, ApiSpec, ApiEndpoints } from "../index"; +import { + Method, + AnyApiResponses, + ApiRes, + ApiSpec, + AnyApiSpec, + AnyApiEndpoints, +} from "../index"; import { NextFunction, ParamsDictionary, @@ -27,12 +34,12 @@ export type Handler< > = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any req: Request, - res: ExpressResponse["resBody"], 200, Locals>, + res: ExpressResponse["responses"], 200, Locals>, next: NextFunction, ) => void; export type ToHandler< - Spec extends ApiSpec | undefined, + Spec extends AnyApiSpec | undefined, Validators extends AnyValidators | undefined, > = Handler< Spec, @@ -41,7 +48,7 @@ export type ToHandler< > >; -export type ToHandlers = { +export type ToHandlers = { [Path in keyof E & string]: { [M in Method]: ToHandler; }; @@ -51,7 +58,7 @@ export type ToHandlers = { * Express Response, but with more strict type information. */ export type ExpressResponse< - Responses extends ApiResponses, + Responses extends AnyApiResponses, SC extends keyof Responses & StatusCode, // eslint-disable-next-line @typescript-eslint/no-explicit-any LocalsObj extends Record = Record, @@ -69,7 +76,7 @@ export type ValidateLocals> = { * Express Router, but with more strict type information. */ export type RouterT< - E extends ApiEndpoints, + E extends AnyApiEndpoints, V extends ValidatorsMap, SC extends StatusCode = StatusCode, > = Omit & { diff --git a/src/express/valibot.test.ts b/src/express/valibot.test.ts index b63acec..c4a2f65 100644 --- a/src/express/valibot.test.ts +++ b/src/express/valibot.test.ts @@ -42,14 +42,18 @@ describe("valibot", () => { params: v.object({ name: v.string(), }), - resBody: { - 200: v.object({ - id: v.string(), - name: v.string(), - }), - 400: v.object({ - message: v.string(), - }), + responses: { + 200: { + body: v.object({ + id: v.string(), + name: v.string(), + }), + }, + 400: { + body: v.object({ + message: v.string(), + }), + }, }, }, }, @@ -293,11 +297,14 @@ describe("valibot", () => { const pathMap = { "/users": { get: { - resBody: { 200: v.array(User) }, + responses: { 200: { body: v.array(User) } }, }, post: { body: UserName, - resBody: { 200: User, ...BadRequest }, + responses: { + 200: { body: User, ...BadRequest }, + 400: { body: Err }, + }, }, }, "/users/:id": { @@ -309,7 +316,10 @@ describe("valibot", () => { query: v.object({ detail: v.union([v.literal("true"), v.literal("false")]), }), - resBody: { 200: User, ...BadRequest }, + responses: { + 200: { body: User, ...BadRequest }, + 400: { body: Err }, + }, }, }, } satisfies ValibotApiEndpoints; @@ -417,22 +427,26 @@ describe("valibot", () => { get: { params: v.object({ active: v.string() }), query: v.object({ name: v.string() }), - resBody: { - 200: v.array( - v.object({ - id: v.string(), - name: v.string(), - active: v.string(), - }), - ), - 400: v.object({ message: v.string() }), + responses: { + 200: { + body: v.array( + v.object({ + id: v.string(), + name: v.string(), + active: v.string(), + }), + ), + }, + 400: { body: v.object({ message: v.string() }) }, }, }, post: { body: v.object({ name: v.string() }), - resBody: { - 200: v.array(v.object({ id: v.string(), name: v.string() })), - 400: v.object({ message: v.string() }), + responses: { + 200: { + body: v.array(v.object({ id: v.string(), name: v.string() })), + }, + 400: { body: v.object({ message: v.string() }) }, }, }, }, diff --git a/src/express/zod.test.ts b/src/express/zod.test.ts index 16943f7..ea07c27 100644 --- a/src/express/zod.test.ts +++ b/src/express/zod.test.ts @@ -35,14 +35,18 @@ describe("validatorMiddleware", () => { params: z.object({ name: z.string(), }), - resBody: { - 200: z.object({ - id: z.string(), - name: z.string(), - }), - 400: z.object({ - message: z.string(), - }), + responses: { + 200: { + body: z.object({ + id: z.string(), + name: z.string(), + }), + }, + 400: { + body: z.object({ + message: z.string(), + }), + }, }, }, }, @@ -237,15 +241,15 @@ describe("typed", () => { const UserName = z.object({ name: z.string() }); const User = UserName.merge(UserId); const Err = z.object({ message: z.string() }); - const BadRequest = { 400: Err }; + const BadRequest = { 400: { body: Err } }; const pathMap = { "/users": { get: { - resBody: { 200: z.array(User) }, + responses: { 200: { body: z.array(User) } }, }, post: { body: UserName, - resBody: { 200: User, ...BadRequest }, + responses: { 200: { body: User }, ...BadRequest }, }, }, "/users/:id": { @@ -257,7 +261,10 @@ describe("typed", () => { query: z.object({ detail: z.union([z.literal("true"), z.literal("false")]), }), - resBody: { 200: User, ...BadRequest }, + responses: { + 200: { body: User }, + ...BadRequest, + }, }, }, } satisfies ZodApiEndpoints; @@ -365,22 +372,26 @@ describe("Handler", () => { get: { params: z.object({ active: z.string() }), query: z.object({ name: z.string() }), - resBody: { - 200: z.array( - z.object({ - id: z.string(), - name: z.string(), - active: z.string(), - }), - ), - 400: z.object({ message: z.string() }), + responses: { + 200: { + body: z.array( + z.object({ + id: z.string(), + name: z.string(), + active: z.string(), + }), + ), + }, + 400: { body: z.object({ message: z.string() }) }, }, }, post: { body: z.object({ name: z.string() }), - resBody: { - 200: z.array(z.object({ id: z.string(), name: z.string() })), - 400: z.object({ message: z.string() }), + responses: { + 200: { + body: z.array(z.object({ id: z.string(), name: z.string() })), + }, + 400: { body: z.object({ message: z.string() }) }, }, }, }, diff --git a/src/fastify/zod.ts b/src/fastify/zod.ts index e326e45..5c5b6ef 100644 --- a/src/fastify/zod.ts +++ b/src/fastify/zod.ts @@ -1,12 +1,27 @@ import { ZodApiEndpoint, ZodApiEndpoints, ZodApiSpec } from "../zod"; -import { Method } from "../common"; +import { AnyApiResponses, Method, StatusCode } from "../common"; + +const toFastifyResponse = ( + responses: Responses, +): ToFastifyResponse => { + const ret = { ...responses }; + Object.keys(responses).forEach((key) => { + const sc = key as keyof Responses as StatusCode; + ret[sc] = responses[sc]!.body; + }); + return ret as ToFastifyResponse; +}; + +type ToFastifyResponse = { + [SC in keyof Responses & StatusCode]: NonNullable["body"]; +}; type FastifySchema = { querystring: Spec["query"]; params: Spec["params"]; body: Spec["body"]; headers: Spec["headers"]; - response: Spec["resBody"]; + response: ToFastifyResponse; }; export const toSchema = ( spec: Spec, @@ -16,7 +31,7 @@ export const toSchema = ( params: spec.params, body: spec.body, headers: spec.headers, - response: spec.resBody, + response: toFastifyResponse(spec.responses), }; }; diff --git a/src/fetch/index.t-test.ts b/src/fetch/index.t-test.ts index d05c046..890d899 100644 --- a/src/fetch/index.t-test.ts +++ b/src/fetch/index.t-test.ts @@ -7,9 +7,7 @@ const JSONT = JSON as JSONT; type Spec = DefineApiEndpoints<{ "/users": { get: { - resBody: { - 200: { prop: string }; - }; + responses: { 200: { body: { prop: string } } }; }; }; }>; @@ -27,17 +25,15 @@ const JSONT = JSON as JSONT; type Spec = DefineApiEndpoints<{ "/users": { get: { - resBody: { - 200: { prop: string }; - }; + responses: { 200: { body: { prop: string } } }; }; post: { body: { userName: string; }; - resBody: { - 200: { postProp: string }; - 400: { error: string }; + responses: { + 200: { body: { postProp: string } }; + 400: { body: { error: string } }; }; }; }; @@ -125,9 +121,7 @@ const JSONT = JSON as JSONT; "/users": { get: { headers: { Cookie: `a=${string}` }; - resBody: { - 200: { prop: string }; - }; + responses: { 200: { body: { prop: string } } }; }; }; }>; @@ -154,9 +148,7 @@ const JSONT = JSON as JSONT; "/packages/list": { get: { headers: { Cookie: `a=${string}` }; - resBody: { - 200: { prop: string }; - }; + responses: { 200: { body: { prop: string } } }; query: { state: boolean; }; @@ -190,17 +182,13 @@ const JSONT = JSON as JSONT; type Spec = DefineApiEndpoints<{ "/vectorize/indexes/:indexName": { post: { - resBody: { - 200: { prop2: string }; - }; + responses: { 200: { body: { prop2: string } } }; }; }; "/vectorize/indexes/:indexName/get-by-ids": { post: { body: { ids: string[] }; - resBody: { - 200: { prop: string }; - }; + responses: { 200: { body: { prop: string } } }; }; }; }>; diff --git a/src/fetch/index.ts b/src/fetch/index.ts index 31fe745..bcc3294 100644 --- a/src/fetch/index.ts +++ b/src/fetch/index.ts @@ -2,11 +2,11 @@ import { ApiEndpoints, ApiHasP, ApiP, - ApiResponses, + AnyApiResponses, CaseInsensitiveMethod, FilterNever, MatchedPatterns, - MergeApiResponses, + MergeApiResponseBodies, Method, NormalizePath, ParseURL, @@ -53,12 +53,21 @@ type FetchT = < M, "query" >, - ResBody extends ApiP = ApiP< + ResBody extends ApiP< E, CandidatePaths, M, - "resBody" - >, + "responses" + > extends AnyApiResponses + ? MergeApiResponseBodies> + : Record = ApiP< + E, + CandidatePaths, + M, + "responses" + > extends AnyApiResponses + ? MergeApiResponseBodies> + : Record, >( input: Input, init: ApiHasP extends true @@ -74,10 +83,6 @@ type FetchT = < ApiP > | undefined, -) => Promise< - MergeApiResponses< - ResBody extends ApiResponses ? ResBody : Record - > ->; +) => Promise; export default FetchT; diff --git a/src/valibot/index.ts b/src/valibot/index.ts index cdab830..d5c4df6 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -1,5 +1,12 @@ import * as v from "valibot"; -import { BaseApiSpec, Method, StatusCode } from "../common"; +import { + AnyApiResponses, + BaseApiSpec, + DefineApiResponses, + DefineResponse, + Method, + StatusCode, +} from "../common"; import { getApiSpec, Validator, @@ -42,12 +49,9 @@ export type ValibotApiSpec< Params extends AnyV = AnyV, Query extends AnyV = AnyV, Body extends AnyV = AnyV, - ResBody extends Partial> = Partial< - Record - >, RequestHeaders extends AnyV = AnyV, - ResponseHeaders extends AnyV = AnyV, -> = BaseApiSpec; + Responses extends AnyApiResponses = AnyApiResponses, +> = BaseApiSpec; export type ToApiEndpoints = { [Path in keyof E & string]: ToApiEndpoint; }; @@ -55,26 +59,28 @@ export type ToApiEndpoint< E extends ValibotApiEndpoints, Path extends keyof E, > = { - [M in keyof E[Path] & Method]: E[Path][M] extends undefined - ? undefined - : ToApiSpec>; + [M in keyof E[Path] & Method]: ToApiSpec>; }; export type ToApiSpec = { query: InferOrUndefined; params: InferOrUndefined; body: InferOrUndefined; - resBody: ToApiResponses; headers: InferOrUndefined; - resHeaders: InferOrUndefined; + responses: ToApiResponses; +}; +export type ToApiResponses = { + [SC in keyof AR & StatusCode]: { + body: InferOrUndefined["body"]>; + headers: InferOrUndefined["headers"]>; + }; }; +type ValibotAnyApiResponse = DefineResponse; +export type ValibotAnyApiResponses = DefineApiResponses; export type ValibotApiResponses = Partial>; export type ValibotApiResSchema< AResponses extends ValibotApiResponses, SC extends keyof AResponses & StatusCode, > = AResponses[SC] extends AnyV ? AResponses[SC] : never; -export type ToApiResponses = { - [SC in keyof AR & StatusCode]: v.InferOutput>; -}; /** * Create a new validator for the given endpoints. diff --git a/src/zod/index.ts b/src/zod/index.ts index 44f2f1f..c70b27e 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -1,5 +1,11 @@ import { SafeParseReturnType, z, ZodError, ZodType } from "zod"; -import { BaseApiSpec, Method, StatusCode } from "../common"; +import { + BaseApiSpec, + DefineApiResponses, + DefineResponse, + Method, + StatusCode, +} from "../common"; import { getApiSpec, Validator, @@ -47,35 +53,31 @@ export type ZodApiSpec< >, Query extends z.ZodTypeAny = z.ZodTypeAny, Body extends z.ZodTypeAny = z.ZodTypeAny, - ResBody extends ZodApiResponses = Partial>, RequestHeaders extends z.ZodTypeAny = z.ZodTypeAny, - ResponseHeaders extends z.ZodTypeAny = z.ZodTypeAny, -> = BaseApiSpec; -export type ZodApiResponses = Partial>; -export type ZodApiResSchema< - AResponses extends ZodApiResponses, - SC extends keyof AResponses & StatusCode, -> = AResponses[SC] extends z.ZodTypeAny ? AResponses[SC] : never; + Responses extends ZodAnyApiResponses = ZodAnyApiResponses, +> = BaseApiSpec; +type ZodAnyApiResponse = DefineResponse; +export type ZodAnyApiResponses = DefineApiResponses; // -- converter -- export type ToApiEndpoints = { [Path in keyof E & string]: ToApiEndpoint; }; export type ToApiEndpoint = { - [M in keyof E[Path] & Method]: E[Path][M] extends undefined - ? undefined - : ToApiSpec>; + [M in keyof E[Path] & Method]: ToApiSpec>; }; export type ToApiSpec = { query: InferOrUndefined; params: InferOrUndefined; body: InferOrUndefined; - resBody: ToApiResponses; headers: InferOrUndefined; - resHeaders: InferOrUndefined; + responses: ToApiResponses; }; -export type ToApiResponses = { - [SC in keyof AR & StatusCode]: z.infer>; +export type ToApiResponses = { + [SC in keyof AR & StatusCode]: { + body: InferOrUndefined["body"]>; + headers: InferOrUndefined["headers"]>; + }; }; /**