Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "api-schema-validator",
|
||||||
|
"description": "Validate API schemas with JSON Schema, Joi, Yup, or Zod",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Jeremy Longshore",
|
||||||
|
"email": "[email protected]"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# api-schema-validator
|
||||||
|
|
||||||
|
Validate API schemas with JSON Schema, Joi, Yup, or Zod
|
||||||
598
commands/validate-schemas.md
Normal file
598
commands/validate-schemas.md
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
---
|
||||||
|
description: Validate API schemas
|
||||||
|
shortcut: schema
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validate API Schemas
|
||||||
|
|
||||||
|
Implement comprehensive schema validation using modern validation libraries like JSON Schema, Joi, Yup, or Zod to ensure type safety, data integrity, and contract compliance across your API.
|
||||||
|
|
||||||
|
## When to Use This Command
|
||||||
|
|
||||||
|
Use `/validate-schemas` when you need to:
|
||||||
|
- Enforce strict data types and formats in API requests and responses
|
||||||
|
- Create reusable validation schemas across multiple endpoints
|
||||||
|
- Generate TypeScript types from validation schemas
|
||||||
|
- Implement complex conditional validation logic
|
||||||
|
- Provide detailed validation error messages to clients
|
||||||
|
- Ensure data consistency before database operations
|
||||||
|
|
||||||
|
DON'T use this when:
|
||||||
|
- Working with unstructured or highly dynamic data (use runtime checks instead)
|
||||||
|
- Building quick prototypes without formal contracts (premature optimization)
|
||||||
|
- Validation logic is trivial (simple type checks may suffice)
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
This command implements **Zod** as the primary approach because:
|
||||||
|
- TypeScript-first with automatic type inference
|
||||||
|
- Composable schemas with chaining syntax
|
||||||
|
- Zero runtime dependencies
|
||||||
|
- Excellent error messages out of the box
|
||||||
|
- Schema transformation and parsing capabilities
|
||||||
|
- Works seamlessly with modern frameworks
|
||||||
|
|
||||||
|
**Alternative considered: Joi**
|
||||||
|
- More mature with extensive ecosystem
|
||||||
|
- Better for JavaScript-only projects
|
||||||
|
- More verbose API
|
||||||
|
- Recommended for legacy Node.js applications
|
||||||
|
|
||||||
|
**Alternative considered: JSON Schema**
|
||||||
|
- Language-agnostic standard
|
||||||
|
- Better for cross-platform validation
|
||||||
|
- More complex to write and maintain
|
||||||
|
- Recommended when sharing schemas across different languages
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before running this command:
|
||||||
|
1. Choose validation library based on your tech stack
|
||||||
|
2. Define validation requirements for each endpoint
|
||||||
|
3. Plan error response format
|
||||||
|
4. Consider performance impact for complex schemas
|
||||||
|
5. Determine validation strategy (fail-fast vs. collect-all-errors)
|
||||||
|
|
||||||
|
## Implementation Process
|
||||||
|
|
||||||
|
### Step 1: Define Base Schemas
|
||||||
|
Create reusable schema primitives for common data types and patterns.
|
||||||
|
|
||||||
|
### Step 2: Compose Endpoint Schemas
|
||||||
|
Build complex schemas by composing base schemas with business rules.
|
||||||
|
|
||||||
|
### Step 3: Integrate with Middleware
|
||||||
|
Set up validation middleware to automatically validate requests and responses.
|
||||||
|
|
||||||
|
### Step 4: Generate Types
|
||||||
|
Generate TypeScript types from schemas for compile-time safety.
|
||||||
|
|
||||||
|
### Step 5: Implement Error Handling
|
||||||
|
Create consistent error formatting and reporting mechanisms.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
The command generates:
|
||||||
|
- `schemas/` - Schema definitions organized by domain
|
||||||
|
- `validators/` - Compiled validation functions
|
||||||
|
- `types/` - Generated TypeScript types from schemas
|
||||||
|
- `middleware/validation.ts` - Request/response validation middleware
|
||||||
|
- `tests/schema.test.ts` - Schema validation test suites
|
||||||
|
- `docs/validation-rules.md` - Documentation of all validation rules
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Example 1: Zod Schema Validation (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// schemas/user.schema.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Base schemas for reuse
|
||||||
|
const emailSchema = z.string().email().toLowerCase();
|
||||||
|
const passwordSchema = z.string()
|
||||||
|
.min(8, 'Password must be at least 8 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain number')
|
||||||
|
.regex(/[^A-Za-z0-9]/, 'Password must contain special character');
|
||||||
|
|
||||||
|
const phoneSchema = z.string().regex(
|
||||||
|
/^\+?[1-9]\d{1,14}$/,
|
||||||
|
'Invalid phone number format (E.164)'
|
||||||
|
);
|
||||||
|
|
||||||
|
const addressSchema = z.object({
|
||||||
|
street: z.string().min(1).max(100),
|
||||||
|
city: z.string().min(1).max(50),
|
||||||
|
state: z.string().length(2).toUpperCase(),
|
||||||
|
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
|
||||||
|
country: z.string().length(2).toUpperCase().default('US')
|
||||||
|
});
|
||||||
|
|
||||||
|
// User schemas
|
||||||
|
export const createUserSchema = z.object({
|
||||||
|
body: z.object({
|
||||||
|
email: emailSchema,
|
||||||
|
password: passwordSchema,
|
||||||
|
firstName: z.string().min(1).max(50),
|
||||||
|
lastName: z.string().min(1).max(50),
|
||||||
|
dateOfBirth: z.string().datetime().refine(
|
||||||
|
(date) => {
|
||||||
|
const age = new Date().getFullYear() - new Date(date).getFullYear();
|
||||||
|
return age >= 18;
|
||||||
|
},
|
||||||
|
{ message: 'Must be at least 18 years old' }
|
||||||
|
),
|
||||||
|
phone: phoneSchema.optional(),
|
||||||
|
address: addressSchema.optional(),
|
||||||
|
preferences: z.object({
|
||||||
|
newsletter: z.boolean().default(false),
|
||||||
|
notifications: z.enum(['email', 'sms', 'push', 'none']).default('email'),
|
||||||
|
theme: z.enum(['light', 'dark', 'auto']).default('auto')
|
||||||
|
}).default({}),
|
||||||
|
metadata: z.record(z.string(), z.any()).optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateUserSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: createUserSchema.shape.body.partial().extend({
|
||||||
|
// Can't update email without verification
|
||||||
|
email: z.never().optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUserSchema = z.object({
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listUsersSchema = z.object({
|
||||||
|
query: z.object({
|
||||||
|
page: z.coerce.number().int().positive().default(1),
|
||||||
|
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||||
|
sort: z.enum(['createdAt', 'email', 'name']).default('createdAt'),
|
||||||
|
order: z.enum(['asc', 'desc']).default('desc'),
|
||||||
|
filter: z.object({
|
||||||
|
email: z.string().optional(),
|
||||||
|
status: z.enum(['active', 'inactive', 'suspended']).optional(),
|
||||||
|
createdAfter: z.string().datetime().optional(),
|
||||||
|
createdBefore: z.string().datetime().optional()
|
||||||
|
}).optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type inference
|
||||||
|
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
||||||
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||||
|
export type GetUserInput = z.infer<typeof getUserSchema>;
|
||||||
|
export type ListUsersInput = z.infer<typeof listUsersSchema>;
|
||||||
|
|
||||||
|
// Validation middleware
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function validate(schema: z.ZodSchema) {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const validated = await schema.parseAsync({
|
||||||
|
body: req.body,
|
||||||
|
query: req.query,
|
||||||
|
params: req.params
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace request properties with validated/transformed data
|
||||||
|
req.body = validated.body || req.body;
|
||||||
|
req.query = validated.query || req.query;
|
||||||
|
req.params = validated.params || req.params;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.errors.map(err => ({
|
||||||
|
path: err.path.join('.'),
|
||||||
|
message: err.message,
|
||||||
|
code: err.code
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in routes
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/users',
|
||||||
|
validate(createUserSchema),
|
||||||
|
async (req, res) => {
|
||||||
|
// req.body is now typed and validated
|
||||||
|
const user = await createUser(req.body);
|
||||||
|
res.json(user);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.put('/users/:id',
|
||||||
|
validate(updateUserSchema),
|
||||||
|
async (req, res) => {
|
||||||
|
const user = await updateUser(req.params.id, req.body);
|
||||||
|
res.json(user);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Joi Schema Validation (JavaScript)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// schemas/product.schema.js
|
||||||
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
// Custom validators
|
||||||
|
const customValidators = {
|
||||||
|
sku: Joi.string().pattern(/^[A-Z]{3}-\d{6}$/),
|
||||||
|
price: Joi.number().precision(2).positive().max(999999.99),
|
||||||
|
url: Joi.string().uri({ scheme: ['http', 'https'] })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Product schemas
|
||||||
|
const productSchema = {
|
||||||
|
create: Joi.object({
|
||||||
|
sku: customValidators.sku.required(),
|
||||||
|
name: Joi.string().min(3).max(200).required(),
|
||||||
|
description: Joi.string().max(2000).required(),
|
||||||
|
price: customValidators.price.required(),
|
||||||
|
compareAtPrice: customValidators.price
|
||||||
|
.greater(Joi.ref('price'))
|
||||||
|
.optional(),
|
||||||
|
category: Joi.string().valid(
|
||||||
|
'electronics',
|
||||||
|
'clothing',
|
||||||
|
'food',
|
||||||
|
'books',
|
||||||
|
'other'
|
||||||
|
).required(),
|
||||||
|
tags: Joi.array()
|
||||||
|
.items(Joi.string().min(2).max(20))
|
||||||
|
.max(10)
|
||||||
|
.unique(),
|
||||||
|
inventory: Joi.object({
|
||||||
|
quantity: Joi.number().integer().min(0).required(),
|
||||||
|
trackInventory: Joi.boolean().default(true),
|
||||||
|
allowBackorder: Joi.boolean().default(false),
|
||||||
|
lowStockThreshold: Joi.number().integer().min(0).default(10)
|
||||||
|
}).required(),
|
||||||
|
images: Joi.array()
|
||||||
|
.items(Joi.object({
|
||||||
|
url: customValidators.url.required(),
|
||||||
|
alt: Joi.string().max(200),
|
||||||
|
isPrimary: Joi.boolean().default(false)
|
||||||
|
}))
|
||||||
|
.min(1)
|
||||||
|
.max(10)
|
||||||
|
.unique('url')
|
||||||
|
.required(),
|
||||||
|
shipping: Joi.object({
|
||||||
|
weight: Joi.number().positive().required(),
|
||||||
|
dimensions: Joi.object({
|
||||||
|
length: Joi.number().positive().required(),
|
||||||
|
width: Joi.number().positive().required(),
|
||||||
|
height: Joi.number().positive().required()
|
||||||
|
}).required(),
|
||||||
|
requiresShipping: Joi.boolean().default(true),
|
||||||
|
shippingClass: Joi.string().valid('standard', 'fragile', 'oversized')
|
||||||
|
}).when('category', {
|
||||||
|
is: 'electronics',
|
||||||
|
then: Joi.required(),
|
||||||
|
otherwise: Joi.optional()
|
||||||
|
}),
|
||||||
|
variants: Joi.array()
|
||||||
|
.items(Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
options: Joi.array()
|
||||||
|
.items(Joi.string())
|
||||||
|
.min(1)
|
||||||
|
.required()
|
||||||
|
}))
|
||||||
|
.optional(),
|
||||||
|
metadata: Joi.object().pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.alternatives().try(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.number(),
|
||||||
|
Joi.boolean()
|
||||||
|
)
|
||||||
|
).optional()
|
||||||
|
}).custom((value, helpers) => {
|
||||||
|
// Custom validation: ensure at least one primary image
|
||||||
|
const primaryImages = value.images.filter(img => img.isPrimary);
|
||||||
|
if (primaryImages.length !== 1) {
|
||||||
|
return helpers.error('any.invalid', {
|
||||||
|
message: 'Exactly one image must be marked as primary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: Joi.object({
|
||||||
|
name: Joi.string().min(3).max(200),
|
||||||
|
description: Joi.string().max(2000),
|
||||||
|
price: customValidators.price,
|
||||||
|
// ... partial schema
|
||||||
|
}).min(1), // At least one field required
|
||||||
|
|
||||||
|
list: Joi.object({
|
||||||
|
page: Joi.number().integer().positive().default(1),
|
||||||
|
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||||
|
category: Joi.string(),
|
||||||
|
minPrice: Joi.number().positive(),
|
||||||
|
maxPrice: Joi.number().positive().greater(Joi.ref('minPrice')),
|
||||||
|
search: Joi.string().max(100),
|
||||||
|
inStock: Joi.boolean()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation middleware
|
||||||
|
function validateRequest(schemaName) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const schema = productSchema[schemaName];
|
||||||
|
if (!schema) {
|
||||||
|
return next(new Error(`Schema ${schemaName} not found`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataToValidate = {
|
||||||
|
...req.body,
|
||||||
|
...req.query,
|
||||||
|
...req.params
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validated = await schema.validateAsync(dataToValidate, {
|
||||||
|
abortEarly: false, // Return all errors
|
||||||
|
stripUnknown: true, // Remove unknown keys
|
||||||
|
convert: true // Type coercion
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge validated data back
|
||||||
|
Object.keys(validated).forEach(key => {
|
||||||
|
if (req.body.hasOwnProperty(key)) req.body[key] = validated[key];
|
||||||
|
if (req.query.hasOwnProperty(key)) req.query[key] = validated[key];
|
||||||
|
if (req.params.hasOwnProperty(key)) req.params[key] = validated[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.isJoi) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: error.details.map(detail => ({
|
||||||
|
field: detail.path.join('.'),
|
||||||
|
message: detail.message,
|
||||||
|
type: detail.type
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
productSchema,
|
||||||
|
validateRequest
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Pydantic Schema Validation (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# schemas/order_schema.py
|
||||||
|
from pydantic import BaseModel, Field, validator, root_validator
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from datetime import datetime, date
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
|
||||||
|
class OrderStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
PROCESSING = "processing"
|
||||||
|
SHIPPED = "shipped"
|
||||||
|
DELIVERED = "delivered"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
class PaymentMethod(str, Enum):
|
||||||
|
CREDIT_CARD = "credit_card"
|
||||||
|
DEBIT_CARD = "debit_card"
|
||||||
|
PAYPAL = "paypal"
|
||||||
|
STRIPE = "stripe"
|
||||||
|
BANK_TRANSFER = "bank_transfer"
|
||||||
|
|
||||||
|
class Address(BaseModel):
|
||||||
|
street: str = Field(..., min_length=1, max_length=200)
|
||||||
|
city: str = Field(..., min_length=1, max_length=100)
|
||||||
|
state: str = Field(..., regex="^[A-Z]{2}$")
|
||||||
|
zip_code: str = Field(..., regex=r"^\d{5}(-\d{4})?$")
|
||||||
|
country: str = Field(default="US", regex="^[A-Z]{2}$")
|
||||||
|
|
||||||
|
@validator('state', 'country')
|
||||||
|
def uppercase_codes(cls, v):
|
||||||
|
return v.upper()
|
||||||
|
|
||||||
|
class OrderItem(BaseModel):
|
||||||
|
product_id: str = Field(..., regex="^[A-Z]{3}-\d{6}$")
|
||||||
|
quantity: int = Field(..., gt=0, le=100)
|
||||||
|
unit_price: Decimal = Field(..., decimal_places=2, ge=0)
|
||||||
|
discount: Optional[Decimal] = Field(None, decimal_places=2, ge=0, le=1)
|
||||||
|
|
||||||
|
@validator('discount')
|
||||||
|
def validate_discount(cls, v, values):
|
||||||
|
if v and v >= 1:
|
||||||
|
raise ValueError('Discount must be less than 100%')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtotal(self) -> Decimal:
|
||||||
|
discount_amount = self.discount or Decimal('0')
|
||||||
|
return self.quantity * self.unit_price * (1 - discount_amount)
|
||||||
|
|
||||||
|
class CreateOrderSchema(BaseModel):
|
||||||
|
customer_email: str = Field(..., regex=r"^[\w\.-]+@[\w\.-]+\.\w+$")
|
||||||
|
items: List[OrderItem] = Field(..., min_items=1, max_items=50)
|
||||||
|
shipping_address: Address
|
||||||
|
billing_address: Optional[Address] = None
|
||||||
|
payment_method: PaymentMethod
|
||||||
|
notes: Optional[str] = Field(None, max_length=500)
|
||||||
|
coupon_code: Optional[str] = Field(None, regex="^[A-Z0-9]{4,12}$")
|
||||||
|
|
||||||
|
@validator('customer_email')
|
||||||
|
def validate_email(cls, v):
|
||||||
|
# Additional email validation
|
||||||
|
if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", v.lower()):
|
||||||
|
raise ValueError('Invalid email format')
|
||||||
|
return v.lower()
|
||||||
|
|
||||||
|
@validator('billing_address', always=True)
|
||||||
|
def set_billing_address(cls, v, values):
|
||||||
|
# Use shipping address if billing not provided
|
||||||
|
return v or values.get('shipping_address')
|
||||||
|
|
||||||
|
@root_validator
|
||||||
|
def validate_order(cls, values):
|
||||||
|
items = values.get('items', [])
|
||||||
|
|
||||||
|
# Check for duplicate products
|
||||||
|
product_ids = [item.product_id for item in items]
|
||||||
|
if len(product_ids) != len(set(product_ids)):
|
||||||
|
raise ValueError('Duplicate products in order')
|
||||||
|
|
||||||
|
# Calculate total
|
||||||
|
total = sum(item.subtotal for item in items)
|
||||||
|
if total <= 0:
|
||||||
|
raise ValueError('Order total must be positive')
|
||||||
|
|
||||||
|
# Validate coupon if provided
|
||||||
|
coupon = values.get('coupon_code')
|
||||||
|
if coupon and not cls.validate_coupon(coupon):
|
||||||
|
raise ValueError('Invalid coupon code')
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_coupon(code: str) -> bool:
|
||||||
|
# Implement coupon validation logic
|
||||||
|
valid_coupons = ['SAVE10', 'FREESHIP', 'WELCOME20']
|
||||||
|
return code in valid_coupons
|
||||||
|
|
||||||
|
class UpdateOrderSchema(BaseModel):
|
||||||
|
status: Optional[OrderStatus] = None
|
||||||
|
shipping_address: Optional[Address] = None
|
||||||
|
notes: Optional[str] = Field(None, max_length=500)
|
||||||
|
tracking_number: Optional[str] = Field(None, regex=r"^[A-Z0-9]{10,30}$")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
use_enum_values = True
|
||||||
|
|
||||||
|
# FastAPI integration
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/orders", response_model=Dict[str, Any])
|
||||||
|
async def create_order(order: CreateOrderSchema):
|
||||||
|
# Validation happens automatically
|
||||||
|
order_dict = jsonable_encoder(order)
|
||||||
|
# Process order
|
||||||
|
return {"id": "ORD-123456", **order_dict}
|
||||||
|
|
||||||
|
@app.patch("/orders/{order_id}")
|
||||||
|
async def update_order(
|
||||||
|
order_id: str,
|
||||||
|
updates: UpdateOrderSchema
|
||||||
|
):
|
||||||
|
# Only provided fields will be updated
|
||||||
|
update_dict = updates.dict(exclude_unset=True)
|
||||||
|
# Update order
|
||||||
|
return {"id": order_id, **update_dict}
|
||||||
|
|
||||||
|
# Custom validation endpoint
|
||||||
|
@app.post("/validate/order")
|
||||||
|
async def validate_order(order: CreateOrderSchema):
|
||||||
|
# Just validate without processing
|
||||||
|
return {"valid": True, "data": order.dict()}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| "Invalid type" | Wrong data type provided | Check schema definition and input data |
|
||||||
|
| "Required field missing" | Mandatory field not provided | Ensure all required fields are present |
|
||||||
|
| "Validation failed" | Business rule violation | Review custom validators and constraints |
|
||||||
|
| "Schema not found" | Referenced schema doesn't exist | Verify schema imports and definitions |
|
||||||
|
| "Circular dependency" | Schema references itself | Refactor to break circular references |
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
**Validation Strategies**
|
||||||
|
- `fail-fast`: Stop at first error (faster)
|
||||||
|
- `collect-all`: Gather all errors (better UX)
|
||||||
|
- `partial`: Allow partial validation for updates
|
||||||
|
|
||||||
|
**Type Coercion**
|
||||||
|
- `strict`: No type conversion
|
||||||
|
- `loose`: Attempt type conversion
|
||||||
|
- `smart`: Context-aware conversion
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
DO:
|
||||||
|
- Create reusable base schemas for common patterns
|
||||||
|
- Use descriptive error messages for custom validators
|
||||||
|
- Generate TypeScript types from schemas
|
||||||
|
- Version schemas with your API
|
||||||
|
- Document all validation rules
|
||||||
|
- Test edge cases and boundary conditions
|
||||||
|
|
||||||
|
DON'T:
|
||||||
|
- Duplicate validation logic across layers
|
||||||
|
- Use overly complex nested schemas
|
||||||
|
- Ignore performance impact of complex validations
|
||||||
|
- Mix validation with business logic
|
||||||
|
- Trust client-side validation alone
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Compile schemas once at startup
|
||||||
|
- Cache validated results for identical inputs
|
||||||
|
- Use async validation for external checks
|
||||||
|
- Limit regex complexity to prevent ReDoS attacks
|
||||||
|
- Consider schema complexity for large payloads
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Sanitize error messages to prevent information leakage
|
||||||
|
- Implement rate limiting on validation endpoints
|
||||||
|
- Use strict type checking to prevent injection attacks
|
||||||
|
- Validate file uploads separately with size limits
|
||||||
|
- Never expose internal schema structure to clients
|
||||||
|
|
||||||
|
## Related Commands
|
||||||
|
|
||||||
|
- `/api-response-validator` - Validate API responses
|
||||||
|
- `/api-contract-generator` - Generate schemas from code
|
||||||
|
- `/api-testing-framework` - Test with schema validation
|
||||||
|
- `/api-documentation-generator` - Document schemas
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- v1.0.0 (2024-10): Initial implementation with Zod, Joi, and Pydantic support
|
||||||
|
- Planned v1.1.0: Add support for Yup and JSON Schema generation
|
||||||
85
plugin.lock.json
Normal file
85
plugin.lock.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:jeremylongshore/claude-code-plugins-plus:plugins/api-development/api-schema-validator",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "6fe99a526667bc7dd391d56742a23288b3bd1d53",
|
||||||
|
"treeHash": "b459f932e847d68e0e05b35c5895225bc94d064291e4b9f5270a8ef65918b307",
|
||||||
|
"generatedAt": "2025-11-28T10:18:08.393637Z",
|
||||||
|
"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-schema-validator",
|
||||||
|
"description": "Validate API schemas with JSON Schema, Joi, Yup, or Zod",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "964deefdc89c0c6afb1af808195fbe152019a4c1bfffc8f3afc0d73245dcfc44"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "ee709d51ea913c41f0b22736ce467df18dc635d400156c703d512ee4c248c5f1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/validate-schemas.md",
|
||||||
|
"sha256": "4b90ff709dc69abb52eacb253f61603dfe9d00ab397507a8729f789fde9293be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/references/examples.md",
|
||||||
|
"sha256": "922bbc3c4ebf38b76f515b5c1998ebde6bf902233e00e2c5a0e9176f975a7572"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/references/best-practices.md",
|
||||||
|
"sha256": "c8f32b3566252f50daacd346d7045a1060c718ef5cfb07c55a0f2dec5f1fb39e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/references/README.md",
|
||||||
|
"sha256": "792835f31b3849b180d74f135ac6fe79cc3ed93b6c826b2ea62d6d6703724dab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/scripts/helper-template.sh",
|
||||||
|
"sha256": "0881d5660a8a7045550d09ae0acc15642c24b70de6f08808120f47f86ccdf077"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/scripts/validation.sh",
|
||||||
|
"sha256": "92551a29a7f512d2036e4f1fb46c2a3dc6bff0f7dde4a9f699533e446db48502"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/scripts/README.md",
|
||||||
|
"sha256": "f2f6a48cd8eb28031af2c6040a6bf96d1f8fa3a9780e8f4bf884ea16bfd3b89d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/assets/test-data.json",
|
||||||
|
"sha256": "ac17dca3d6e253a5f39f2a2f1b388e5146043756b05d9ce7ac53a0042eee139d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/assets/README.md",
|
||||||
|
"sha256": "784881a613b5da6273956467a368958c1c73d156a8122cbe93d85d62a6c83978"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/assets/skill-schema.json",
|
||||||
|
"sha256": "f5639ba823a24c9ac4fb21444c0717b7aefde1a4993682897f5bf544f863c2cd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-adapter/assets/config-template.json",
|
||||||
|
"sha256": "0c2ba33d2d3c5ccb266c0848fc43caa68a2aa6a80ff315d4b378352711f83e1c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "b459f932e847d68e0e05b35c5895225bc94d064291e4b9f5270a8ef65918b307"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
7
skills/skill-adapter/assets/README.md
Normal file
7
skills/skill-adapter/assets/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Assets
|
||||||
|
|
||||||
|
Bundled resources for api-schema-validator skill
|
||||||
|
|
||||||
|
- [ ] example_schemas/: A directory containing example schemas in various formats (JSON Schema, Joi, Yup, Zod).
|
||||||
|
- [ ] test_data/: A directory containing example data files to validate against the schemas.
|
||||||
|
- [ ] schema_templates/: Templates for creating new schemas in different formats.
|
||||||
32
skills/skill-adapter/assets/config-template.json
Normal file
32
skills/skill-adapter/assets/config-template.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"skill": {
|
||||||
|
"name": "skill-name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"enabled": true,
|
||||||
|
"settings": {
|
||||||
|
"verbose": false,
|
||||||
|
"autoActivate": true,
|
||||||
|
"toolRestrictions": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"keywords": [
|
||||||
|
"example-trigger-1",
|
||||||
|
"example-trigger-2"
|
||||||
|
],
|
||||||
|
"patterns": []
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"allowed": [
|
||||||
|
"Read",
|
||||||
|
"Grep",
|
||||||
|
"Bash"
|
||||||
|
],
|
||||||
|
"restricted": []
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Plugin Author",
|
||||||
|
"category": "general",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
28
skills/skill-adapter/assets/skill-schema.json
Normal file
28
skills/skill-adapter/assets/skill-schema.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Claude Skill Configuration",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "description"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9-]+$",
|
||||||
|
"maxLength": 64,
|
||||||
|
"description": "Skill identifier (lowercase, hyphens only)"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1024,
|
||||||
|
"description": "What the skill does and when to use it"
|
||||||
|
},
|
||||||
|
"allowed-tools": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of allowed tools"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
||||||
|
"description": "Semantic version (x.y.z)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
skills/skill-adapter/assets/test-data.json
Normal file
27
skills/skill-adapter/assets/test-data.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"testCases": [
|
||||||
|
{
|
||||||
|
"name": "Basic activation test",
|
||||||
|
"input": "trigger phrase example",
|
||||||
|
"expected": {
|
||||||
|
"activated": true,
|
||||||
|
"toolsUsed": ["Read", "Grep"],
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Complex workflow test",
|
||||||
|
"input": "multi-step trigger example",
|
||||||
|
"expected": {
|
||||||
|
"activated": true,
|
||||||
|
"steps": 3,
|
||||||
|
"toolsUsed": ["Read", "Write", "Bash"],
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixtures": {
|
||||||
|
"sampleInput": "example data",
|
||||||
|
"expectedOutput": "processed result"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
skills/skill-adapter/references/README.md
Normal file
9
skills/skill-adapter/references/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# References
|
||||||
|
|
||||||
|
Bundled resources for api-schema-validator skill
|
||||||
|
|
||||||
|
- [ ] json_schema_spec.md: A markdown file containing a summary of the JSON Schema specification.
|
||||||
|
- [ ] joi_documentation.md: A markdown file containing key excerpts from the Joi documentation.
|
||||||
|
- [ ] yup_documentation.md: A markdown file containing key excerpts from the Yup documentation.
|
||||||
|
- [ ] zod_documentation.md: A markdown file containing key excerpts from the Zod documentation.
|
||||||
|
- [ ] schema_best_practices.md: A guide to writing effective and maintainable schemas.
|
||||||
69
skills/skill-adapter/references/best-practices.md
Normal file
69
skills/skill-adapter/references/best-practices.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Skill Best Practices
|
||||||
|
|
||||||
|
Guidelines for optimal skill usage and development.
|
||||||
|
|
||||||
|
## For Users
|
||||||
|
|
||||||
|
### Activation Best Practices
|
||||||
|
|
||||||
|
1. **Use Clear Trigger Phrases**
|
||||||
|
- Match phrases from skill description
|
||||||
|
- Be specific about intent
|
||||||
|
- Provide necessary context
|
||||||
|
|
||||||
|
2. **Provide Sufficient Context**
|
||||||
|
- Include relevant file paths
|
||||||
|
- Specify scope of analysis
|
||||||
|
- Mention any constraints
|
||||||
|
|
||||||
|
3. **Understand Tool Permissions**
|
||||||
|
- Check allowed-tools in frontmatter
|
||||||
|
- Know what the skill can/cannot do
|
||||||
|
- Request appropriate actions
|
||||||
|
|
||||||
|
### Workflow Optimization
|
||||||
|
|
||||||
|
- Start with simple requests
|
||||||
|
- Build up to complex workflows
|
||||||
|
- Verify each step before proceeding
|
||||||
|
- Use skill consistently for related tasks
|
||||||
|
|
||||||
|
## For Developers
|
||||||
|
|
||||||
|
### Skill Development Guidelines
|
||||||
|
|
||||||
|
1. **Clear Descriptions**
|
||||||
|
- Include explicit trigger phrases
|
||||||
|
- Document all capabilities
|
||||||
|
- Specify limitations
|
||||||
|
|
||||||
|
2. **Proper Tool Permissions**
|
||||||
|
- Use minimal necessary tools
|
||||||
|
- Document security implications
|
||||||
|
- Test with restricted tools
|
||||||
|
|
||||||
|
3. **Comprehensive Documentation**
|
||||||
|
- Provide usage examples
|
||||||
|
- Document common pitfalls
|
||||||
|
- Include troubleshooting guide
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Keep version updated
|
||||||
|
- Test after tool updates
|
||||||
|
- Monitor user feedback
|
||||||
|
- Iterate on descriptions
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
- Scope skills to specific domains
|
||||||
|
- Avoid overlapping trigger phrases
|
||||||
|
- Keep descriptions under 1024 chars
|
||||||
|
- Test activation reliability
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Never include secrets in skill files
|
||||||
|
- Validate all inputs
|
||||||
|
- Use read-only tools when possible
|
||||||
|
- Document security requirements
|
||||||
70
skills/skill-adapter/references/examples.md
Normal file
70
skills/skill-adapter/references/examples.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Skill Usage Examples
|
||||||
|
|
||||||
|
This document provides practical examples of how to use this skill effectively.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Example 1: Simple Activation
|
||||||
|
|
||||||
|
**User Request:**
|
||||||
|
```
|
||||||
|
[Describe trigger phrase here]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill Response:**
|
||||||
|
1. Analyzes the request
|
||||||
|
2. Performs the required action
|
||||||
|
3. Returns results
|
||||||
|
|
||||||
|
### Example 2: Complex Workflow
|
||||||
|
|
||||||
|
**User Request:**
|
||||||
|
```
|
||||||
|
[Describe complex scenario]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
1. Step 1: Initial analysis
|
||||||
|
2. Step 2: Data processing
|
||||||
|
3. Step 3: Result generation
|
||||||
|
4. Step 4: Validation
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Chaining Operations
|
||||||
|
|
||||||
|
Combine this skill with other tools:
|
||||||
|
```
|
||||||
|
Step 1: Use this skill for [purpose]
|
||||||
|
Step 2: Chain with [other tool]
|
||||||
|
Step 3: Finalize with [action]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Error Handling
|
||||||
|
|
||||||
|
If issues occur:
|
||||||
|
- Check trigger phrase matches
|
||||||
|
- Verify context is available
|
||||||
|
- Review allowed-tools permissions
|
||||||
|
|
||||||
|
## Tips & Best Practices
|
||||||
|
|
||||||
|
- ✅ Be specific with trigger phrases
|
||||||
|
- ✅ Provide necessary context
|
||||||
|
- ✅ Check tool permissions match needs
|
||||||
|
- ❌ Avoid vague requests
|
||||||
|
- ❌ Don't mix unrelated tasks
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Issue:** Skill doesn't activate
|
||||||
|
**Solution:** Use exact trigger phrases from description
|
||||||
|
|
||||||
|
**Issue:** Unexpected results
|
||||||
|
**Solution:** Check input format and context
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- Main SKILL.md for full documentation
|
||||||
|
- scripts/ for automation helpers
|
||||||
|
- assets/ for configuration examples
|
||||||
7
skills/skill-adapter/scripts/README.md
Normal file
7
skills/skill-adapter/scripts/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Scripts
|
||||||
|
|
||||||
|
Bundled resources for api-schema-validator skill
|
||||||
|
|
||||||
|
- [ ] validate_schema.py: Script to validate a given schema against a data file or API endpoint.
|
||||||
|
- [ ] generate_schema.py: Script to generate a basic schema from a data file or API endpoint response.
|
||||||
|
- [ ] convert_schema.py: Script to convert between different schema formats (e.g., JSON Schema to Yup).
|
||||||
42
skills/skill-adapter/scripts/helper-template.sh
Executable file
42
skills/skill-adapter/scripts/helper-template.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Helper script template for skill automation
|
||||||
|
# Customize this for your skill's specific needs
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function show_usage() {
|
||||||
|
echo "Usage: $0 [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -h, --help Show this help message"
|
||||||
|
echo " -v, --verbose Enable verbose output"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h|--help)
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-v|--verbose)
|
||||||
|
VERBOSE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
show_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Your skill logic here
|
||||||
|
if [ "$VERBOSE" = true ]; then
|
||||||
|
echo "Running skill automation..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Complete"
|
||||||
32
skills/skill-adapter/scripts/validation.sh
Executable file
32
skills/skill-adapter/scripts/validation.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Skill validation helper
|
||||||
|
# Validates skill activation and functionality
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Validating skill..."
|
||||||
|
|
||||||
|
# Check if SKILL.md exists
|
||||||
|
if [ ! -f "../SKILL.md" ]; then
|
||||||
|
echo "❌ Error: SKILL.md not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate frontmatter
|
||||||
|
if ! grep -q "^---$" "../SKILL.md"; then
|
||||||
|
echo "❌ Error: No frontmatter found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
if ! grep -q "^name:" "../SKILL.md"; then
|
||||||
|
echo "❌ Error: Missing 'name' field"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "^description:" "../SKILL.md"; then
|
||||||
|
echo "❌ Error: Missing 'description' field"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Skill validation passed"
|
||||||
Reference in New Issue
Block a user