rouzer 3.0.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ import type { Promisable } from './common.js';
2
+ import type { RouteRequest } from './types/request.js';
3
+ /** Runtime key carried by response plugin markers. */
4
+ export declare const responsePluginMarker: unique symbol;
5
+ /**
6
+ * Compile-time response marker handled by a client/router response plugin pair.
7
+ *
8
+ * @remarks `TClient` is the value returned by generated client action
9
+ * functions. `TRouter` is the non-`Response` value accepted from route handlers.
10
+ * Plugin markers may be used directly as an action response or as success
11
+ * entries in a status-keyed response map.
12
+ */
13
+ export type ResponsePluginMarker<TClient, TRouter = TClient, TId extends string = string> = Record<number, unknown> & {
14
+ readonly [responsePluginMarker]: {
15
+ readonly id: TId;
16
+ readonly client: TClient;
17
+ readonly router: TRouter;
18
+ };
19
+ };
20
+ /** Client-side response plugin used by `createClient({ plugins })`. */
21
+ export type ClientResponsePlugin = {
22
+ /** Stable response codec id matched against route response markers. */
23
+ readonly id: string;
24
+ /** Decode a successful `Response` into the client action result. */
25
+ decode(response: Response, context: {
26
+ marker: ResponsePluginMarker<any, any>;
27
+ request: RouteRequest;
28
+ }): Promisable<unknown>;
29
+ };
30
+ /** Router-side response plugin used by `createRouter({ plugins })`. */
31
+ export type RouterResponsePlugin = {
32
+ /** Stable response codec id matched against route response markers. */
33
+ readonly id: string;
34
+ /** Encode a handler result into the HTTP response. */
35
+ encode(value: unknown, context: {
36
+ marker: ResponsePluginMarker<any, any>;
37
+ request: Request;
38
+ }): Promisable<Response>;
39
+ };
40
+ /** Create a response marker for a response plugin. */
41
+ export declare function createResponsePluginMarker<TClient, TRouter = TClient, const TId extends string = string>(id: TId): ResponsePluginMarker<TClient, TRouter, TId>;
42
+ /** Get the response plugin id from a plugin marker, if present. */
43
+ export declare function getResponsePluginMarkerId(value: unknown): string | undefined;
44
+ /** Return true when a route response marker is handled by a response plugin. */
45
+ export declare function isResponsePluginMarker(value: unknown): value is ResponsePluginMarker<unknown, unknown>;
46
+ /** Create a plugin lookup map and reject duplicate plugin ids. */
47
+ export declare function createResponsePluginMap<TPlugin extends {
48
+ readonly id: string;
49
+ }>(plugins?: readonly TPlugin[], label?: string): Map<string, TPlugin>;
@@ -0,0 +1,33 @@
1
+ /** Runtime key carried by response plugin markers. */
2
+ export const responsePluginMarker = Symbol.for('rouzer.response-plugin');
3
+ /** Create a response marker for a response plugin. */
4
+ export function createResponsePluginMarker(id) {
5
+ return {
6
+ [responsePluginMarker]: {
7
+ id,
8
+ client: undefined,
9
+ router: undefined,
10
+ },
11
+ };
12
+ }
13
+ /** Get the response plugin id from a plugin marker, if present. */
14
+ export function getResponsePluginMarkerId(value) {
15
+ return isResponsePluginMarker(value)
16
+ ? value[responsePluginMarker].id
17
+ : undefined;
18
+ }
19
+ /** Return true when a route response marker is handled by a response plugin. */
20
+ export function isResponsePluginMarker(value) {
21
+ return (typeof value === 'object' && value !== null && responsePluginMarker in value);
22
+ }
23
+ /** Create a plugin lookup map and reject duplicate plugin ids. */
24
+ export function createResponsePluginMap(plugins = [], label = 'response') {
25
+ const map = new Map();
26
+ for (const plugin of plugins) {
27
+ if (map.has(plugin.id)) {
28
+ throw new Error(`Duplicate ${label} plugin: ${plugin.id}`);
29
+ }
30
+ map.set(plugin.id, plugin);
31
+ }
32
+ return map;
33
+ }
@@ -1,6 +1,7 @@
1
1
  import type { HattipHandler } from '@hattip/core';
