@beignet/core
Version:
Core framework primitives for Beignet
159 lines (137 loc) • 4.77 kB
text/typescript
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;
},
};
}