Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
export {
parseRedocUiOptions,
type RedocUiOptions,
type RedocUiOptionsInput,
redocUiOptionsSchema,
} from "./options.js";

View File

@@ -0,0 +1,82 @@
# `options` — `RedocUiOptions` schema + parser
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module defines the Zod schema and parser for `RedocUiOptions`, the
configuration bundle the ReDoc UI handler accepts. It is the single boundary
where caller-supplied option values transition from untrusted input to a
fully-defaulted, immutable typed record the rest of the package treats as
ground truth. Refusing malformed input here means the rendered HTML shell
never has to defend against half-validated values at request time.
## Features
- Strict schema — unknown keys are refused so typos surface immediately
instead of silently producing a misconfigured shell.
- Sensible defaults so the bare `createRedocUiHandler()` call still produces
a working `/docs/ui/redoc` shell against `/openapi.json`.
- `expandResponses` is a regex-validated comma-separated list of three-character
HTTP status codes (or `"all"`), matching the official ReDoc option.
- `uiAssets` strategy literal — one of `"cdn"`, `"bundled"`, `"inline"`.
- Zod failures are wrapped as `OpenapiUiRedocError(INVALID_OPTIONS)` with the
original `ZodError` attached as `cause`.
## Usage: Basic
```ts
import { parseRedocUiOptions } from "@suaveplan/api-openapi-ui-redoc/options";
const opts = parseRedocUiOptions({
openapiUrl: "/openapi.json",
theme: "dark",
});
```
## Usage: Advanced
Combine with the host-config layer to validate `host.openapi.ui = "redoc"`
options at ingestion time, before any handler is constructed:
```ts
import {
parseRedocUiOptions,
redocUiOptionsSchema,
type RedocUiOptionsInput,
} from "@suaveplan/api-openapi-ui-redoc/options";
const hostConfigSchema = z.object({
openapi: z.object({
ui: z.literal("redoc"),
redoc: redocUiOptionsSchema.partial().default({}),
}),
});
function buildHandler(raw: unknown) {
const cfg = hostConfigSchema.parse(raw);
const opts: RedocUiOptionsInput = cfg.openapi.redoc;
return parseRedocUiOptions(opts);
}
```
## API Reference
| Symbol | Kind | Description |
| ------------------------- | ------ | ----------- |
| `redocUiOptionsSchema` | const | `z.ZodObject` of the strict options shape |
| `RedocUiOptions` | type | `z.infer<typeof redocUiOptionsSchema>` (defaulted) |
| `RedocUiOptionsInput` | type | `z.input<typeof redocUiOptionsSchema>` (caller-facing) |
| `parseRedocUiOptions` | fn | `(input?) => RedocUiOptions` — throws `OpenapiUiRedocError(INVALID_OPTIONS)` |
## Implementation Notes
The schema is `.strict()` so additional keys raise the catch-all
`INVALID_OPTIONS` code rather than silently passing through. We use
`safeParse` internally so we can wrap the error rather than re-throw the raw
`ZodError`; the wrapper carries the registry code and stable message but the
underlying issue list remains accessible via `err.cause`. Defaults are baked
into the schema so a `parseRedocUiOptions({})` call returns a fully-formed
options record without callers having to reach for any other layer.

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
OPENAPI_UI_REDOC_CODES,
OpenapiUiRedocError,
} from "../error-codes/error-codes.js";
import { parseRedocUiOptions, redocUiOptionsSchema } from "./options.js";
describe("parseRedocUiOptions", () => {
it("defaults are applied when no input is given", { timeout: 5000 }, () => {
const parsed = parseRedocUiOptions(undefined);
expect(parsed.openapiUrl).toBe("/openapi.json");
expect(parsed.title).toBe("API Reference");
expect(parsed.theme).toBe("light");
expect(parsed.uiAssets).toBe("cdn");
expect(parsed.hideDownloadButton).toBe(false);
expect(parsed.nativeScrollbars).toBe(false);
expect(parsed.expandResponses).toBeUndefined();
});
it("passes valid expandResponses values", { timeout: 5000 }, () => {
expect(
parseRedocUiOptions({ expandResponses: "200,201" }).expandResponses
).toBe("200,201");
expect(
parseRedocUiOptions({ expandResponses: "all" }).expandResponses
).toBe("all");
expect(
parseRedocUiOptions({ expandResponses: "2xx" }).expandResponses
).toBe("2xx");
});
it("invalid expandResponses rejected", { timeout: 5000 }, () => {
try {
parseRedocUiOptions({ expandResponses: "blahblah" });
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OpenapiUiRedocError);
expect((err as OpenapiUiRedocError).redocCode).toBe(
OPENAPI_UI_REDOC_CODES.INVALID_OPTIONS
);
}
});
it("unknown keys are refused by the strict schema", {
timeout: 5000,
}, () => {
expect(() =>
parseRedocUiOptions({
// @ts-expect-error — deliberate
bogus: "field",
})
).toThrow(OpenapiUiRedocError);
});
it("schema is exported and accepts a partial input", {
timeout: 5000,
}, () => {
const result = redocUiOptionsSchema.safeParse({});
expect(result.success).toBe(true);
});
it("returned options object is frozen", { timeout: 5000 }, () => {
const parsed = parseRedocUiOptions({});
expect(Object.isFrozen(parsed)).toBe(true);
});
});

