Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .changepacks/changepack_log_PQDHMsoTbCYQqEAfpwVEi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Support rel, nested object","date":"2026-02-03T17:14:35.195097800Z"}
48 changes: 40 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# VESPERA PROJECT KNOWLEDGE BASE

**Generated:** 2026-01-07
**Commit:** 939a801
**Generated:** 2026-02-04
**Branch:** main

## OVERVIEW
Expand All @@ -13,7 +12,7 @@ Vespera is a fully automated OpenAPI 3.1 engine for Axum - delivers FastAPI-like
```
vespera/
├── crates/
│ ├── vespera/ # Public API - re-exports everything
│ ├── vespera/ # Public API - re-exports everything (+ chrono re-export)
│ ├── vespera_core/ # OpenAPI types, route/schema abstractions
│ └── vespera_macro/ # Proc-macros (main logic lives here)
└── examples/axum-example/ # Demo app with route patterns
Expand All @@ -28,18 +27,51 @@ vespera/
| Add route parser feature | `crates/vespera_macro/src/parser/` | Type extraction logic |
| Change schema generation | `crates/vespera_macro/src/parser/schema.rs` | Rust→JSON Schema |
| Modify route attribute | `crates/vespera_macro/src/args.rs` | `#[route]` parsing |
| Modify schema_type! macro | `crates/vespera_macro/src/schema_macro.rs` | Type derivation & SeaORM support |
| Add core types | `crates/vespera_core/src/` | OpenAPI spec types |
| Test new features | `examples/axum-example/` | Add route, run example |

## KEY COMPONENTS

| File | Lines | Role |
|------|-------|------|
| `vespera_macro/src/lib.rs` | 1044 | `vespera!`, `#[route]`, `#[derive(Schema)]` |
| `vespera_macro/src/parser/schema.rs` | 1527 | Rust struct → JSON Schema conversion |
| `vespera_macro/src/parser/parameters.rs` | 845 | Extract path/query params from handlers |
| `vespera_macro/src/openapi_generator.rs` | 808 | OpenAPI doc assembly |
| `vespera_macro/src/collector.rs` | 707 | Filesystem route scanning |
| `vespera_macro/src/lib.rs` | ~1044 | `vespera!`, `#[route]`, `#[derive(Schema)]` |
| `vespera_macro/src/schema_macro.rs` | ~3000 | `schema_type!` macro, SeaORM relation handling |
| `vespera_macro/src/parser/schema.rs` | ~1527 | Rust struct → JSON Schema conversion |
| `vespera_macro/src/parser/parameters.rs` | ~845 | Extract path/query params from handlers |
| `vespera_macro/src/openapi_generator.rs` | ~808 | OpenAPI doc assembly |
| `vespera_macro/src/collector.rs` | ~707 | Filesystem route scanning |

## SCHEMA_TYPE! MACRO

Generate request/response types from existing structs with powerful transformations.

### Key Features
- **Same-file Model reference**: `schema_type!(Schema from Model, name = "UserSchema")` - infers module path from file location
- **Cross-file reference**: `schema_type!(Response from crate::models::user::Model, omit = ["password"])`
- **SeaORM integration**: Automatic conversion of `HasOne`, `BelongsTo`, `HasMany` relations
- **Chrono conversion**: `DateTimeWithTimeZone` → `vespera::chrono::DateTime<FixedOffset>`
- **Circular reference handling**: Automatic detection and inline field generation

### Parameters
| Parameter | Description |
|-----------|-------------|
| `pick` | Include only specified fields |
| `omit` | Exclude specified fields |
| `rename` | Rename fields: `[("old", "new")]` |
| `add` | Add new fields (disables auto `From`) |
| `clone` | Control Clone derive (default: true) |
| `partial` | Make fields optional for PATCH |
| `name` | Custom OpenAPI schema name |
| `rename_all` | Serde rename strategy |
| `ignore` | Skip Schema derive |

### Module Path Resolution
When using simple `Model` path (no `crate::` prefix):
1. `find_struct_from_path()` calls `find_struct_by_name_in_all_files()`
2. Uses `schema_name` hint to disambiguate (e.g., "UserSchema" → prefers `user.rs`)
3. `file_path_to_module_path()` infers module path from file location
4. This enables `super::` resolution in relation types

## CONVENTIONS

