Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 25 additions & 140 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: vespera
description: Build APIs with Vespera - FastAPI-like DX for Rust/Axum. Covers route handlers, Schema derivation, and OpenAPI generation.
description: "Build REST APIs with Vespera, a FastAPI-inspired framework for Rust/Axum. Define pub async fn route handlers with Path/Query/Json extractors, derive request/response schemas with schema_type!, and auto-generate OpenAPI 3.1 docs at compile time. Use when creating Rust web services with Axum, adding HTTP endpoints with #[vespera::route], configuring Swagger/ReDoc documentation, deriving OpenAPI schemas from SeaORM models, handling multipart uploads, or merging multi-crate API specs."
---

# Vespera Usage Guide
Expand Down Expand Up @@ -28,6 +28,17 @@ pub async fn get_user(Path(id): Path<u32>) -> Json<User> { ... }
pub struct User { id: u32, name: String }
```

> **Validate**: After adding routes, run `npx @apidevtools/swagger-cli validate openapi.json` to confirm the generated spec includes all endpoints.

### End-to-End Workflow

1. **Define model** β€” create SeaORM entity or plain struct with `#[derive(Schema)]`
2. **Create schema_type!** β€” derive request/response types with `pick`/`omit` from the model
3. **Write handler** β€” `pub async fn` with extractors (`Path`, `Query`, `Json`, etc.)
4. **Annotate** β€” add `#[vespera::route(method, path = "...")]`
5. **Build** β€” `cargo build` regenerates `openapi.json` automatically
6. **Validate** β€” `npx @apidevtools/swagger-cli validate openapi.json`

---

## Type Mapping Reference
Expand Down Expand Up @@ -182,42 +193,6 @@ npx @apidevtools/swagger-cli validate openapi.json
> - SeaORM relation support (HasOne, BelongsTo, HasMany)
> - No manual field synchronization

### Best Practices

| DO | DON'T |
|----|-------|
| Use `pick` to select only needed fields | Define manual structs that duplicate Model fields |
| Use `omit` to exclude sensitive fields | Use `name` parameter unnecessarily |
| Use full `crate::models::...` paths | Rely on implicit module resolution |
| Define schema near route handlers | Scatter schemas across unrelated files |

**Primary Parameters (USE THESE):**
- `pick = [...]` - Allowlist: include ONLY these fields
- `omit = [...]` - Denylist: exclude these fields
- `omit_default` - Auto-omit fields with DB defaults (primary_key, default_value)

**Advanced Parameters (USE SPARINGLY):**
- `partial` - For PATCH endpoints only
- `rename` - Only when API naming differs from model
- `add` - Only when truly new fields needed (breaks `From` impl)
- `name` - **AVOID** unless same-file Model reference (see below)

### Why Not Manual Structs?

```rust
// ❌ BAD: Manual struct definition - requires sync with Model
#[derive(Serialize, Deserialize, Schema)]
pub struct UserResponse {
pub id: i32,
pub name: String,
pub email: String,
// Forgot to add new field? Schema out of sync!
}

// βœ… GOOD: Derive from Model - always in sync
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
```

### Basic Syntax

```rust
Expand Down Expand Up @@ -258,37 +233,6 @@ schema_type!(InternalDTO from Model, ignore);
schema_type!(LargeResponse from SomeType, clone = false);
```

### Same-File Model Reference (When to Use `name`)

> **The `name` parameter is ONLY needed for same-file Model references.**
> For cross-file references, use full paths and descriptive struct names instead.

When defining Schema in the same file as Model (common for SeaORM entities):

```rust
// In src/models/user.rs
pub struct Model {
pub id: i32,
pub name: String,
pub status: UserStatus, // Custom enum - auto-resolved to absolute path
}

pub enum UserStatus { Active, Inactive }

// βœ… CORRECT: Same-file reference - use `name` for OpenAPI schema name
vespera::schema_type!(Schema from Model, name = "UserSchema");

// ❌ WRONG: Using `name` for cross-file reference
// schema_type!(Schema from crate::models::user::Model, name = "UserResponse");
// βœ… CORRECT: Use descriptive struct name instead
// schema_type!(UserResponse from crate::models::user::Model, omit = ["password"]);
```