2
2
  import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes } from 'alien-middleware';
3
3
  import type { HttpRouteTree } from '../http.js';
4
+ import { type RouterResponsePlugin } from '../response.js';
4
5
  import type { RouteRequestHandlerMap } from '../types/server.js';
5
6
  export { chain };
6
7
  /** Configuration for `createRouter`. */
@@ -25,6 +26,8 @@ export type RouterConfig = {
25
26
  * and logs missing route handlers to the console.
26
27
  */
27
28
  debug?: boolean;
29
+ /** Response codec plugins used for route handler results. */
30
+ plugins?: readonly RouterResponsePlugin[];
28
31
  /** CORS configuration for requests with an `Origin` header. */
29
32
  cors?: {
30
33
  /**
@@ -67,8 +70,8 @@ export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T
67
70
  /**
68
71
  * Create a Rouzer router that can be mounted by any Hattip adapter.
69
72
  *
70
- * @param config Optional router configuration for base path, debug behavior, and
71
- * CORS origin restrictions.
73
+ * @param config Optional router configuration for base path, debug behavior,
74
+ * response plugins, and CORS origin restrictions.
72
75
  * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
73
76
  * and route registration.
74
77
  */
@@ -3,15 +3,19 @@ import { createMatcher } from '@remix-run/route-pattern/match';
3
3
  import { chain, MiddlewareChain, } from 'alien-middleware';
4
4
  import * as z from 'zod';
5
5
  import { mapValues } from '../common.js';
6
+ import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
7
+ import { getDefaultSuccessStatus, getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
6
8
  export { chain };
7
9
  // Internal prototype for the router instance.
8
10
  class RouterObject extends MiddlewareChain {
9
11
  config;
10
12
  basePath;
13
+ responsePlugins;
11
14
  constructor(config) {
12
15
  super();
13
16
  this.config = config;
14
17
  this.basePath = config.basePath?.replace(/\/?$/, '/');
18
+ this.responsePlugins = createResponsePluginMap(config.plugins, 'router response');
15
19
  const allowOrigins = config.cors?.allowOrigins?.map(createOriginPattern);
16
20
  if (allowOrigins) {
17
21
  super.use(((ctx) => {
@@ -31,14 +35,15 @@ class RouterObject extends MiddlewareChain {
31
35
  }
32
36
  /** @internal */
33
37
  useRoutes(routeSchemas, handlers) {
34
- const { config, basePath } = this;
38
+ const { config, basePath, responsePlugins } = this;
35
39
  const routes = flattenRoutes(routeSchemas, handlers, basePath ?? '', config.debug);
40
+ validateRouterResponsePlugins(routes, responsePlugins);
36
41
  const addDebugHeaders = config.debug
37
42
  ? (context, route) => {
38
43
  context.setHeader('X-Route-Name', route.name);
39
44
  }
40
45
  : null;
41
- return super.use((async function (context) {
46
+ return super.use(async function (context) {
42
47
  const request = context.request;
43
48
  const origin = request.headers.get('Origin');
44
49
  const url = (context.url ??= new URL(request.url));
@@ -105,21 +110,41 @@ class RouterObject extends MiddlewareChain {
105
110
  return httpClientError(error, 'Invalid request body', config);
106
111
  }
107
112
  }
113
+ if (isResponseMap(schema.response)) {
114
+ ;
115
+ context.error = createResponseHelper(schema.response, request, responsePlugins, true);
116
+ context.success = createResponseHelper(schema.response, request, responsePlugins, false);
117
+ }
108
118
  const result = await handler(context);
109
119
  addDebugHeaders?.(context, route);
110
120
  if (result instanceof Response) {
111
121
  return result;
112
122
  }
123
+ const pluginId = getResponsePluginMarkerId(schema.response);
124
+ if (pluginId) {
125
+ const plugin = responsePlugins.get(pluginId);
126
+ if (!plugin) {
127
+ throw missingRouterResponsePlugin(pluginId);
128
+ }
129
+ return plugin.encode(result, {
130
+ marker: schema.response,
131
+ request,
132
+ });
133
+ }
134
+ if (isResponseMap(schema.response)) {
135
+ const status = getDefaultSuccessStatus(schema.response);
136
+ return encodeResponseMapResult(schema.response, status, result, request, responsePlugins);
137
+ }
113
138
  return Response.json(result);
114
139
  }
115
- }));
140
+ });
116
141
  }
117
142
  }
118
143
  /**
119
144
  * Create a Rouzer router that can be mounted by any Hattip adapter.
120
145
  *
121
- * @param config Optional router configuration for base path, debug behavior, and
122
- * CORS origin restrictions.
146
+ * @param config Optional router configuration for base path, debug behavior,
147
+ * response plugins, and CORS origin restrictions.
123
148
  * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
124
149
  * and route registration.
125
150
  */
@@ -152,6 +177,21 @@ function flattenRoutes(tree, handlers, prefix, debug) {
152
177
  }
153
178
  return routes;
154
179
  }
180
+ function validateRouterResponsePlugins(routes, plugins) {
181
+ for (const route of routes) {
182
+ const pluginIds = isResponseMap(route.schema.response)
183
+ ? getResponseMapPluginIds(route.schema.response)
184
+ : [getResponsePluginMarkerId(route.schema.response)].filter(pluginId => pluginId !== undefined);
185
+ for (const pluginId of pluginIds) {
186
+ if (!plugins.has(pluginId)) {
187
+ throw missingRouterResponsePlugin(pluginId);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ function missingRouterResponsePlugin(pluginId) {
193
+ return new Error(`Missing router response plugin for ${pluginId}`);
194
+ }
155
195
  function joinPaths(left, right) {
156
196
  return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
157
197
  }
@@ -252,3 +292,39 @@ function createOriginPattern(origin) {
252
292
  }
253
293
  return new ExactPattern(origin);
254
294
  }
295
+ /** Create `ctx.error(status, body)` or `ctx.success(status, body)`. */
296
+ function createResponseHelper(responseMap, request, responsePlugins, error) {
297
+ return (status, body) => {
298
+ const marker = responseMap[status];
299
+ if (!marker || isErrorMarker(marker) !== error) {
300
+ throw new Error(`Undeclared ${error ? 'error' : 'success'} response status: ${status}`);
301
+ }
302
+ return encodeResponseMapResult(responseMap, status, body, request, responsePlugins);
303
+ };
304
+ }
305
+ async function encodeResponseMapResult(responseMap, status, value, request, responsePlugins) {
306
+ const marker = responseMap[status];
307
+ if (!marker) {
308
+ throw new Error(`Undeclared response status: ${status}`);
309
+ }
310
+ if (isErrorMarker(marker)) {
311
+ return Response.json(value, { status });
312
+ }
313
+ const pluginId = getResponsePluginMarkerId(marker);
314
+ if (!pluginId) {
315
+ return Response.json(value, { status });
316
+ }
317
+ const plugin = responsePlugins.get(pluginId);
318
+ if (!plugin) {
319
+ throw missingRouterResponsePlugin(pluginId);
320
+ }
321
+ const response = await plugin.encode(value, {
322
+ marker: marker,
323
+ request,
324
+ });
325
+ return new Response(response.body, {
326
+ status,
327
+ statusText: response.statusText,
328
+ headers: response.headers,
329
+ });
330
+ }
package/dist/type.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { Unchecked } from './common.js';
1
+ import type { Unchecked, UncheckedError } from './common.js';
2
2
  /**
3
3
  * Create a compile-time-only marker for an action's JSON response payload type.
4
4
  *
5
- * @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
6
- * server handler return values and client action functions for HTTP actions
7
- * whose responses are expected to be JSON.
5
+ * @remarks `$type<T>()` does not validate handler return values at the server
6
+ * boundary. It lets Rouzer type server handler return values and client action
7
+ * functions for HTTP actions whose responses are expected to be JSON. Use it
8
+ * directly as `response` for one JSON success shape, or as a success entry in a
9
+ * status-keyed response map. Validate response data where it enters your server
10
+ * or client code when runtime integrity is required.
8
11
  *
9
12
  * @example
10
13
  * ```ts
@@ -20,3 +23,29 @@ export declare function $type<T>(): Unchecked<T>;
20
23
  export declare namespace $type {
21
24
  var symbol: symbol;
22
25
  }
26
+ /**
27
+ * Create a compile-time-only marker for a declared error response type.
28
+ *
29
+ * @remarks `$error<T>()` marks a non-success response branch in a status-keyed
30
+ * response map. It is a type contract, not a runtime validator. On the server,
31
+ * handlers use `ctx.error(status, body)` to return declared errors. On the
32
+ * client, declared error responses resolve as `[error, null, status]` tuple
33
+ * entries instead of rejecting the promise.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { $type, $error } from 'rouzer'
38
+ * import * as http from 'rouzer/http'
39
+ *
40
+ * const getUser = http.get('users/:id', {
41
+ * response: {
42
+ * 200: $type<User>(),
43
+ * 404: $error<{ code: string; message: string }>(),
44
+ * },
45
+ * })
46
+ * ```
47
+ */
48
+ export declare function $error<T>(): UncheckedError<T>;
49
+ export declare namespace $error {
50
+ var symbol: symbol;
51
+ }
package/dist/type.js CHANGED
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Create a compile-time-only marker for an action's JSON response payload type.
3
3
  *
4
- * @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
5
- * server handler return values and client action functions for HTTP actions
6
- * whose responses are expected to be JSON.
4
+ * @remarks `$type<T>()` does not validate handler return values at the server
5
+ * boundary. It lets Rouzer type server handler return values and client action
6
+ * functions for HTTP actions whose responses are expected to be JSON. Use it
7
+ * directly as `response` for one JSON success shape, or as a success entry in a
8
+ * status-keyed response map. Validate response data where it enters your server
9
+ * or client code when runtime integrity is required.
7
10
  *
8
11
  * @example
9
12
  * ```ts
@@ -19,3 +22,29 @@ export function $type() {
19
22
  return $type.symbol;
20
23
  }
21
24
  $type.symbol = Symbol();
25
+ /**
26
+ * Create a compile-time-only marker for a declared error response type.
27
+ *
28
+ * @remarks `$error<T>()` marks a non-success response branch in a status-keyed
29
+ * response map. It is a type contract, not a runtime validator. On the server,
30
+ * handlers use `ctx.error(status, body)` to return declared errors. On the
31
+ * client, declared error responses resolve as `[error, null, status]` tuple
32
+ * entries instead of rejecting the promise.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { $type, $error } from 'rouzer'
37
+ * import * as http from 'rouzer/http'
38
+ *
39
+ * const getUser = http.get('users/:id', {
40
+ * response: {
41
+ * 200: $type<User>(),
42
+ * 404: $error<{ code: string; message: string }>(),
43
+ * },
44
+ * })
45
+ * ```
46
+ */
47
+ export function $error() {
48
+ return $error.symbol;
49
+ }
50
+ $error.symbol = Symbol();
@@ -3,11 +3,41 @@ import type { AnyMiddlewareChain, MiddlewareContext } from 'alien-middleware';
3
3
  import type * as z from 'zod';
4
4
  import { Promisable } from '../common.js';
5
5
  import type { HttpAction } from '../http.js';
6
- import type { InferRouteResponse } from './response.js';
7
- import type { RouteSchema } from './schema.js';
6
+ import type { InferRouteHandlerResult, InferResponseMapErrors, InferResponseMapSuccesses } from './response.js';
7
+ import type { RouteResponseMap, RouteSchema } from './schema.js';
8
8
  type RequestContext<TMiddleware extends AnyMiddlewareChain> = MiddlewareContext<TMiddleware>;
9
- export type RouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TArgs extends object, TResult> = (context: RequestContext<TMiddleware> & TArgs) => Promisable<TResult | Response>;
10
- export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction extends HttpAction, TPath extends string> = TAction['method'] extends 'GET' ? RouteRequestHandler<TMiddleware, {
9
+ /**
10
+ * Error response returned by `ctx.error(status, body)` in route handlers.
11
+ *
12
+ * @remarks This is an opaque branded type returned by the error helper. Route
13
+ * handlers may return it to signal a declared error response.
14
+ */
15
+ export type RouteErrorResponse = Response & {
16
+ __routeError__: true;
17
+ };
18
+ /** Response returned by `ctx.success(status, body)` in route handlers. */
19
+ export type RouteSuccessResponse = Response & {
20
+ __routeSuccess__: true;
21
+ };
22
+ export type RouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TArgs extends object, TResult, TErrors = never, TSuccesses = never> = (context: RequestContext<TMiddleware> & TArgs & ([TErrors] extends [never] ? {} : {
23
+ /**
24
+ * Return a declared error response.
25
+ *
26
+ * @remarks Only statuses declared with `$error<T>()` in the response
27
+ * map are accepted.
28
+ */
29
+ error: <TEntry extends TErrors>(...args: TEntry extends [infer S extends number, infer B] ? [status: S, body: B] : never) => RouteErrorResponse;
30
+ }) & ([TSuccesses] extends [never] ? {} : {
31
+ /**
32
+ * Return a declared success response with an explicit status.
33
+ *
34
+ * @remarks Useful when a response map declares multiple 2xx statuses.
35
+ */
36
+ success: <TEntry extends TSuccesses>(...args: TEntry extends [infer S extends number, infer B] ? [status: S, body: B] : never) => RouteSuccessResponse;
37
+ })) => Promisable<TResult | Response>;
38
+ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction extends HttpAction, TPath extends string> = TAction['schema'] extends {
39
+ response: infer R extends RouteResponseMap;
40
+ } ? TAction['method'] extends 'GET' ? RouteRequestHandler<TMiddleware, {
11
41
  path: TAction['schema'] extends {
12
42
  path: any;
13
43
  } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
@@ -17,7 +47,7 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
17
47
  headers: TAction['schema'] extends {
18
48
  headers: any;
19
49
  } ? z.infer<TAction['schema']['headers']> : undefined;
20
- }, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>> : RouteRequestHandler<TMiddleware, {
50
+ }, InferRouteHandlerResult<Extract<TAction['schema'], RouteSchema>>, InferResponseMapErrors<R>, InferResponseMapSuccesses<R>> : RouteRequestHandler<TMiddleware, {
21
51
  path: TAction['schema'] extends {
22
52
  path: any;
23
53
  } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
@@ -27,5 +57,25 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
27
57
  headers: TAction['schema'] extends {
28
58
  headers: any;
29
59
  } ? z.infer<TAction['schema']['headers']> : undefined;
30
- }, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>>;
60
+ }, InferRouteHandlerResult<Extract<TAction['schema'], RouteSchema>>, InferResponseMapErrors<R>, InferResponseMapSuccesses<R>> : TAction['method'] extends 'GET' ? RouteRequestHandler<TMiddleware, {
61
+ path: TAction['schema'] extends {
62
+ path: any;
63
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
64
+ query: TAction['schema'] extends {
65
+ query: any;
66
+ } ? z.infer<TAction['schema']['query']> : undefined;
67
+ headers: TAction['schema'] extends {
68
+ headers: any;
69
+ } ? z.infer<TAction['schema']['headers']> : undefined;
70
+ }, InferRouteHandlerResult<Extract<TAction['schema'], RouteSchema>>> : RouteRequestHandler<TMiddleware, {
71
+ path: TAction['schema'] extends {
72
+ path: any;
73
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
74
+ body: TAction['schema'] extends {
75
+ body: any;
76
+ } ? z.infer<TAction['schema']['body']> : undefined;
77
+ headers: TAction['schema'] extends {
78
+ headers: any;
79
+ } ? z.infer<TAction['schema']['headers']> : undefined;
80
+ }, InferRouteHandlerResult<Extract<TAction['schema'], RouteSchema>>>;
31
81
  export {};
@@ -30,6 +30,6 @@ export type RouteRequestFactory<T extends RouteSchema, P extends string> = {
30
30
  (...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never): RouteRequest<InferRouteResponse<T>>;
31
31
  /** Inferred argument type for this request factory. */
32
32
  $args: RouteArgs<T, P>;
33
- /** Inferred JSON response type for this request factory. */
33
+ /** Inferred response type for this request factory. */
34
34
  $response: InferRouteResponse<T>;
35
35
  };
@@ -1,9 +1,57 @@
1
- import type { Unchecked, RouteSchema } from './schema.js';
1
+ import type { Unchecked, UncheckedError } from '../common.js';
2
+ import type { ResponsePluginMarker } from '../response.js';
3
+ import type { RouteResponseMap, RouteSchema } from './schema.js';
2
4
  /** `Response` whose `.json()` method resolves to a known payload type. */
3
5
  export type RouteResponse<TResult = any> = Response & {
4
6
  json(): Promise<TResult>;
5
7
  };
6
- /** Infer the JSON response payload type from an action schema. */
8
+ /**
9
+ * Helper: given a status-keyed response map, produce the discriminated tuple
10
+ * union for the client.
11
+ *
12
+ * Each entry becomes:
13
+ * - `$type<T>()` → `[null, T, Status]`
14
+ * - `$error<T>()` → `[T, null, Status]`
15
+ */
16
+ type InferResponseMapClient<T extends RouteResponseMap> = {
17
+ [K in keyof T & number]: T[K] extends UncheckedError<infer TError> ? [TError, null, K] : T[K] extends Unchecked<infer TSuccess> ? [null, TSuccess, K] : T[K] extends ResponsePluginMarker<infer TClient, any> ? [null, TClient, K] : never;
18
+ }[keyof T & number];
19
+ /**
20
+ * Infer the generated client action result type from an action schema.
21
+ *
22
+ * @remarks Direct JSON markers infer their payload type, plugin markers infer
23
+ * their client result type, and status-keyed response maps infer a tuple union
24
+ * of `[null, value, status]` success entries and `[error, null, status]` error
25
+ * entries.
26
+ */
7
27
  export type InferRouteResponse<T extends RouteSchema> = T extends {
8
- response: Unchecked<infer TResponse>;
9
- } ? TResponse : void;
28
+ response: infer R;
29
+ } ? R extends ResponsePluginMarker<infer TClient, any> ? TClient : R extends Unchecked<infer TResponse> ? TResponse : R extends RouteResponseMap ? InferResponseMapClient<R> : void : void;
30
+ /**
31
+ * Helper: given a status-keyed response map, produce the union of handler
32
+ * result types (success values the handler can return directly).
33
+ */
34
+ type InferResponseMapHandlerResult<T extends RouteResponseMap> = {
35
+ [K in keyof T & number]: T[K] extends Unchecked<infer TSuccess> ? TSuccess : T[K] extends ResponsePluginMarker<any, infer TRouter> ? TRouter : never;
36
+ }[keyof T & number];
37
+ /**
38
+ * Infer the non-`Response` handler result type from an action schema.
39
+ *
40
+ * @remarks For status-keyed response maps, this includes only success result
41
+ * values. Declared error responses are returned with `ctx.error(status, body)`.
42
+ */
43
+ export type InferRouteHandlerResult<T extends RouteSchema> = T extends {
44
+ response: infer R;
45
+ } ? R extends ResponsePluginMarker<any, infer TRouter> ? TRouter : R extends Unchecked<infer TResponse> ? TResponse : R extends RouteResponseMap ? InferResponseMapHandlerResult<R> : void : void;
46
+ /**
47
+ * Helper: given a status-keyed response map, extract error entries as a union
48
+ * of `[status, body]` pairs for typing `ctx.error(status, body)`.
49
+ */
50
+ export type InferResponseMapErrors<T extends RouteResponseMap> = {
51
+ [K in keyof T & number]: T[K] extends UncheckedError<infer TError> ? [K, TError] : never;
52
+ }[keyof T & number];
53
+ /** Extract success entries as a union of `[status, body]` pairs. */
54
+ export type InferResponseMapSuccesses<T extends RouteResponseMap> = {
55
+ [K in keyof T & number]: T[K] extends Unchecked<infer TSuccess> ? [K, TSuccess] : T[K] extends ResponsePluginMarker<any, infer TRouter> ? [K, TRouter] : never;
56
+ }[keyof T & number];
57
+ export {};
@@ -1,12 +1,28 @@
1
1
  import * as z from 'zod';
2
- import { Unchecked } from '../common.js';
2
+ import type { Unchecked, UncheckedError } from '../common.js';
3
+ import type { ResponsePluginMarker } from '../response.js';
3
4
  /**
4
- * Compile-time-only marker used by `$type<T>()` for unchecked response types.
5
+ * Compile-time-only marker used by `$type<T>()` for unchecked JSON response
6
+ * types.
5
7
  *
6
8
  * @remarks Application code should usually call `$type<T>()` instead of naming
7
9
  * this marker directly.
8
10
  */
9
- export type { Unchecked };
11
+ export type { ResponsePluginMarker, Unchecked, UncheckedError };
12
+ /** Single response marker accepted by status-keyed response maps. */
13
+ export type RouteResponseMarker = Unchecked<any> | UncheckedError<any> | ResponsePluginMarker<any, any>;
14
+ /**
15
+ * Status-keyed response map for declaring multiple response types.
16
+ *
17
+ * @remarks Numeric keys are HTTP status codes. Use `$type<T>()` or a response
18
+ * plugin marker for success responses and `$error<T>()` for declared error
19
+ * JSON responses.
20
+ */
21
+ export type RouteResponseMap = {
22
+ [status: number]: RouteResponseMarker;
23
+ };
24
+ /** Response marker accepted by HTTP action schemas. */
25
+ export type RouteResponseSchema = Unchecked<any> | ResponsePluginMarker<any, any> | RouteResponseMap;
10
26
  /** Schema shape for `GET` route methods. */
11
27
  export type QueryRouteSchema = {
12
28
  /** Optional Zod object used to validate path params. */
@@ -17,8 +33,8 @@ export type QueryRouteSchema = {
17
33
  body?: never;
18
34
  /** Optional Zod object used to validate request headers. */
19
35
  headers?: z.ZodObject<any>;
20
- /** Optional compile-time-only JSON response type marker. */
21
- response?: Unchecked<any>;
36
+ /** Optional compile-time-only JSON or plugin response type marker. */
37
+ response?: RouteResponseSchema;
22
38
  };
23
39
  /** Schema shape for mutation route methods. */
24
40
  export type MutationRouteSchema = {
@@ -30,8 +46,8 @@ export type MutationRouteSchema = {
30
46
  body?: z.ZodType<any, any>;
31
47
  /** Optional Zod object used to validate request headers. */
32
48
  headers?: z.ZodObject<any>;
33
- /** Optional compile-time-only JSON response type marker. */
34
- response?: Unchecked<any>;
49
+ /** Optional compile-time-only JSON or plugin response type marker. */
50
+ response?: RouteResponseSchema;
35
51
  };
36
52
  /** Any HTTP action schema Rouzer can execute. */
37
53
  export type RouteSchema = QueryRouteSchema | MutationRouteSchema;