Expand Down
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 79 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,27 @@ schema_type!(UserResponse from crate::models::user::Model, omit = ["password_has
schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"], add = [("id": i32)]);
```

### Same-File Model Reference

When the model is in the same file, you can use a simple name with `name` parameter:

```rust
// In src/models/user.rs
pub struct Model {
pub id: i32,
pub name: String,
pub email: String,
}

// Simple `Model` path works when using `name` parameter
vespera::schema_type!(Schema from Model, name = "UserSchema");
```

The macro infers the module path from the file location, so relation types like `HasOne<super::user::Entity>` are resolved correctly.

### Cross-File References

Reference structs from other files using module paths:
Reference structs from other files using full module paths:

```rust
// In src/routes/users.rs - references src/models/user.rs
Expand All @@ -273,6 +291,62 @@ let model: Model = db.find_user(id).await?;
Json(model.into()) // Automatic conversion!
```

### Partial Updates (PATCH)

Use `partial` to make fields optional for PATCH-style updates:

```rust
// All fields become Option<T>
schema_type!(UserPatch from User, partial);

// Only specific fields become Option<T>
schema_type!(UserPatch from User, partial = ["name", "email"]);
```

### Serde Rename All

Apply serde rename_all strategy:

```rust
// Convert field names to camelCase in JSON
schema_type!(UserDTO from User, rename_all = "camelCase");

// Available: "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", etc.
```

### SeaORM Integration

`schema_type!` has first-class support for SeaORM models with relations:

```rust
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, DeriveEntityModel)]
#[sea_orm(table_name = "memos")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub user_id: i32,
pub user: BelongsTo<super::user::Entity>, // → Option<Box<UserSchema>>
pub comments: HasMany<super::comment::Entity>, // → Vec<CommentSchema>
}

// Generates Schema with proper relation types
vespera::schema_type!(Schema from Model, name = "MemoSchema");
```

**Relation Type Conversions:**

| SeaORM Type | Generated Schema Type |
|-------------|----------------------|
| `HasOne<Entity>` | `Box<Schema>` or `Option<Box<Schema>>` |
| `BelongsTo<Entity>` | `Option<Box<Schema>>` |
| `HasMany<Entity>` | `Vec<Schema>` |
| `DateTimeWithTimeZone` | `chrono::DateTime<FixedOffset>` |

**Circular Reference Handling:** When schemas reference each other (e.g., User ↔ Memo), the macro automatically detects and handles circular references by inlining fields to prevent infinite recursion.

### Parameters

| Parameter | Description |
Expand All @@ -282,6 +356,10 @@ Json(model.into()) // Automatic conversion!
| `rename` | Rename fields: `rename = [("old", "new")]` |
| `add` | Add new fields (disables auto `From` impl) |
| `clone` | Control Clone derive (default: true) |
| `partial` | Make fields optional: `partial` or `partial = ["field1"]` |
| `name` | Custom OpenAPI schema name: `name = "UserSchema"` |
| `rename_all` | Serde rename strategy: `rename_all = "camelCase"` |
| `ignore` | Skip Schema derive (bare keyword, no value) |

---

Expand Down
119 changes: 114 additions & 5 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,32 @@ npx @apidevtools/swagger-cli validate openapi.json

---

## schema_type! Macro
## schema_type! Macro (RECOMMENDED)

Generate request/response types from existing structs with field filtering. Supports cross-file references and auto-generates `From` impl.
> **ALWAYS prefer `schema_type!` over manually defining request/response structs.**
>
> Benefits:
> - Single source of truth (your model)
> - Auto-generated `From` impl for easy conversion
> - Automatic type resolution (enums, custom types → absolute paths)
> - SeaORM relation support (HasOne, BelongsTo, HasMany)
> - No manual field synchronization

### 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

Expand All @@ -181,10 +204,43 @@ schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"],
// Rename fields
schema_type!(UserDTO from crate::models::user::Model, rename = [("id", "user_id")]);

// Partial updates (all fields become Option<T>)
schema_type!(UserPatch from crate::models::user::Model, partial);

// Partial updates (specific fields only)
schema_type!(UserPatch from crate::models::user::Model, partial = ["name", "email"]);

// Custom serde rename strategy
schema_type!(UserSnakeCase from crate::models::user::Model, rename_all = "snake_case");

// Custom OpenAPI schema name
schema_type!(Schema from Model, name = "UserSchema");

// Skip Schema derive (won't appear in OpenAPI)
schema_type!(InternalDTO from Model, ignore);

// Disable Clone derive
schema_type!(LargeResponse from SomeType, clone = false);
```