**Why avoid `name` for cross-file references?**
- The struct name itself becomes the OpenAPI schema name
- `UserResponse` is clearer than `Schema` with `name = "UserResponse"`
- Less parameters = less complexity

### Cross-File References

Reference structs from other files using full module paths:
Expand Down Expand Up @@ -329,31 +273,19 @@ Json(model.into()) // Easy conversion!

### Parameters

**Recommended (Primary):**

| Parameter | Description | Example |
|-----------|-------------|---------|
| `pick` | Include only these fields | `pick = ["name", "email"]` |
| `omit` | Exclude these fields | `omit = ["password"]` |
| `omit_default` | Auto-omit fields with DB defaults | `omit_default` (bare keyword) |

**Situational (Use When Needed):**

| Parameter | Description | When to Use |
|-----------|-------------|-------------|
| `partial` | Make fields optional | PATCH endpoints only |
| `rename` | Rename fields | API naming differs from model |
| `rename_all` | Serde rename strategy | Different casing needed |
| `add` | Add new fields | New fields not in model (breaks `From` impl) |
| `multipart` | Derive `Multipart` | Multipart form-data endpoints |

**Avoid (Special Cases Only):**

| Parameter | Description | When to Use |
|-----------|-------------|-------------|
| `name` | Custom OpenAPI schema name | **Same-file Model reference only** |
| `ignore` | Skip Schema derive | Internal DTOs not for OpenAPI |
| `clone` | Control Clone derive | Large structs where Clone is expensive |
| Parameter | Priority | Description | Usage |
|-----------|----------|-------------|-------|
| `pick` | Primary | Include only these fields | `pick = ["name", "email"]` |
| `omit` | Primary | Exclude these fields | `omit = ["password"]` |
| `omit_default` | Primary | Auto-omit fields with DB defaults | Bare keyword |
| `partial` | Situational | Make fields optional | PATCH endpoints only |
| `rename` | Situational | Rename fields | API naming differs from model |
| `rename_all` | Situational | Serde rename strategy | Different casing needed |
| `add` | Situational | Add new fields (breaks `From`) | New fields not in model |
| `multipart` | Situational | Derive `Multipart` | Multipart form-data endpoints |
| `name` | Avoid | Custom OpenAPI schema name | Same-file Model reference only |
| `ignore` | Avoid | Skip Schema derive | Internal DTOs not for OpenAPI |
| `clone` | Avoid | Control Clone derive | Large structs where Clone is expensive |

### SeaORM Integration (RECOMMENDED)

Expand Down Expand Up @@ -446,53 +378,6 @@ Rules:
- The adapter wrapper is hidden from OpenAPI; the spec still references the original related schema (`UserSchema`, `CategorySchema`)
- `HasMany` relations remain excluded by default unless explicitly `pick`ed or `add`ed

### Complete Example

```rust
// ============================================
// src/models/user.rs (SeaORM entity)
// ============================================
#[derive(Clone, Debug, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub email: String,
pub status: UserStatus,
pub password_hash: String, // Never expose!
pub created_at: DateTimeWithTimeZone,
}

// βœ… Same-file: use `name` parameter for OpenAPI schema name
vespera::schema_type!(Schema from Model, name = "UserSchema");

// ============================================
// src/routes/users.rs (Route handlers)
// ============================================
use vespera::schema_type;

// βœ… Cross-file: use descriptive struct names + pick/omit
// NO `name` parameter needed - struct name = OpenAPI schema name
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
schema_type!(UserPatch from crate::models::user::Model, omit = ["password_hash", "id"], partial);

#[vespera::route(get, path = "/{id}")]
pub async fn get_user(Path(id): Path<i32>, State(db): State<DbPool>) -> Json<UserResponse> {
let user = User::find_by_id(id).one(&db).await.unwrap().unwrap();
Json(user.into()) // From impl handles conversion
}

#[vespera::route(patch, path = "/{id}")]
pub async fn patch_user(
Path(id): Path<i32>,
Json(patch): Json<UserPatch>, // All fields are Option<T>
) -> Json<UserResponse> {
// Apply partial update...
}
```

### Multipart Mode (`multipart`)

Generate `Multipart` structs from existing multipart request types:
Expand Down