Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "api-contracts-and-zod-validation",
|
||||||
|
"description": "Generate Zod schemas and TypeScript types for forms, API routes, and Server Actions with runtime validation. Use this skill when creating API contracts, validating request/response payloads, generating form schemas, adding input validation to Server Actions or route handlers, or ensuring type safety across client-server boundaries. Trigger terms include zod, schema, validation, API contract, form validation, type inference, runtime validation, parse, safeParse, input validation, request validati",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Hope Overture",
|
||||||
|
"email": "support@worldbuilding-app-skills.dev"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# api-contracts-and-zod-validation
|
||||||
|
|
||||||
|
Generate Zod schemas and TypeScript types for forms, API routes, and Server Actions with runtime validation. Use this skill when creating API contracts, validating request/response payloads, generating form schemas, adding input validation to Server Actions or route handlers, or ensuring type safety across client-server boundaries. Trigger terms include zod, schema, validation, API contract, form validation, type inference, runtime validation, parse, safeParse, input validation, request validati
|
||||||
69
plugin.lock.json
Normal file
69
plugin.lock.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/api-contracts-and-zod-validation",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "77a0f2121742d8e4932d0e2aa143a1c97aa17596",
|
||||||
|
"treeHash": "58555bca51fcdfb92b21fedc000b43fd5d48f772e6caadbbf0ede0932b1725d2",
|
||||||
|
"generatedAt": "2025-11-28T10:17:31.022390Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "api-contracts-and-zod-validation",
|
||||||
|
"description": "Generate Zod schemas and TypeScript types for forms, API routes, and Server Actions with runtime validation. Use this skill when creating API contracts, validating request/response payloads, generating form schemas, adding input validation to Server Actions or route handlers, or ensuring type safety across client-server boundaries. Trigger terms include zod, schema, validation, API contract, form validation, type inference, runtime validation, parse, safeParse, input validation, request validati",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "d0a0dc2863c094f3005666a3349526352174ef87dfc5280a9489b5069d395622"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "74acb295a7b756b01a5654aebc4c5d02e9f17da4ea9b589fa72d9b2539b69591"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/SKILL.md",
|
||||||
|
"sha256": "e4add77daf6cb22a225c9504431afe0d9c4e57ed12f4d5ccf645f38197cf5cd3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/references/zod_patterns.md",
|
||||||
|
"sha256": "e5a4ad84e445e20b59cf22eb81036f310e68fa8c35c487c0e13cd147f335da32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/scripts/generate_zod_schema.py",
|
||||||
|
"sha256": "247dcc77226a6b9d17e3f80d94de29e93558526a309ccc5f5070b9d9fbc1e7a2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/assets/schema_templates/form_schema_template.ts",
|
||||||
|
"sha256": "f552d06a4b68ddfe50e5a81ccc8d8aff5cf5fe8add8abb0d92abb671a876b163"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/assets/schema_templates/api_route_schema_template.ts",
|
||||||
|
"sha256": "1c244e70d23e97de636e90c677175962eae3767d3f80f3900397a4ece4ceeb78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/assets/schema_templates/entity_schema_template.ts",
|
||||||
|
"sha256": "2f18355726177380fa66e4898474e752c39b84deb895120abb9d176393969cba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/api-contracts-and-zod-validation/assets/schema_templates/server_action_schema_template.ts",
|
||||||
|
"sha256": "4db03c84bd401b95dca83f20cb7a281cdb6325541fa2659d5a2b387199347da0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "58555bca51fcdfb92b21fedc000b43fd5d48f772e6caadbbf0ede0932b1725d2"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
124
skills/api-contracts-and-zod-validation/SKILL.md
Normal file
124
skills/api-contracts-and-zod-validation/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
name: api-contracts-and-zod-validation
|
||||||
|
description: Generate Zod schemas and TypeScript types for forms, API routes, and Server Actions with runtime validation. Use this skill when creating API contracts, validating request/response payloads, generating form schemas, adding input validation to Server Actions or route handlers, or ensuring type safety across client-server boundaries. Trigger terms include zod, schema, validation, API contract, form validation, type inference, runtime validation, parse, safeParse, input validation, request validation, Server Action validation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Contracts and Zod Validation
|
||||||
|
|
||||||
|
Generate Zod schemas with TypeScript type inference for forms, API routes, and Server Actions. Validate inputs in Server Actions or route handlers with runtime type checking.
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. Generate Zod Schemas from TypeScript Types
|
||||||
|
|
||||||
|
When creating API contracts or form validations:
|
||||||
|
|
||||||
|
- Analyze existing TypeScript interfaces or types
|
||||||
|
- Generate equivalent Zod schemas with proper validation rules
|
||||||
|
- Ensure bidirectional type compatibility (Zod -> TypeScript)
|
||||||
|
- Use `z.infer<typeof schema>` for automatic type inference
|
||||||
|
|
||||||
|
### 2. Form Validation Schemas
|
||||||
|
|
||||||
|
To create form validation with Zod:
|
||||||
|
|
||||||
|
- Generate schemas for React Hook Form, Formik, or native forms
|
||||||
|
- Include field-level validation rules (min/max length, regex, custom validators)
|
||||||
|
- Support nested objects, arrays, and complex data structures
|
||||||
|
- Provide helpful error messages for validation failures
|
||||||
|
- Use `references/zod_patterns.md` for common validation patterns
|
||||||
|
|
||||||
|
### 3. API Route and Server Action Validation
|
||||||
|
|
||||||
|
To add validation to API endpoints or Server Actions:
|
||||||
|
|
||||||
|
- Wrap handler logic with schema validation using `.parse()` or `.safeParse()`
|
||||||
|
- Return typed errors for validation failures
|
||||||
|
- Generate request/response schemas for API contracts
|
||||||
|
- Validate query parameters, body payloads, and route parameters
|
||||||
|
- Ensure type safety between client and server code
|
||||||
|
|
||||||
|
### 4. Schema Generation Script
|
||||||
|
|
||||||
|
Use `scripts/generate_zod_schema.py` to automate schema generation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/generate_zod_schema.py --input types/entities.ts --output schemas/entities.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
- Parses TypeScript interfaces/types
|
||||||
|
- Generates Zod schemas with appropriate validators
|
||||||
|
- Preserves JSDoc comments as descriptions
|
||||||
|
- Handles common patterns (optional fields, unions, enums)
|
||||||
|
|
||||||
|
## Resource Files
|
||||||
|
|
||||||
|
### scripts/generate_zod_schema.py
|
||||||
|
Automated TypeScript-to-Zod schema generator. Parses TypeScript AST and generates equivalent Zod schemas.
|
||||||
|
|
||||||
|
### references/zod_patterns.md
|
||||||
|
Common Zod validation patterns including:
|
||||||
|
- String validation (email, URL, UUID, custom regex)
|
||||||
|
- Number constraints (min, max, positive, integer)
|
||||||
|
- Date validation and transformation
|
||||||
|
- Array and tuple validation
|
||||||
|
- Object shape validation
|
||||||
|
- Union and discriminated unions
|
||||||
|
- Optional and nullable fields
|
||||||
|
- Custom refinements and transforms
|
||||||
|
- Error message customization
|
||||||
|
|
||||||
|
### assets/schema_templates/
|
||||||
|
Pre-built Zod schema templates:
|
||||||
|
- `form_schema_template.ts` - Basic form validation
|
||||||
|
- `api_route_schema_template.ts` - API route request/response
|
||||||
|
- `server_action_schema_template.ts` - Server Action input/output
|
||||||
|
- `entity_schema_template.ts` - Database entity validation
|
||||||
|
|
||||||
|
## Usage Workflow
|
||||||
|
|
||||||
|
### For New API Routes
|
||||||
|
|
||||||
|
1. Define request/response TypeScript types
|
||||||
|
2. Generate Zod schemas using `scripts/generate_zod_schema.py` or templates
|
||||||
|
3. Add validation middleware to route handler
|
||||||
|
4. Return typed validation errors on failure
|
||||||
|
|
||||||
|
### For Server Actions
|
||||||
|
|
||||||
|
1. Define input parameters as TypeScript types
|
||||||
|
2. Create Zod schema for validation
|
||||||
|
3. Use `.safeParse()` at the beginning of the action
|
||||||
|
4. Return validation errors in action result
|
||||||
|
5. Handle errors on the client side
|
||||||
|
|
||||||
|
### For Forms
|
||||||
|
|
||||||
|
1. Define form fields as TypeScript interface
|
||||||
|
2. Generate Zod schema with field validators
|
||||||
|
3. Integrate with form library (React Hook Form recommended)
|
||||||
|
4. Display validation errors inline
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Always use `.safeParse()` instead of `.parse()` to avoid throwing exceptions
|
||||||
|
- Provide clear, user-friendly error messages
|
||||||
|
- Validate early (at API boundary or Server Action entry)
|
||||||
|
- Keep schemas colocated with routes/actions for maintainability
|
||||||
|
- Use `z.infer<typeof schema>` for automatic TypeScript types
|
||||||
|
- Document validation rules in schema comments
|
||||||
|
- Test edge cases (empty values, invalid formats, boundary conditions)
|
||||||
|
|
||||||
|
## Integration with Worldbuilding App
|
||||||
|
|
||||||
|
Common use cases for worldbuilding applications:
|
||||||
|
|
||||||
|
- **Entity creation forms**: Validate character, location, faction data
|
||||||
|
- **Relationship APIs**: Ensure valid entity IDs and relationship types
|
||||||
|
- **Timeline events**: Validate dates, ordering, and event data
|
||||||
|
- **Search/filter endpoints**: Validate query parameters and filters
|
||||||
|
- **Bulk operations**: Validate arrays of entities or updates
|
||||||
|
- **Import/export**: Validate file formats and data structure
|
||||||
|
|
||||||
|
Consult `references/zod_patterns.md` for specific validation patterns applicable to worldbuilding entities.
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Route validation schema template
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Define request and response schemas
|
||||||
|
* 2. Validate incoming requests with safeParse
|
||||||
|
* 3. Return typed errors for invalid requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Request schema
|
||||||
|
const requestSchema = z.object({
|
||||||
|
query: z.string().min(1, 'Query is required'),
|
||||||
|
filters: z.object({
|
||||||
|
type: z.array(z.string()).optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.string().datetime().optional(),
|
||||||
|
end: z.string().datetime().optional(),
|
||||||
|
}).optional(),
|
||||||
|
}).optional(),
|
||||||
|
pagination: z.object({
|
||||||
|
page: z.number().int().positive().default(1),
|
||||||
|
limit: z.number().int().min(1).max(100).default(10),
|
||||||
|
}).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response schema
|
||||||
|
const responseSchema = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
data: z.any().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
details: z.any().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Types
|
||||||
|
type RequestBody = z.infer<typeof requestSchema>;
|
||||||
|
type ResponseBody = z.infer<typeof responseSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST endpoint with validation
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest): Promise<NextResponse<ResponseBody>> {
|
||||||
|
try {
|
||||||
|
// Parse request body
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
const result = requestSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid request body',
|
||||||
|
details: result.error.format(),
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe validated data
|
||||||
|
const { query, filters, pagination } = result.data;
|
||||||
|
|
||||||
|
// Perform business logic
|
||||||
|
const data = await processRequest(query, filters, pagination);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processRequest(
|
||||||
|
query: string,
|
||||||
|
filters?: RequestBody['filters'],
|
||||||
|
pagination?: RequestBody['pagination']
|
||||||
|
) {
|
||||||
|
// Implement your business logic here
|
||||||
|
return { results: [], total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET endpoint with query parameter validation
|
||||||
|
*/
|
||||||
|
const queryParamsSchema = z.object({
|
||||||
|
id: z.string().uuid('Invalid ID format'),
|
||||||
|
includeRelated: z.coerce.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest): Promise<NextResponse<ResponseBody>> {
|
||||||
|
// Parse query parameters
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const queryParams = {
|
||||||
|
id: searchParams.get('id'),
|
||||||
|
includeRelated: searchParams.get('includeRelated'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate query parameters
|
||||||
|
const result = queryParamsSchema.safeParse(queryParams);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid query parameters',
|
||||||
|
details: result.error.format(),
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, includeRelated } = result.data;
|
||||||
|
|
||||||
|
// Fetch data
|
||||||
|
const data = await fetchData(id, includeRelated);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(id: string, includeRelated?: boolean) {
|
||||||
|
// Implement your data fetching logic here
|
||||||
|
return { id, includeRelated };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH endpoint with partial update validation
|
||||||
|
*/
|
||||||
|
const updateSchema = z.object({
|
||||||
|
name: z.string().min(1).max(100).optional(),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
tags: z.array(z.string()).max(10).optional(),
|
||||||
|
status: z.enum(['active', 'inactive', 'archived']).optional(),
|
||||||
|
}).refine(
|
||||||
|
(data) => Object.keys(data).length > 0,
|
||||||
|
{ message: 'At least one field must be provided for update' }
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
): Promise<NextResponse<ResponseBody>> {
|
||||||
|
// Validate ID from route params
|
||||||
|
const idResult = z.string().uuid().safeParse(params.id);
|
||||||
|
|
||||||
|
if (!idResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid ID format',
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate request body
|
||||||
|
const body = await request.json();
|
||||||
|
const result = updateSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid update data',
|
||||||
|
details: result.error.format(),
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = idResult.data;
|
||||||
|
const updates = result.data;
|
||||||
|
|
||||||
|
// Perform update
|
||||||
|
const data = await updateData(id, updates);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateData(id: string, updates: z.infer<typeof updateSchema>) {
|
||||||
|
// Implement your update logic here
|
||||||
|
return { id, ...updates };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Middleware with validation
|
||||||
|
export function validateRequest<T>(schema: z.ZodSchema<T>) {
|
||||||
|
return async (request: NextRequest): Promise<{ data?: T; error?: ResponseBody }> => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const result = schema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
success: false,
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: result.error.format(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: result.data };
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid JSON body',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage of middleware:
|
||||||
|
/*
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const { data, error } = await validateRequest(requestSchema)(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json(error, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use validated data
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity validation schema template for worldbuilding applications
|
||||||
|
*
|
||||||
|
* Common schemas for worldbuilding entities like characters, locations, items, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Base entity schema (shared fields across all entities)
|
||||||
|
const baseEntitySchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().min(1, 'Name is required').max(200),
|
||||||
|
description: z.string().max(5000).optional(),
|
||||||
|
tags: z.array(z.string()).max(20),
|
||||||
|
metadata: z.object({
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
createdBy: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Entity types enum
|
||||||
|
export const entityTypeSchema = z.enum([
|
||||||
|
'character',
|
||||||
|
'location',
|
||||||
|
'item',
|
||||||
|
'faction',
|
||||||
|
'event',
|
||||||
|
'concept',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type EntityType = z.infer<typeof entityTypeSchema>;
|
||||||
|
|
||||||
|
// Character entity schema
|
||||||
|
export const characterSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('character'),
|
||||||
|
attributes: z.object({
|
||||||
|
age: z.number().int().nonnegative().optional(),
|
||||||
|
gender: z.string().optional(),
|
||||||
|
species: z.string().optional(),
|
||||||
|
occupation: z.string().optional(),
|
||||||
|
birthplace: z.string().uuid().optional(), // Reference to location entity
|
||||||
|
}),
|
||||||
|
relationships: z.array(
|
||||||
|
z.object({
|
||||||
|
targetId: z.string().uuid(),
|
||||||
|
type: z.string(),
|
||||||
|
strength: z.number().min(0).max(1).optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Character = z.infer<typeof characterSchema>;
|
||||||
|
|
||||||
|
// Location entity schema
|
||||||
|
export const locationSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('location'),
|
||||||
|
attributes: z.object({
|
||||||
|
locationType: z.enum(['city', 'region', 'country', 'landmark', 'building', 'other']),
|
||||||
|
population: z.number().int().nonnegative().optional(),
|
||||||
|
parentLocation: z.string().uuid().optional(), // Reference to parent location
|
||||||
|
coordinates: z.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
}).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Location = z.infer<typeof locationSchema>;
|
||||||
|
|
||||||
|
// Item entity schema
|
||||||
|
export const itemSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('item'),
|
||||||
|
attributes: z.object({
|
||||||
|
itemType: z.enum(['weapon', 'armor', 'tool', 'artifact', 'consumable', 'other']),
|
||||||
|
rarity: z.enum(['common', 'uncommon', 'rare', 'legendary']).optional(),
|
||||||
|
owner: z.string().uuid().optional(), // Reference to character entity
|
||||||
|
location: z.string().uuid().optional(), // Reference to location entity
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Item = z.infer<typeof itemSchema>;
|
||||||
|
|
||||||
|
// Faction/Organization entity schema
|
||||||
|
export const factionSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('faction'),
|
||||||
|
attributes: z.object({
|
||||||
|
factionType: z.enum(['guild', 'government', 'religion', 'military', 'criminal', 'other']),
|
||||||
|
size: z.enum(['small', 'medium', 'large', 'massive']).optional(),
|
||||||
|
headquarters: z.string().uuid().optional(), // Reference to location entity
|
||||||
|
leader: z.string().uuid().optional(), // Reference to character entity
|
||||||
|
founded: z.object({
|
||||||
|
year: z.number().int(),
|
||||||
|
month: z.number().int().min(1).max(12).optional(),
|
||||||
|
day: z.number().int().min(1).max(31).optional(),
|
||||||
|
}).optional(),
|
||||||
|
}),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
characterId: z.string().uuid(),
|
||||||
|
role: z.string(),
|
||||||
|
joinDate: z.date().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Faction = z.infer<typeof factionSchema>;
|
||||||
|
|
||||||
|
// Event entity schema
|
||||||
|
export const eventSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('event'),
|
||||||
|
attributes: z.object({
|
||||||
|
eventType: z.enum(['battle', 'meeting', 'ceremony', 'discovery', 'disaster', 'other']),
|
||||||
|
date: z.object({
|
||||||
|
year: z.number().int(),
|
||||||
|
month: z.number().int().min(1).max(12).optional(),
|
||||||
|
day: z.number().int().min(1).max(31).optional(),
|
||||||
|
}),
|
||||||
|
location: z.string().uuid().optional(), // Reference to location entity
|
||||||
|
participants: z.array(z.string().uuid()), // References to character entities
|
||||||
|
}),
|
||||||
|
}).refine(
|
||||||
|
(data) => !data.attributes.date.day || data.attributes.date.month,
|
||||||
|
{ message: 'Month is required when day is specified', path: ['attributes', 'date', 'month'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Event = z.infer<typeof eventSchema>;
|
||||||
|
|
||||||
|
// Concept entity schema (abstract ideas, magic systems, technologies, etc.)
|
||||||
|
export const conceptSchema = baseEntitySchema.extend({
|
||||||
|
type: z.literal('concept'),
|
||||||
|
attributes: z.object({
|
||||||
|
conceptType: z.enum(['magic', 'technology', 'philosophy', 'language', 'custom']),
|
||||||
|
relatedEntities: z.array(z.string().uuid()), // References to any entity
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Concept = z.infer<typeof conceptSchema>;
|
||||||
|
|
||||||
|
// Union of all entity types
|
||||||
|
export const entitySchema = z.discriminatedUnion('type', [
|
||||||
|
characterSchema,
|
||||||
|
locationSchema,
|
||||||
|
itemSchema,
|
||||||
|
factionSchema,
|
||||||
|
eventSchema,
|
||||||
|
conceptSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type Entity = z.infer<typeof entitySchema>;
|
||||||
|
|
||||||
|
// Relationship schema
|
||||||
|
export const relationshipSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
sourceId: z.string().uuid(),
|
||||||
|
targetId: z.string().uuid(),
|
||||||
|
type: z.string().min(1).max(50),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
strength: z.number().min(0).max(1).optional(),
|
||||||
|
metadata: z.record(z.string(), z.any()).optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
}).refine(
|
||||||
|
(data) => data.sourceId !== data.targetId,
|
||||||
|
{ message: 'Source and target must be different entities', path: ['targetId'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Relationship = z.infer<typeof relationshipSchema>;
|
||||||
|
|
||||||
|
// Create entity input schema (without auto-generated fields)
|
||||||
|
export const createEntityInputSchema = z.object({
|
||||||
|
type: entityTypeSchema,
|
||||||
|
name: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(5000).optional(),
|
||||||
|
tags: z.array(z.string()).max(20).default([]),
|
||||||
|
attributes: z.record(z.string(), z.any()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateEntityInput = z.infer<typeof createEntityInputSchema>;
|
||||||
|
|
||||||
|
// Update entity input schema (partial)
|
||||||
|
export const updateEntityInputSchema = z.object({
|
||||||
|
name: z.string().min(1).max(200).optional(),
|
||||||
|
description: z.string().max(5000).optional(),
|
||||||
|
tags: z.array(z.string()).max(20).optional(),
|
||||||
|
attributes: z.record(z.string(), z.any()).optional(),
|
||||||
|
}).refine(
|
||||||
|
(data) => Object.keys(data).length > 0,
|
||||||
|
{ message: 'At least one field must be provided for update' }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type UpdateEntityInput = z.infer<typeof updateEntityInputSchema>;
|
||||||
|
|
||||||
|
// Query/filter schema for entity search
|
||||||
|
export const entityQuerySchema = z.object({
|
||||||
|
query: z.string().optional(),
|
||||||
|
types: z.array(entityTypeSchema).optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
createdAfter: z.date().optional(),
|
||||||
|
createdBefore: z.date().optional(),
|
||||||
|
createdBy: z.string().optional(),
|
||||||
|
pagination: z.object({
|
||||||
|
page: z.number().int().positive().default(1),
|
||||||
|
limit: z.number().int().min(1).max(100).default(10),
|
||||||
|
}).optional(),
|
||||||
|
sortBy: z.enum(['name', 'createdAt', 'updatedAt']).default('createdAt'),
|
||||||
|
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EntityQuery = z.infer<typeof entityQuerySchema>;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form validation schema template
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Customize the schema fields for your form
|
||||||
|
* 2. Use with React Hook Form or other form libraries
|
||||||
|
* 3. Extract TypeScript type with z.infer<typeof formSchema>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
// Text input
|
||||||
|
name: z.string()
|
||||||
|
.min(1, 'Name is required')
|
||||||
|
.max(100, 'Name must be 100 characters or less'),
|
||||||
|
|
||||||
|
// Email input
|
||||||
|
email: z.string()
|
||||||
|
.email('Invalid email address'),
|
||||||
|
|
||||||
|
// Optional text field
|
||||||
|
description: z.string()
|
||||||
|
.max(500, 'Description must be 500 characters or less')
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
// Number input
|
||||||
|
age: z.coerce.number()
|
||||||
|
.int('Age must be a whole number')
|
||||||
|
.positive('Age must be positive')
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
// Select/dropdown
|
||||||
|
category: z.enum(['option1', 'option2', 'option3'], {
|
||||||
|
required_error: 'Please select a category',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Checkbox
|
||||||
|
agreedToTerms: z.boolean()
|
||||||
|
.refine((val) => val === true, {
|
||||||
|
message: 'You must agree to the terms',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Multi-select
|
||||||
|
tags: z.array(z.string())
|
||||||
|
.min(1, 'At least one tag is required')
|
||||||
|
.max(10, 'Maximum 10 tags allowed'),
|
||||||
|
|
||||||
|
// Date input
|
||||||
|
birthDate: z.coerce.date()
|
||||||
|
.max(new Date(), 'Birth date cannot be in the future')
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infer TypeScript type from schema
|
||||||
|
export type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
// Example: React Hook Form integration
|
||||||
|
/*
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
function MyForm() {
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
console.log('Valid form data:', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<input {...register('name')} />
|
||||||
|
{errors.name && <span>{errors.name.message}</span>}
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server Action validation schema template
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Define input schema for Server Action parameters
|
||||||
|
* 2. Validate with safeParse at the beginning of the action
|
||||||
|
* 3. Return validation errors to the client
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Input schema
|
||||||
|
const actionInputSchema = z.object({
|
||||||
|
id: z.string().uuid('Invalid ID format'),
|
||||||
|
name: z.string().min(1, 'Name is required').max(100),
|
||||||
|
type: z.enum(['type1', 'type2', 'type3']),
|
||||||
|
metadata: z.record(z.string(), z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Output schema (optional, for type safety)
|
||||||
|
const actionOutputSchema = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
data: z.any().optional(),
|
||||||
|
errors: z.record(z.array(z.string())).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Types
|
||||||
|
type ActionInput = z.infer<typeof actionInputSchema>;
|
||||||
|
type ActionOutput = z.infer<typeof actionOutputSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example Server Action with validation
|
||||||
|
*/
|
||||||
|
export async function myServerAction(input: unknown): Promise<ActionOutput> {
|
||||||
|
// Validate input
|
||||||
|
const result = actionInputSchema.safeParse(input);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: result.error.flatten().fieldErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe validated data
|
||||||
|
const { id, name, type, metadata } = result.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform server-side logic
|
||||||
|
// Example: database operation, API call, etc.
|
||||||
|
const data = await performAction(id, name, type, metadata);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
_general: ['An unexpected error occurred'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performAction(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
) {
|
||||||
|
// Implement your server-side logic here
|
||||||
|
return { id, name, type, metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: FormData-based Server Action
|
||||||
|
export async function formDataAction(formData: FormData): Promise<ActionOutput> {
|
||||||
|
const rawData = {
|
||||||
|
id: formData.get('id'),
|
||||||
|
name: formData.get('name'),
|
||||||
|
type: formData.get('type'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = actionInputSchema.safeParse(rawData);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: result.error.flatten().fieldErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process validated data
|
||||||
|
const { id, name, type } = result.data;
|
||||||
|
|
||||||
|
// ... rest of implementation
|
||||||
|
|
||||||
|
return { success: true, data: { id, name, type } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Client-side usage
|
||||||
|
/*
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { myServerAction } from './actions';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
||||||
|
|
||||||
|
async function handleSubmit(data: any) {
|
||||||
|
const result = await myServerAction(data);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setErrors(result.errors || {});
|
||||||
|
} else {
|
||||||
|
console.log('Success:', result.data);
|
||||||
|
setErrors({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{errors.name && <span>{errors.name[0]}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
# Zod Validation Patterns
|
||||||
|
|
||||||
|
Common Zod validation patterns for API contracts, forms, and Server Actions.
|
||||||
|
|
||||||
|
## String Validation
|
||||||
|
|
||||||
|
### Basic String Constraints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Required string
|
||||||
|
const requiredString = z.string();
|
||||||
|
|
||||||
|
// Optional string
|
||||||
|
const optionalString = z.string().optional();
|
||||||
|
|
||||||
|
// String with length constraints
|
||||||
|
const username = z.string().min(3).max(20);
|
||||||
|
|
||||||
|
// Non-empty string
|
||||||
|
const nonEmpty = z.string().min(1);
|
||||||
|
|
||||||
|
// String with default value
|
||||||
|
const withDefault = z.string().default('default value');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common String Formats
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Email validation
|
||||||
|
const email = z.string().email();
|
||||||
|
|
||||||
|
// URL validation
|
||||||
|
const url = z.string().url();
|
||||||
|
|
||||||
|
// UUID validation
|
||||||
|
const uuid = z.string().uuid();
|
||||||
|
|
||||||
|
// Custom regex pattern
|
||||||
|
const phoneNumber = z.string().regex(/^\+?[1-9]\d{1,14}$/);
|
||||||
|
|
||||||
|
// Alphanumeric only
|
||||||
|
const alphanumeric = z.string().regex(/^[a-zA-Z0-9]+$/);
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
const trimmed = z.string().trim();
|
||||||
|
|
||||||
|
// Lowercase transformation
|
||||||
|
const lowercase = z.string().toLowerCase();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced String Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Custom refinement
|
||||||
|
const strongPassword = z.string()
|
||||||
|
.min(8)
|
||||||
|
.refine(
|
||||||
|
(val) => /[A-Z]/.test(val) && /[a-z]/.test(val) && /[0-9]/.test(val),
|
||||||
|
{ message: 'Password must contain uppercase, lowercase, and number' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multiple validations with custom messages
|
||||||
|
const entityName = z.string()
|
||||||
|
.min(1, 'Name is required')
|
||||||
|
.max(100, 'Name must be 100 characters or less')
|
||||||
|
.regex(/^[a-zA-Z0-9\s-]+$/, 'Name can only contain letters, numbers, spaces, and hyphens');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Number Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic number
|
||||||
|
const age = z.number();
|
||||||
|
|
||||||
|
// Integer only
|
||||||
|
const count = z.number().int();
|
||||||
|
|
||||||
|
// Positive number
|
||||||
|
const positive = z.number().positive();
|
||||||
|
|
||||||
|
// Non-negative (>= 0)
|
||||||
|
const nonNegative = z.number().nonnegative();
|
||||||
|
|
||||||
|
// Min/max constraints
|
||||||
|
const percentage = z.number().min(0).max(100);
|
||||||
|
|
||||||
|
// Multiple of
|
||||||
|
const evenNumber = z.number().multipleOf(2);
|
||||||
|
|
||||||
|
// Finite number (not Infinity or NaN)
|
||||||
|
const finite = z.number().finite();
|
||||||
|
|
||||||
|
// Convert string to number
|
||||||
|
const stringToNumber = z.coerce.number();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boolean Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic boolean
|
||||||
|
const isActive = z.boolean();
|
||||||
|
|
||||||
|
// Optional boolean with default
|
||||||
|
const isPublic = z.boolean().default(false);
|
||||||
|
|
||||||
|
// Coerce to boolean (accepts "true", "false", 1, 0)
|
||||||
|
const coercedBoolean = z.coerce.boolean();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic date
|
||||||
|
const createdAt = z.date();
|
||||||
|
|
||||||
|
// Date with min/max constraints
|
||||||
|
const futureDate = z.date().min(new Date());
|
||||||
|
const pastDate = z.date().max(new Date());
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
const dateInRange = z.date()
|
||||||
|
.min(new Date('2020-01-01'))
|
||||||
|
.max(new Date('2030-12-31'));
|
||||||
|
|
||||||
|
// Convert string to date
|
||||||
|
const stringToDate = z.coerce.date();
|
||||||
|
|
||||||
|
// ISO date string
|
||||||
|
const isoDate = z.string().datetime();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Array Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Array of strings
|
||||||
|
const tags = z.array(z.string());
|
||||||
|
|
||||||
|
// Array with length constraints
|
||||||
|
const limitedTags = z.array(z.string()).min(1).max(10);
|
||||||
|
|
||||||
|
// Non-empty array
|
||||||
|
const nonEmptyArray = z.array(z.string()).nonempty();
|
||||||
|
|
||||||
|
// Array with unique items
|
||||||
|
const uniqueTags = z.array(z.string())
|
||||||
|
.refine((items) => new Set(items).size === items.length, {
|
||||||
|
message: 'Tags must be unique'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tuple (fixed-length array with different types)
|
||||||
|
const coordinate = z.tuple([z.number(), z.number()]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic object shape
|
||||||
|
const user = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
age: z.number().int().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nested objects
|
||||||
|
const entity = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
metadata: z.object({
|
||||||
|
createdBy: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Partial object (all fields optional)
|
||||||
|
const partialUser = user.partial();
|
||||||
|
|
||||||
|
// Pick specific fields
|
||||||
|
const userCredentials = user.pick({ email: true, password: true });
|
||||||
|
|
||||||
|
// Omit specific fields
|
||||||
|
const publicUser = user.omit({ password: true });
|
||||||
|
|
||||||
|
// Extend object
|
||||||
|
const extendedUser = user.extend({
|
||||||
|
role: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge objects
|
||||||
|
const merged = user.merge(z.object({ role: z.string() }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union and Intersection Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Union (one of multiple types)
|
||||||
|
const stringOrNumber = z.union([z.string(), z.number()]);
|
||||||
|
|
||||||
|
// Discriminated union
|
||||||
|
const result = z.discriminatedUnion('status', [
|
||||||
|
z.object({ status: z.literal('success'), data: z.any() }),
|
||||||
|
z.object({ status: z.literal('error'), error: z.string() }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Intersection (combines multiple types)
|
||||||
|
const timestamped = z.object({
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
const userWithTimestamps = z.intersection(user, timestamped);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enum and Literal Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enum
|
||||||
|
const roleEnum = z.enum(['admin', 'user', 'guest']);
|
||||||
|
|
||||||
|
// Literal
|
||||||
|
const statusLiteral = z.literal('active');
|
||||||
|
|
||||||
|
// Multiple literals (union)
|
||||||
|
const status = z.union([
|
||||||
|
z.literal('active'),
|
||||||
|
z.literal('inactive'),
|
||||||
|
z.literal('pending'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Native enum support
|
||||||
|
enum Role {
|
||||||
|
Admin = 'admin',
|
||||||
|
User = 'user',
|
||||||
|
Guest = 'guest',
|
||||||
|
}
|
||||||
|
const nativeEnum = z.nativeEnum(Role);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional and Nullable
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Optional (can be undefined)
|
||||||
|
const optional = z.string().optional();
|
||||||
|
|
||||||
|
// Nullable (can be null)
|
||||||
|
const nullable = z.string().nullable();
|
||||||
|
|
||||||
|
// Both optional and nullable
|
||||||
|
const optionalNullable = z.string().optional().nullable();
|
||||||
|
|
||||||
|
// Nullish (undefined or null)
|
||||||
|
const nullish = z.string().nullish();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Error Messages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Field-level messages
|
||||||
|
const username = z.string({
|
||||||
|
required_error: 'Username is required',
|
||||||
|
invalid_type_error: 'Username must be a string',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation-level messages
|
||||||
|
const password = z.string()
|
||||||
|
.min(8, { message: 'Password must be at least 8 characters' })
|
||||||
|
.max(100, { message: 'Password is too long' });
|
||||||
|
|
||||||
|
// Custom refinement with message
|
||||||
|
const ageValidation = z.number()
|
||||||
|
.refine((val) => val >= 18, {
|
||||||
|
message: 'Must be 18 or older',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transformations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Transform to different type
|
||||||
|
const numberFromString = z.string().transform((val) => parseInt(val, 10));
|
||||||
|
|
||||||
|
// Preprocessing
|
||||||
|
const trimmedString = z.preprocess(
|
||||||
|
(val) => typeof val === 'string' ? val.trim() : val,
|
||||||
|
z.string()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chain transforms
|
||||||
|
const normalized = z.string()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.transform((val) => val.replace(/\s+/g, '-'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Conditional Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Dependent fields
|
||||||
|
const formSchema = z.object({
|
||||||
|
hasAddress: z.boolean(),
|
||||||
|
address: z.string().optional(),
|
||||||
|
}).refine(
|
||||||
|
(data) => !data.hasAddress || (data.hasAddress && data.address),
|
||||||
|
{ message: 'Address is required when hasAddress is true', path: ['address'] }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Record/Map Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Record with string keys
|
||||||
|
const metadata = z.record(z.string(), z.any());
|
||||||
|
|
||||||
|
// Record with specific value type
|
||||||
|
const scores = z.record(z.string(), z.number());
|
||||||
|
|
||||||
|
// Map
|
||||||
|
const map = z.map(z.string(), z.number());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recursive Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Self-referencing type (tree structure)
|
||||||
|
interface Category {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
children?: Category[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const categorySchema: z.ZodType<Category> = z.lazy(() =>
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
children: z.array(categorySchema).optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Integration Patterns
|
||||||
|
|
||||||
|
### React Hook Form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
age: z.number().int().positive().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
function MyForm() {
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
console.log(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{/* Form fields */}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Validation Patterns
|
||||||
|
|
||||||
|
### Server Action with Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const createEntitySchema = z.object({
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
type: z.enum(['character', 'location', 'item']),
|
||||||
|
description: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createEntity(formData: FormData) {
|
||||||
|
const rawData = {
|
||||||
|
name: formData.get('name'),
|
||||||
|
type: formData.get('type'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate with safeParse
|
||||||
|
const result = createEntitySchema.safeParse(rawData);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: result.error.flatten().fieldErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe data
|
||||||
|
const { name, type, description } = result.data;
|
||||||
|
|
||||||
|
// Process validated data
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return { success: true, data: { id: '123' } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Route with Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const requestSchema = z.object({
|
||||||
|
query: z.string(),
|
||||||
|
filters: z.object({
|
||||||
|
type: z.array(z.string()).optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
}).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
const result = requestSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid request', details: result.error.format() },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { query, filters } = result.data;
|
||||||
|
|
||||||
|
// Process validated data
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return NextResponse.json({ results: [] });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Worldbuilding-Specific Patterns
|
||||||
|
|
||||||
|
### Entity Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const entitySchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
type: z.enum(['character', 'location', 'item', 'faction', 'event']),
|
||||||
|
name: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(5000).optional(),
|
||||||
|
tags: z.array(z.string()).max(20),
|
||||||
|
attributes: z.record(z.string(), z.any()),
|
||||||
|
metadata: z.object({
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
createdBy: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relationship Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const relationshipSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
sourceId: z.string().uuid(),
|
||||||
|
targetId: z.string().uuid(),
|
||||||
|
type: z.string().min(1).max(50),
|
||||||
|
strength: z.number().min(0).max(1).optional(),
|
||||||
|
metadata: z.record(z.string(), z.any()).optional(),
|
||||||
|
}).refine(
|
||||||
|
(data) => data.sourceId !== data.targetId,
|
||||||
|
{ message: 'Source and target must be different entities', path: ['targetId'] }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline Event Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const timelineEventSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().min(1).max(200),
|
||||||
|
date: z.object({
|
||||||
|
year: z.number().int(),
|
||||||
|
month: z.number().int().min(1).max(12).optional(),
|
||||||
|
day: z.number().int().min(1).max(31).optional(),
|
||||||
|
}),
|
||||||
|
description: z.string().max(5000).optional(),
|
||||||
|
entityIds: z.array(z.string().uuid()),
|
||||||
|
}).refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.date.day && !data.date.month) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{ message: 'Month is required when day is specified', path: ['date', 'month'] }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use `.safeParse()` over `.parse()`**: Prevents throwing exceptions
|
||||||
|
2. **Provide clear error messages**: Help users understand what went wrong
|
||||||
|
3. **Validate early**: At API boundaries and form submissions
|
||||||
|
4. **Use type inference**: `z.infer<typeof schema>` for TypeScript types
|
||||||
|
5. **Keep schemas colocated**: Near the code that uses them
|
||||||
|
6. **Document complex validations**: Add comments explaining business rules
|
||||||
|
7. **Test edge cases**: Empty values, boundary conditions, invalid formats
|
||||||
|
8. **Use refinements for complex logic**: When built-in validators aren't enough
|
||||||
|
9. **Consider performance**: Complex validations on large datasets may be slow
|
||||||
|
10. **Version your schemas**: When APIs evolve, maintain backward compatibility
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate Zod schemas from TypeScript interfaces/types.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python generate_zod_schema.py --input types/entities.ts --output schemas/entities.ts
|
||||||
|
python generate_zod_schema.py --input-string "interface User { name: string; age: number; }"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class TypeScriptToZodConverter:
|
||||||
|
"""Convert TypeScript types to Zod schemas."""
|
||||||
|
|
||||||
|
# Type mappings from TS to Zod
|
||||||
|
TYPE_MAP = {
|
||||||
|
'string': 'z.string()',
|
||||||
|
'number': 'z.number()',
|
||||||
|
'boolean': 'z.boolean()',
|
||||||
|
'Date': 'z.date()',
|
||||||
|
'any': 'z.any()',
|
||||||
|
'unknown': 'z.unknown()',
|
||||||
|
'null': 'z.null()',
|
||||||
|
'undefined': 'z.undefined()',
|
||||||
|
'void': 'z.void()',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.interfaces: Dict[str, str] = {}
|
||||||
|
self.types: Dict[str, str] = {}
|
||||||
|
self.enums: Dict[str, List[str]] = {}
|
||||||
|
|
||||||
|
def convert_type(self, ts_type: str, optional: bool = False) -> str:
|
||||||
|
"""Convert a TypeScript type to Zod schema."""
|
||||||
|
ts_type = ts_type.strip()
|
||||||
|
|
||||||
|
# Handle optional (?)
|
||||||
|
if ts_type.endswith('?'):
|
||||||
|
ts_type = ts_type[:-1].strip()
|
||||||
|
optional = True
|
||||||
|
|
||||||
|
# Handle union with undefined/null
|
||||||
|
if ' | undefined' in ts_type or ' | null' in ts_type:
|
||||||
|
optional = True
|
||||||
|
ts_type = re.sub(r'\s*\|\s*(undefined|null)', '', ts_type).strip()
|
||||||
|
|
||||||
|
# Simple types
|
||||||
|
if ts_type in self.TYPE_MAP:
|
||||||
|
zod = self.TYPE_MAP[ts_type]
|
||||||
|
# Array types
|
||||||
|
elif ts_type.endswith('[]'):
|
||||||
|
inner_type = ts_type[:-2].strip()
|
||||||
|
inner_zod = self.convert_type(inner_type)
|
||||||
|
zod = f'z.array({inner_zod})'
|
||||||
|
# Generic Array<T>
|
||||||
|
elif ts_type.startswith('Array<') and ts_type.endswith('>'):
|
||||||
|
inner_type = ts_type[6:-1].strip()
|
||||||
|
inner_zod = self.convert_type(inner_type)
|
||||||
|
zod = f'z.array({inner_zod})'
|
||||||
|
# Record<K, V>
|
||||||
|
elif ts_type.startswith('Record<'):
|
||||||
|
match = re.match(r'Record<\s*(.+?)\s*,\s*(.+?)\s*>', ts_type)
|
||||||
|
if match:
|
||||||
|
value_type = match.group(2)
|
||||||
|
value_zod = self.convert_type(value_type)
|
||||||
|
zod = f'z.record({value_zod})'
|
||||||
|
else:
|
||||||
|
zod = 'z.record(z.any())'
|
||||||
|
# Union types
|
||||||
|
elif ' | ' in ts_type:
|
||||||
|
union_types = [t.strip() for t in ts_type.split('|')]
|
||||||
|
union_zods = [self.convert_type(t) for t in union_types]
|
||||||
|
zod = f'z.union([{", ".join(union_zods)}])'
|
||||||
|
# Literal types
|
||||||
|
elif ts_type.startswith("'") or ts_type.startswith('"'):
|
||||||
|
zod = f'z.literal({ts_type})'
|
||||||
|
# Reference to another interface/type/enum
|
||||||
|
elif ts_type in self.interfaces or ts_type in self.types or ts_type in self.enums:
|
||||||
|
zod = f'{ts_type}Schema'
|
||||||
|
else:
|
||||||
|
# Unknown type - use z.any() with comment
|
||||||
|
zod = f'z.any() /* TODO: define schema for {ts_type} */'
|
||||||
|
|
||||||
|
# Add optional modifier
|
||||||
|
if optional:
|
||||||
|
zod = f'{zod}.optional()'
|
||||||
|
|
||||||
|
return zod
|
||||||
|
|
||||||
|
def parse_interface(self, interface_str: str) -> Tuple[str, List[Tuple[str, str, bool, Optional[str]]]]:
|
||||||
|
"""Parse a TypeScript interface and return name and fields."""
|
||||||
|
# Extract interface name
|
||||||
|
name_match = re.search(r'interface\s+(\w+)', interface_str)
|
||||||
|
if not name_match:
|
||||||
|
raise ValueError("Could not parse interface name")
|
||||||
|
|
||||||
|
name = name_match.group(1)
|
||||||
|
|
||||||
|
# Extract fields
|
||||||
|
fields = []
|
||||||
|
field_pattern = re.compile(
|
||||||
|
r'^\s*(?:/\*\*\s*(.+?)\s*\*/\s*)?' # Optional JSDoc comment
|
||||||
|
r'(\w+)(\??)\s*:\s*(.+?);?\s*$', # fieldName?: type;
|
||||||
|
re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
body_match = re.search(r'\{(.+?)\}', interface_str, re.DOTALL)
|
||||||
|
if body_match:
|
||||||
|
body = body_match.group(1)
|
||||||
|
for match in field_pattern.finditer(body):
|
||||||
|
description = match.group(1)
|
||||||
|
field_name = match.group(2)
|
||||||
|
optional = match.group(3) == '?'
|
||||||
|
field_type = match.group(4).strip()
|
||||||
|
fields.append((field_name, field_type, optional, description))
|
||||||
|
|
||||||
|
return name, fields
|
||||||
|
|
||||||
|
def parse_type_alias(self, type_str: str) -> Tuple[str, str]:
|
||||||
|
"""Parse a TypeScript type alias."""
|
||||||
|
match = re.search(r'type\s+(\w+)\s*=\s*(.+?);?\s*$', type_str, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
raise ValueError("Could not parse type alias")
|
||||||
|
|
||||||
|
name = match.group(1)
|
||||||
|
type_def = match.group(2).strip().rstrip(';')
|
||||||
|
return name, type_def
|
||||||
|
|
||||||
|
def parse_enum(self, enum_str: str) -> Tuple[str, List[str]]:
|
||||||
|
"""Parse a TypeScript enum."""
|
||||||
|
name_match = re.search(r'enum\s+(\w+)', enum_str)
|
||||||
|
if not name_match:
|
||||||
|
raise ValueError("Could not parse enum name")
|
||||||
|
|
||||||
|
name = name_match.group(1)
|
||||||
|
|
||||||
|
# Extract enum values
|
||||||
|
values = []
|
||||||
|
body_match = re.search(r'\{(.+?)\}', enum_str, re.DOTALL)
|
||||||
|
if body_match:
|
||||||
|
body = body_match.group(1)
|
||||||
|
value_pattern = re.compile(r'(\w+)\s*(?:=\s*["\'](.+?)["\']\s*)?', re.MULTILINE)
|
||||||
|
for match in value_pattern.finditer(body):
|
||||||
|
value = match.group(2) if match.group(2) else match.group(1)
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
return name, values
|
||||||
|
|
||||||
|
def interface_to_zod(self, name: str, fields: List[Tuple[str, str, bool, Optional[str]]]) -> str:
|
||||||
|
"""Convert interface fields to Zod schema."""
|
||||||
|
schema_lines = [f'export const {name}Schema = z.object({{']
|
||||||
|
|
||||||
|
for field_name, field_type, optional, description in fields:
|
||||||
|
zod_type = self.convert_type(field_type, optional)
|
||||||
|
|
||||||
|
# Add description if present
|
||||||
|
if description:
|
||||||
|
desc_line = f' {field_name}: {zod_type}.describe("{description}"),'
|
||||||
|
else:
|
||||||
|
desc_line = f' {field_name}: {zod_type},'
|
||||||
|
|
||||||
|
schema_lines.append(desc_line)
|
||||||
|
|
||||||
|
schema_lines.append('});')
|
||||||
|
schema_lines.append('')
|
||||||
|
schema_lines.append(f'export type {name} = z.infer<typeof {name}Schema>;')
|
||||||
|
|
||||||
|
return '\n'.join(schema_lines)
|
||||||
|
|
||||||
|
def type_alias_to_zod(self, name: str, type_def: str) -> str:
|
||||||
|
"""Convert type alias to Zod schema."""
|
||||||
|
zod_type = self.convert_type(type_def)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f'export const {name}Schema = {zod_type};',
|
||||||
|
'',
|
||||||
|
f'export type {name} = z.infer<typeof {name}Schema>;'
|
||||||
|
]
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def enum_to_zod(self, name: str, values: List[str]) -> str:
|
||||||
|
"""Convert enum to Zod schema."""
|
||||||
|
quoted_values = [f'"{v}"' for v in values]
|
||||||
|
|
||||||
|
if len(values) == 1:
|
||||||
|
zod_def = f'z.literal({quoted_values[0]})'
|
||||||
|
else:
|
||||||
|
zod_def = f'z.enum([{", ".join(quoted_values)}])'
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f'export const {name}Schema = {zod_def};',
|
||||||
|
'',
|
||||||
|
f'export type {name} = z.infer<typeof {name}Schema>;'
|
||||||
|
]
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def convert_file(self, input_content: str) -> str:
|
||||||
|
"""Convert an entire TypeScript file to Zod schemas."""
|
||||||
|
output_lines = [
|
||||||
|
"import { z } from 'zod';",
|
||||||
|
'',
|
||||||
|
'// Auto-generated Zod schemas',
|
||||||
|
''
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parse all interfaces
|
||||||
|
interface_pattern = re.compile(
|
||||||
|
r'(?:export\s+)?interface\s+\w+\s*\{[^}]*\}',
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for match in interface_pattern.finditer(input_content):
|
||||||
|
interface_str = match.group(0)
|
||||||
|
try:
|
||||||
|
name, fields = self.parse_interface(interface_str)
|
||||||
|
self.interfaces[name] = interface_str
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parse all type aliases
|
||||||
|
type_pattern = re.compile(
|
||||||
|
r'(?:export\s+)?type\s+\w+\s*=\s*[^;]+;?',
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for match in type_pattern.finditer(input_content):
|
||||||
|
type_str = match.group(0)
|
||||||
|
try:
|
||||||
|
name, type_def = self.parse_type_alias(type_str)
|
||||||
|
self.types[name] = type_def
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Parse all enums
|
||||||
|
enum_pattern = re.compile(
|
||||||
|
r'(?:export\s+)?enum\s+\w+\s*\{[^}]*\}',
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for match in enum_pattern.finditer(input_content):
|
||||||
|
enum_str = match.group(0)
|
||||||
|
try:
|
||||||
|
name, values = self.parse_enum(enum_str)
|
||||||
|
self.enums[name] = values
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Generate Zod schemas
|
||||||
|
# Enums first (no dependencies)
|
||||||
|
for name, values in self.enums.items():
|
||||||
|
output_lines.append(self.enum_to_zod(name, values))
|
||||||
|
output_lines.append('')
|
||||||
|
|
||||||
|
# Then type aliases
|
||||||
|
for name, type_def in self.types.items():
|
||||||
|
output_lines.append(self.type_alias_to_zod(name, type_def))
|
||||||
|
output_lines.append('')
|
||||||
|
|
||||||
|
# Finally interfaces
|
||||||
|
for name, interface_str in self.interfaces.items():
|
||||||
|
_, fields = self.parse_interface(interface_str)
|
||||||
|
output_lines.append(self.interface_to_zod(name, fields))
|
||||||
|
output_lines.append('')
|
||||||
|
|
||||||
|
return '\n'.join(output_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Generate Zod schemas from TypeScript interfaces/types'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--input',
|
||||||
|
help='Input TypeScript file path'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
help='Output file path for Zod schemas'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--input-string',
|
||||||
|
help='Input TypeScript code as string'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Read input
|
||||||
|
if args.input:
|
||||||
|
input_path = Path(args.input)
|
||||||
|
if not input_path.exists():
|
||||||
|
print(f"Error: Input file not found: {args.input}")
|
||||||
|
return 1
|
||||||
|
input_content = input_path.read_text(encoding='utf-8')
|
||||||
|
elif args.input_string:
|
||||||
|
input_content = args.input_string
|
||||||
|
else:
|
||||||
|
print("Error: Either --input or --input-string must be provided")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Convert
|
||||||
|
converter = TypeScriptToZodConverter()
|
||||||
|
try:
|
||||||
|
output_content = converter.convert_file(input_content)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during conversion: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Write output
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(output_content, encoding='utf-8')
|
||||||
|
print(f"Generated Zod schemas written to: {args.output}")
|
||||||
|
else:
|
||||||
|
print(output_content)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
Reference in New Issue
Block a user