Skip to content
Merged
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
15 changes: 10 additions & 5 deletions examples/simple/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
};
};
};
};
}>;
Expand Down
27 changes: 16 additions & 11 deletions examples/spec/valibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() }) },
},
},
},
Expand Down
25 changes: 15 additions & 10 deletions examples/spec/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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() }) },
},
},
},
Expand Down
39 changes: 20 additions & 19 deletions src/common/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -62,16 +60,11 @@ export type ApiSpec<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Query extends Record<string, string> = Record<string, any>,
Body extends object = object,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ResBody extends Partial<Record<StatusCode, any>> = Partial<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Record<StatusCode, any>
>,
RequestHeaders extends Record<string, string> = Record<string, string>,
ResponseHeaders extends Record<string, string> = Record<string, string>,
> = BaseApiSpec<Params, Query, Body, ResBody, RequestHeaders, ResponseHeaders>;
Responses extends AnyApiResponses = AnyApiResponses,
> = BaseApiSpec<Params, Query, Body, RequestHeaders, Responses>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyApiSpec = BaseApiSpec<any, any, any, any, any, any>;
export type AnyApiSpec = BaseApiSpec<any, any, any, any, any>;

type JsonHeader = {
"Content-Type": "application/json";
Expand All @@ -82,7 +75,10 @@ type WithJsonHeader<H extends Record<string, string> | undefined> =

type AsJsonApiSpec<AS extends ApiSpec> = Omit<AS, "headers" | "resHeaders"> & {
headers: WithJsonHeader<AS["headers"]>;
resHeaders: WithJsonHeader<AS["resHeaders"]>;
// FIXME: いい感じにマージが必要
// response: {
// headers: WithJsonHeader<AS["response"]["headers"]>;
// };
};

export type ApiP<
Expand Down Expand Up @@ -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<Res = object> = Partial<Record<StatusCode, Res>>;
export type ApiClientResponses<AResponses extends ApiResponses> = {
> = AResponses[SC] extends AnyResponse ? AResponses[SC]["body"] : undefined;
export type AnyApiResponses = DefineApiResponses<AnyResponse>;
export type DefineApiResponses<Response extends AnyResponse> = Partial<
Record<StatusCode, Response>
>;
export type DefineResponse<Body, Headers> = { body: Body; headers?: Headers };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyResponse = DefineResponse<any, any>;
export type ApiClientResponses<AResponses extends AnyApiResponses> = {
[SC in keyof AResponses & StatusCode]: ClientResponse<
ApiRes<AResponses, SC>,
SC,
"json"
>;
};
export type MergeApiResponses<AR extends ApiResponses> =
export type MergeApiResponseBodies<AR extends AnyApiResponses> =
ApiClientResponses<AR>[keyof ApiClientResponses<AR>];

/**
Expand Down
19 changes: 13 additions & 6 deletions src/express/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -27,12 +34,12 @@ export type Handler<
> = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
req: Request<ParamsDictionary, any, any, ParsedQs, Locals>,
res: ExpressResponse<NonNullable<Spec>["resBody"], 200, Locals>,
res: ExpressResponse<NonNullable<Spec>["responses"], 200, Locals>,
next: NextFunction,
) => void;

export type ToHandler<
Spec extends ApiSpec | undefined,
Spec extends AnyApiSpec | undefined,
Validators extends AnyValidators | undefined,
> = Handler<
Spec,
Expand All @@ -41,7 +48,7 @@ export type ToHandler<
>
>;

export type ToHandlers<E extends ApiEndpoints, V extends ValidatorsMap> = {
export type ToHandlers<E extends AnyApiEndpoints, V extends ValidatorsMap> = {
[Path in keyof E & string]: {
[M in Method]: ToHandler<E[Path][M], V[Path][M]>;
};
Expand All @@ -51,7 +58,7 @@ export type ToHandlers<E extends ApiEndpoints, V extends ValidatorsMap> = {
* 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<string, any> = Record<string, any>,
Expand All @@ -69,7 +76,7 @@ export type ValidateLocals<Vs extends AnyValidators | Record<string, never>> = {
* 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<IRouter, Method> & {
Expand Down
60 changes: 37 additions & 23 deletions src/express/valibot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
},
},
},
},
Expand Down Expand Up @@ -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": {
Expand All @@ -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;
Expand Down Expand Up @@ -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() }) },
},
},
},
Expand Down
Loading