UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

159 lines (137 loc) 4.77 kB
import { type ZodTypeAny, z } from "zod"; import { getObjectSchemaShape } from "../contracts/schema-shape.js"; export type SchemaIO = "input" | "output"; type ConvertedSchemaObject = Record<string, unknown>; /** * Context passed to an OpenAPI schema converter. */ export type SchemaConverterContext = { /** * Whether the schema is documenting request input or response output. */ io: SchemaIO; /** * Component name hint Beignet will use when registering the converted schema. */ nameHint: string; }; /** * Converts a validation schema into an OpenAPI-compatible JSON Schema object. */ export interface SchemaConverter { /** * Human-readable converter name used in diagnostics. */ name: string; /** * Return true when this converter owns the schema value. */ canConvert(schema: unknown): boolean; /** * Convert the schema into an OpenAPI-compatible JSON Schema object. */ toJSONSchema( schema: unknown, context: SchemaConverterContext, ): ConvertedSchemaObject; } /** * Schema introspection adapter. * * Abstracts the details of reading metadata from a schema library (e.g. Zod) * so that the OpenAPI generator is not directly coupled to `_def` internals. * * A default Zod implementation is provided via `createZodIntrospector()`. * To support a different schema library, implement this interface. */ export interface SchemaIntrospector { /** * Extract the shape (field name → field schema) from an object schema. * Returns undefined if the schema is not an object type or cannot be inspected. */ getShape(schema: unknown): Record<string, unknown> | undefined; /** * Extract the user-supplied `.describe()` string from a schema. */ getDescription(schema: unknown): string | undefined; /** * Return true if the schema represents an optional wrapper. */ isOptional(schema: unknown): boolean; /** * If the schema is an optional wrapper, return the inner (unwrapped) schema. * Otherwise return the original schema unchanged. */ unwrapOptional(schema: unknown): unknown; } /** * Create the default schema converter for Zod schemas. */ export function createZodSchemaConverter(): SchemaConverter { return { name: "zod", canConvert(schema: unknown): boolean { return schema instanceof z.ZodType; }, toJSONSchema( schema: unknown, context: SchemaConverterContext, ): ConvertedSchemaObject { const jsonSchema = z.toJSONSchema(schema as ZodTypeAny, { target: "draft-2020-12", unrepresentable: "any", io: context.io, }) as ConvertedSchemaObject; delete jsonSchema.$schema; return jsonSchema; }, }; } /** * Create a schema introspector for Zod schemas. * * This accesses Zod's internal `_def` property, which is a common pattern in * Zod ecosystem libraries but may break with major Zod updates. Each helper * gracefully returns a safe default if the structure is unexpected. */ export function createZodIntrospector(): SchemaIntrospector { return { getShape(schema: unknown): Record<string, unknown> | undefined { return getObjectSchemaShape(schema); }, getDescription(schema: unknown): string | undefined { if (!schema || typeof schema !== "object") return undefined; const directDescription = (schema as { description?: unknown }) .description; if (typeof directDescription === "string") { return directDescription; } const schemaDef = (schema as Record<string, unknown>)?._def; if (!schemaDef || typeof schemaDef !== "object") return undefined; const description = (schemaDef as Record<string, unknown>).description; return typeof description === "string" ? description : undefined; }, isOptional(schema: unknown): boolean { if (!schema || typeof schema !== "object") return false; const schemaWithOptional = schema as { isOptional?: () => boolean; }; if (typeof schemaWithOptional.isOptional === "function") { return schemaWithOptional.isOptional(); } const schemaDef = (schema as Record<string, unknown>)?._def; if (!schemaDef || typeof schemaDef !== "object") return false; const def = schemaDef as Record<string, unknown>; return def.typeName === "ZodOptional" || def.type === "optional"; }, unwrapOptional(schema: unknown): unknown { const schemaDef = (schema as Record<string, unknown>)?._def; const def = schemaDef as Record<string, unknown> | undefined; if (def?.typeName !== "ZodOptional" && def?.type !== "optional") { return schema; } const innerType = def?.innerType; return innerType ?? schema; }, }; }