View File

@@ -0,0 +1,74 @@
import z from "zod";
import {
OPENAPI_UI_REDOC_CODES,
OpenapiUiRedocError,
} from "../error-codes/error-codes.js";
/**
* Asset-strategy literals. `cdn` — point the shell at the upstream
* ReDoc CDN URL. `bundled` — serve the runtime from a caller-supplied
* payload (see `assets.ts`). `inline` — embed the entire runtime
* literal into the HTML shell (no separate asset request).
*/
const uiAssetsSchema = z
.enum(["cdn", "bundled", "inline"] as const)
.default("cdn");
/**
* `expandResponses` is a comma-separated list of HTTP status codes
* (or `"all"`) per the ReDoc option of the same name. We validate
* shape strictly to refuse free-form garbage that would silently
* break the shell at runtime.
*/
const EXPAND_RESPONSES_RE = /^(all|[0-9xX*]{3}(,[0-9xX*]{3})*)$/;
const expandResponsesSchema = z
.string()
.regex(EXPAND_RESPONSES_RE, {
message:
"expandResponses must be 'all' or a comma-separated list of three-character codes (e.g. '200,201' or '2xx' / '2XX')",
})
.optional();
/**
* Zod schema for {@link RedocUiOptions}. Strict — unknown keys are
* refused so typos surface at boundary parse time, not at runtime
* when the rendered shell silently ignores them.
*/
export const redocUiOptionsSchema = z
.object({
openapiUrl: z.string().min(1).default("/openapi.json"),
title: z.string().min(1).default("API Reference"),
theme: z.enum(["light", "dark"] as const).default("light"),
expandResponses: expandResponsesSchema,
hideDownloadButton: z.boolean().default(false),
nativeScrollbars: z.boolean().default(false),
uiAssets: uiAssetsSchema,
})
.strict();
/** Parsed, defaulted options consumed by `createRedocUiHandler`. */
export type RedocUiOptions = z.infer<typeof redocUiOptionsSchema>;
/** Caller-facing input — every field optional, defaults applied by Zod. */
export type RedocUiOptionsInput = z.input<typeof redocUiOptionsSchema>;
/**
* Validate caller-supplied options. Throws
* `OpenapiUiRedocError(INVALID_OPTIONS)` on shape violation with the
* underlying `ZodError` attached as `cause`. Returns a fully
* defaulted, immutable `RedocUiOptions` on success.
*/
export function parseRedocUiOptions(
input: RedocUiOptionsInput | undefined
): RedocUiOptions {
const result = redocUiOptionsSchema.safeParse(input ?? {});
if (!result.success) {
throw new OpenapiUiRedocError(
OPENAPI_UI_REDOC_CODES.INVALID_OPTIONS,
`Invalid RedocUiOptions: ${result.error.message}`,
{ cause: result.error }
);
}
return Object.freeze(result.data);
}