@boon4681/giri 0.0.1-hotfix-1 → 0.0.2
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.
- package/README.md +18 -16
- package/dist/adapters/hono.d.ts +26 -3
- package/dist/adapters/hono.js +49 -2
- package/dist/adapters/hono.js.map +1 -1
- package/dist/cli.js +1173 -1105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1326 -1288
- package/dist/index.js.map +1 -1
- package/dist/{types-BrUMxh5u.d.ts → types-DkrKD1S4.d.ts} +4 -2
- package/dist/validators/valibot.d.ts +1 -1
- package/dist/validators/valibot.js.map +1 -1
- package/dist/validators/zod.d.ts +1 -1
- package/dist/validators/zod.js.map +1 -1
- package/package.json +1 -1
|
@@ -93,9 +93,11 @@ type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, in
|
|
|
93
93
|
/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */
|
|
94
94
|
type MergeStack<T> = T extends readonly [infer Head, ...infer Rest] ? VarsOf<Head> & MergeStack<Rest> : {};
|
|
95
95
|
/**
|
|
96
|
-
* Merge the vars
|
|
96
|
+
* Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;
|
|
97
|
+
* a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a
|
|
98
|
+
* plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.
|
|
97
99
|
*/
|
|
98
|
-
type InferStackVars<T> = T extends readonly [unknown, ...unknown[]] ? MergeStack<T> : {};
|
|
100
|
+
type InferStackVars<T> = T extends readonly [unknown, ...unknown[]] ? MergeStack<T> : T extends Middleware<Record<string, string>, ValidatedInput, any> ? VarsOf<T> : {};
|
|
99
101
|
/**
|
|
100
102
|
* The vars injected by a module own `middleware` export (a `stack(...)`). Used by the
|
|
101
103
|
* generated per-method handle so a verb file's own `export const middleware` types
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
|
-
import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-
|
|
2
|
+
import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-DkrKD1S4.js';
|
|
3
3
|
|
|
4
4
|
type AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;
|
|
5
5
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/validators/valibot.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { toJsonSchema } from '@valibot/to-json-schema';\nimport * as v from 'valibot';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\ntype AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;\n\n/** Wrap a single Valibot schema as a giri input schema. */\nfunction wrap<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return defineInputSchema<v.InferOutput<Schema>>({\n validate(value) {\n const result = v.safeParse(schema, value);\n return result.success\n ? { ok: true, value: result.output }\n : { ok: false, issues: result.issues };\n },\n toJsonSchema() {\n return toJsonSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Valibot adapter. Peer-depends `valibot` and `@valibot/to-json-schema`.\n *\n * ```ts\n * import * as v from 'valibot';\n * import { valibot } from '@boon4681/giri/validators/valibot';\n *\n * export const body = valibot.body({ json: v.object({ name: v.pipe(v.string(), v.minLength(1)) }) });\n * export const query = valibot.query(v.object({ page: v.string() }));\n * ```\n */\nexport const valibot = {\n body<Map extends Partial<Record<BodyContentType, AnySchema>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never;\n }>;\n },\n query<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the vars from a middleware stack export. A plain `Middleware[]` (not a `stack(...)` tuple)\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]] ? MergeStack<T> : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAA6B;AAC7B,QAAmB;;;AC8JZ,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACxJpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF3CA,SAAS,KAA+B,QAAwD;AAC5F,SAAO,kBAAyC;AAAA,IAC5C,SAAS,OAAO;AACZ,YAAM,SAAW,YAAU,QAAQ,KAAK;AACxC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,OAAO,IACjC,EAAE,IAAI,OAAO,QAAQ,OAAO,OAAO;AAAA,IAC7C;AAAA,IACA,eAAe;AACX,iBAAO,oCAAa,MAAM;AAAA,IAC9B;AAAA,EACJ,CAAC;AACL;AAaO,IAAM,UAAU;AAAA,EACnB,KACI,KAC8F;AAC9F,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAwD;AACpF,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/validators/valibot.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { toJsonSchema } from '@valibot/to-json-schema';\nimport * as v from 'valibot';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\ntype AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;\n\n/** Wrap a single Valibot schema as a giri input schema. */\nfunction wrap<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return defineInputSchema<v.InferOutput<Schema>>({\n validate(value) {\n const result = v.safeParse(schema, value);\n return result.success\n ? { ok: true, value: result.output }\n : { ok: false, issues: result.issues };\n },\n toJsonSchema() {\n return toJsonSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Valibot adapter. Peer-depends `valibot` and `@valibot/to-json-schema`.\n *\n * ```ts\n * import * as v from 'valibot';\n * import { valibot } from '@boon4681/giri/validators/valibot';\n *\n * export const body = valibot.body({ json: v.object({ name: v.pipe(v.string(), v.minLength(1)) }) });\n * export const query = valibot.query(v.object({ page: v.string() }));\n * ```\n */\nexport const valibot = {\n body<Map extends Partial<Record<BodyContentType, AnySchema>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never;\n }>;\n },\n query<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAA6B;AAC7B,QAAmB;;;ACqKZ,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC/JpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF3CA,SAAS,KAA+B,QAAwD;AAC5F,SAAO,kBAAyC;AAAA,IAC5C,SAAS,OAAO;AACZ,YAAM,SAAW,YAAU,QAAQ,KAAK;AACxC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,OAAO,IACjC,EAAE,IAAI,OAAO,QAAQ,OAAO,OAAO;AAAA,IAC7C;AAAA,IACA,eAAe;AACX,iBAAO,oCAAa,MAAM;AAAA,IAC9B;AAAA,EACJ,CAAC;AACL;AAaO,IAAM,UAAU;AAAA,EACnB,KACI,KAC8F;AAC9F,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAwD;AACpF,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
|
package/dist/validators/zod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-
|
|
2
|
+
import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-DkrKD1S4.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Zod adapter. Peer-depends `zod`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return defineInputSchema<z.infer<Schema>>({\n validate(value) {\n const result = schema.safeParse(value);\n return result.success\n ? { ok: true, value: result.data }\n : { ok: false, issues: result.error };\n },\n toJsonSchema() {\n return z.toJSONSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Zod adapter. Peer-depends `zod`.\n *\n * ```ts\n * import { z } from 'zod';\n * import { zod } from '@boon4681/giri/validators/zod';\n *\n * // JSON body\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\n * // JSON *or* multipart dispatched on Content-Type at runtime\n * export const body = zod.body({\n * json: z.object({ name: z.string() }),\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\n * });\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\n * ```\n */\nexport const zod = {\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\n }>;\n },\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the vars from a middleware stack export. A plain `Middleware[]` (not a `stack(...)` tuple)\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]] ? MergeStack<T> : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;AC+JX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACxJpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF9CA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AACX,aAAO,aAAE,aAAa,MAAM;AAAA,IAChC;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return defineInputSchema<z.infer<Schema>>({\n validate(value) {\n const result = schema.safeParse(value);\n return result.success\n ? { ok: true, value: result.data }\n : { ok: false, issues: result.error };\n },\n toJsonSchema() {\n return z.toJSONSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Zod adapter. Peer-depends `zod`.\n *\n * ```ts\n * import { z } from 'zod';\n * import { zod } from '@boon4681/giri/validators/zod';\n *\n * // JSON body\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\n * // JSON *or* multipart dispatched on Content-Type at runtime\n * export const body = zod.body({\n * json: z.object({ name: z.string() }),\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\n * });\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\n * ```\n */\nexport const zod = {\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\n }>;\n },\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;ACsKX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC/JpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF9CA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AACX,aAAO,aAAE,aAAa,MAAM;AAAA,IAChC;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
|