### Same-File Model Reference

When the model is in the same file, use simple name with `name` parameter:

```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 }

// Simple `Model` path works - module path inferred from file location
vespera::schema_type!(Schema from Model, name = "UserSchema");
```

### Cross-File References

Reference structs from other files using full module paths:
Expand Down Expand Up @@ -231,11 +287,51 @@ Json(model.into()) // Easy conversion!
| `omit` | Exclude these fields | `omit = ["password"]` |
| `rename` | Rename fields | `rename = [("id", "user_id")]` |
| `add` | Add new fields (disables From impl) | `add = [("extra": String)]` |
| `partial` | Make fields optional for PATCH | `partial` or `partial = ["name"]` |
| `name` | Custom OpenAPI schema name | `name = "UserSchema"` |
| `rename_all` | Serde rename strategy | `rename_all = "camelCase"` |
| `ignore` | Skip Schema derive | bare keyword |
| `clone` | Control Clone derive (default: true) | `clone = false` |

### Use Case: Sea-ORM Models
### SeaORM Integration (RECOMMENDED)

`schema_type!` has first-class SeaORM support with automatic relation handling:

```rust
// src/models/memo.rs
#[derive(Clone, Debug, DeriveEntityModel)]
#[sea_orm(table_name = "memo")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub user_id: i32,
pub status: MemoStatus, // Custom enum
pub user: BelongsTo<super::user::Entity>, // → Option<Box<UserSchema>>
pub comments: HasMany<super::comment::Entity>, // → Vec<CommentSchema>
pub created_at: DateTimeWithTimeZone, // → chrono::DateTime<FixedOffset>
}

#[derive(EnumIter, DeriveActiveEnum, Serialize, Deserialize, Schema)]
pub enum MemoStatus { Draft, Published, Archived }

// Generates Schema with proper types - no imports needed!
vespera::schema_type!(Schema from Model, name = "MemoSchema");
```

**Automatic Type Conversions:**

Perfect for creating API types from database models:
| SeaORM Type | Generated Type | Notes |
|-------------|---------------|-------|
| `HasOne<Entity>` | `Box<Schema>` or `Option<Box<Schema>>` | Based on FK nullability |
| `BelongsTo<Entity>` | `Option<Box<Schema>>` | Always optional |
| `HasMany<Entity>` | `Vec<Schema>` | |
| `DateTimeWithTimeZone` | `vespera::chrono::DateTime<FixedOffset>` | No SeaORM import needed |
| Custom enums | `crate::module::EnumName` | Auto-resolved to absolute path |

**Circular Reference Handling:** Automatically detected and handled by inlining fields.

### Complete Example

```rust
// src/models/user.rs (Sea-ORM entity)
Expand All @@ -246,19 +342,32 @@ pub struct Model {
pub id: i32,
pub name: String,
pub email: String,
pub status: UserStatus,
pub password_hash: String, // Never expose!
pub created_at: DateTimeWithTimeZone,
}

// src/routes/users.rs
// Generate Schema in same file - simple Model path
vespera::schema_type!(Schema from Model, name = "UserSchema");

// src/routes/users.rs - use full path for cross-file reference
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...
}
```

---
Expand Down
1 change: 1 addition & 0 deletions crates/vespera/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vespera_core = { workspace = true }
vespera_macro = { workspace = true }
axum = "0.8"
axum-extra = { version = "0.12", optional = true }
chrono = { version = "0.4", features = ["serde"] }
serde_json = "1"
tower-layer = "0.3"
tower-service = "0.3"
4 changes: 4 additions & 0 deletions crates/vespera/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub use vespera_macro::{Schema, export_app, route, schema, schema_type, vespera}
// Re-export serde_json for merge feature (runtime spec merging)
pub use serde_json;

// Re-export chrono for schema_type! datetime conversion
// This allows generated types to use chrono::DateTime without users adding chrono dependency
pub use chrono;

// Re-export axum for convenience
pub mod axum {
pub use axum::*;
Expand Down
Loading