@axarai/axar
Version:
TypeScript-based agent framework for building agentic applications powered by LLMs
304 lines (303 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.toZodSchema = toZodSchema;
require("reflect-metadata");
const zod_1 = require("zod");
const meta_keys_1 = require("./meta-keys");
const info_1 = require("./info");
/**
* Checks if a property is marked as optional
* @param target - Target object (class prototype)
* @param propertyKey - Property name
*/
function isOptional(target, propertyKey) {
return Reflect.getMetadata(meta_keys_1.META_KEYS.OPTIONAL, target, propertyKey) || false;
}
// Type guards
function isZodString(schema) {
return schema instanceof zod_1.z.ZodString;
}
function isZodNumber(schema) {
return schema instanceof zod_1.z.ZodNumber;
}
function isZodArray(schema) {
return schema instanceof zod_1.z.ZodArray;
}
/**
* Creates a base Zod schema for a property based on its type
* @param target - Target object (class prototype)
* @param propertyKey - Property name
*/
function createBaseSchema(target, propertyKey) {
// Try to create enum schema first
const enumSchema = createEnumSchema(target, propertyKey);
if (enumSchema)
return enumSchema;
const propertyType = Reflect.getMetadata('design:type', target, propertyKey);
if (propertyType === Array) {
return createArraySchema(target, propertyKey);
}
return createPrimitiveOrObjectSchema(propertyType, propertyKey);
}
function createEnumSchema(target, propertyKey) {
const enumValues = Reflect.getMetadata(meta_keys_1.META_KEYS.ENUM_VALUES, target, propertyKey);
if (!enumValues)
return null;
if (!Array.isArray(enumValues) || enumValues.length === 0) {
throw new Error(`Enum values for ${String(propertyKey)} must be a non-empty array`);
}
if (enumValues.every((v) => typeof v === 'string')) {
return zod_1.z.enum(enumValues);
}
if (enumValues.every((v) => typeof v === 'number')) {
return zod_1.z
.enum(enumValues.map(String))
.transform((val) => {
const num = Number(val);
if (Number.isNaN(num)) {
throw new Error(`Invalid enum value for ${String(propertyKey)}`);
}
return num;
});
}
throw new Error(`Enum values for ${String(propertyKey)} must be all strings or all numbers`);
}
function createArraySchema(target, propertyKey) {
const itemTypeFn = Reflect.getMetadata(meta_keys_1.META_KEYS.ARRAY_ITEM_TYPE, target, propertyKey);
if (!itemTypeFn) {
throw new Error(`Array property ${String(propertyKey)} must use @arrayItems decorator`);
}
const itemType = itemTypeFn();
return zod_1.z.array(createPrimitiveOrObjectSchema(itemType, propertyKey));
}
/**
* Creates a Zod schema for a primitive type or custom class
* @param type - Constructor function for the type
* @param propertyKey - Property name (used for error messages)
* @returns Appropriate Zod schema for the type
* @throws Error if type is unsupported or invalid
*/
function createPrimitiveOrObjectSchema(type, propertyKey) {
switch (type) {
case undefined:
throw new Error(`Type for property ${String(propertyKey)} cannot be undefined`);
case String:
return zod_1.z.string();
case Number:
return zod_1.z.number();
case Boolean:
return zod_1.z.boolean();
case Date:
return zod_1.z.date();
case Symbol:
return zod_1.z.symbol();
case BigInt:
return zod_1.z.bigint();
default:
if (type?.name === 'Object') {
throw new Error(`Type 'any' is not allowed for property ${String(propertyKey)}. Please specify a concrete type`);
}
if (type?.prototype) {
try {
if (!(0, info_1.hasSchemaDef)(type)) {
throw new Error(`Type ${type.name} must be decorated with @schema`);
}
return toZodSchema(type);
}
catch (error) {
throw new Error(`Failed to create schema for nested type ${type.name} in ${String(propertyKey)}: ${error.message}`);
}
}
throw new Error(`Unsupported type for property ${String(propertyKey)}`);
}
}
/**
* Applies a validation rule to a Zod schema
* @param schema - Base Zod schema
* @param rule - Validation rule to apply
* @throws Error if validation rule cannot be applied to schema type
*/
function applyValidationRule(schema, rule) {
switch (rule.type) {
// String validations
case 'email':
if (!isZodString(schema)) {
throw new Error('Email validation can only be applied to strings');
}
return schema.email();
case 'url':
if (!isZodString(schema)) {
throw new Error('URL validation can only be applied to strings');
}
return schema.url();
case 'uuid':
if (!isZodString(schema)) {
throw new Error('UUID validation can only be applied to strings');
}
return schema.uuid();
case 'cuid':
if (!isZodString(schema)) {
throw new Error('CUID validation can only be applied to strings');
}
return schema.cuid();
case 'datetime':
if (!isZodString(schema)) {
throw new Error('Datetime validation can only be applied to strings');
}
return schema.datetime();
case 'ip':
if (!isZodString(schema)) {
throw new Error('IP validation can only be applied to strings');
}
return schema.ip();
case 'pattern':
if (!isZodString(schema)) {
throw new Error('Pattern validation can only be applied to strings');
}
return schema.regex(rule.params[0]);
case 'min':
if (isZodString(schema)) {
return schema.min(rule.params[0]);
}
if (isZodArray(schema)) {
return schema.min(rule.params[0]);
}
if (isZodNumber(schema)) {
return schema.gte(rule.params[0]);
}
throw new Error(`Min validation cannot be applied to ${schema.constructor.name}`);
case 'max':
if (isZodString(schema)) {
return schema.max(rule.params[0]);
}
if (isZodArray(schema)) {
return schema.max(rule.params[0]);
}
if (isZodNumber(schema)) {
return schema.lte(rule.params[0]);
}
throw new Error(`Max validation cannot be applied to ${schema.constructor.name}`);
// Number validations
case 'minimum':
if (!isZodNumber(schema)) {
throw new Error('Minimum validation can only be applied to numbers');
}
return schema.gte(rule.params[0]);
case 'maximum':
if (!isZodNumber(schema)) {
throw new Error('Maximum validation can only be applied to numbers');
}
return schema.lte(rule.params[0]);
case 'exclusiveMinimum':
if (!isZodNumber(schema)) {
throw new Error('ExclusiveMinimum validation can only be applied to numbers');
}
return schema.gt(rule.params[0]);
case 'exclusiveMaximum':
if (!isZodNumber(schema)) {
throw new Error('ExclusiveMaximum validation can only be applied to numbers');
}
return schema.lt(rule.params[0]);
case 'multipleOf':
if (!isZodNumber(schema)) {
throw new Error('MultipleOf validation can only be applied to numbers');
}
return schema.multipleOf(rule.params[0]);
case 'integer':
if (!isZodNumber(schema)) {
throw new Error('Integer validation can only be applied to numbers');
}
return schema.int();
// Array validations
case 'minItems':
if (!isZodArray(schema)) {
throw new Error('MinItems validation can only be applied to arrays');
}
return schema.min(rule.params[0]);
case 'maxItems':
if (!isZodArray(schema)) {
throw new Error('MaxItems validation can only be applied to arrays');
}
return schema.max(rule.params[0]);
case 'uniqueItems':
if (!isZodArray(schema)) {
throw new Error('UniqueItems validation can only be applied to arrays');
}
return schema.refine((items) => new Set(items.map((item) => JSON.stringify(item))).size ===
items.length, { message: 'All items in array must be unique' });
case 'enum':
// Enums are handled in createBaseSchema
return schema;
default:
throw new Error(`Unknown validation type: ${rule.type}`);
}
}
// Implement caching for performance
const schemaCache = new WeakMap();
/**
* Creates a Zod schema for a given class constructor.
*
* Scans the class for property decorators and applies their validation rules
* to the generated schema.
*
* @param target - The class constructor to generate the schema for
* @returns A cached Zod object schema with all validations applied
*
* @example
* ```ts
* @schema({ description: 'User data' })
* class User {
* @property({ description: 'User email' })
* @validate('email')
* email: string;
*
* @optional()
* @validate('min', 3)
* name?: string;
* }
*
* const userSchema = toZodSchema(User);
* ``` *
* @features
* - Schema-level descriptions via `@schema` decorator
* - Property-level descriptions via `@property` decorator
* - Optional properties via `@optional` decorator
* - Built-in validations (min, max, pattern, etc.)
* - Custom validation rules via `@validate` decorator
* - Automatic schema caching for performance
*/
function toZodSchema(target) {
const cached = schemaCache.get(target);
if (cached)
return cached;
const shape = {};
const properties = Reflect.getMetadata(meta_keys_1.META_KEYS.PROPERTIES, target.prototype) || [];
for (const propertyKey of properties) {
const rules = Reflect.getMetadata(meta_keys_1.META_KEYS.PROPERTY_RULES, target.prototype, propertyKey) || [];
const propertyOptions = Reflect.getMetadata(meta_keys_1.META_KEYS.PROPERTY, target.prototype, propertyKey);
// Create base schema from type
let propertySchema = createBaseSchema(target.prototype, propertyKey);
// Apply validation rules
rules.forEach((rule) => {
propertySchema = applyValidationRule(propertySchema, rule);
});
// Apply description if exists
if (propertyOptions?.description) {
propertySchema = propertySchema.describe(propertyOptions.description);
}
// Check if the property is optional
if (isOptional(target.prototype, propertyKey)) {
propertySchema = propertySchema.optional();
}
shape[propertyKey] = propertySchema;
}
let zodSchema = zod_1.z.object(shape);
// Apply schema-level description if exists
const schemaOptions = Reflect.getMetadata(meta_keys_1.META_KEYS.SCHEMA, target) || {};
if (schemaOptions.description) {
zodSchema = zodSchema.describe(schemaOptions.description);
}
// Cache the schema for future use
schemaCache.set(target, zodSchema);
return zodSchema;
}