Skip to content

Commit 6aaf44b

Browse files
authored
Merge pull request #61 from dev-five-git/sea-schema-type
Sea schema type
2 parents 8cda376 + 5bd7d86 commit 6aaf44b

File tree

26 files changed

+3713
-251
lines changed

26 files changed

+3713
-251
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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"}

AGENTS.md

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# VESPERA PROJECT KNOWLEDGE BASE
22

3-
**Generated:** 2026-01-07
4-
**Commit:** 939a801
3+
**Generated:** 2026-02-04
54
**Branch:** main
65

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

3434
## KEY COMPONENTS
3535

3636
| File | Lines | Role |
3737
|------|-------|------|
38-
| `vespera_macro/src/lib.rs` | 1044 | `vespera!`, `#[route]`, `#[derive(Schema)]` |
39-
| `vespera_macro/src/parser/schema.rs` | 1527 | Rust struct → JSON Schema conversion |
40-
| `vespera_macro/src/parser/parameters.rs` | 845 | Extract path/query params from handlers |
41-
| `vespera_macro/src/openapi_generator.rs` | 808 | OpenAPI doc assembly |
42-
| `vespera_macro/src/collector.rs` | 707 | Filesystem route scanning |
38+
| `vespera_macro/src/lib.rs` | ~1044 | `vespera!`, `#[route]`, `#[derive(Schema)]` |
39+
| `vespera_macro/src/schema_macro.rs` | ~3000 | `schema_type!` macro, SeaORM relation handling |
40+
| `vespera_macro/src/parser/schema.rs` | ~1527 | Rust struct → JSON Schema conversion |
41+
| `vespera_macro/src/parser/parameters.rs` | ~845 | Extract path/query params from handlers |
42+
| `vespera_macro/src/openapi_generator.rs` | ~808 | OpenAPI doc assembly |
43+
| `vespera_macro/src/collector.rs` | ~707 | Filesystem route scanning |
44+
45+
## SCHEMA_TYPE! MACRO
46+
47+
Generate request/response types from existing structs with powerful transformations.
48+
49+
### Key Features
50+
- **Same-file Model reference**: `schema_type!(Schema from Model, name = "UserSchema")` - infers module path from file location
51+
- **Cross-file reference**: `schema_type!(Response from crate::models::user::Model, omit = ["password"])`
52+
- **SeaORM integration**: Automatic conversion of `HasOne`, `BelongsTo`, `HasMany` relations
53+
- **Chrono conversion**: `DateTimeWithTimeZone``vespera::chrono::DateTime<FixedOffset>`
54+
- **Circular reference handling**: Automatic detection and inline field generation
55+
56+
### Parameters
57+
| Parameter | Description |
58+
|-----------|-------------|
59+
| `pick` | Include only specified fields |
60+
| `omit` | Exclude specified fields |
61+
| `rename` | Rename fields: `[("old", "new")]` |
62+
| `add` | Add new fields (disables auto `From`) |
63+
| `clone` | Control Clone derive (default: true) |
64+
| `partial` | Make fields optional for PATCH |
65+
| `name` | Custom OpenAPI schema name |
66+
| `rename_all` | Serde rename strategy |
67+
| `ignore` | Skip Schema derive |
68+
69+
### Module Path Resolution
70+
When using simple `Model` path (no `crate::` prefix):
71+
1. `find_struct_from_path()` calls `find_struct_by_name_in_all_files()`
72+
2. Uses `schema_name` hint to disambiguate (e.g., "UserSchema" → prefers `user.rs`)
73+
3. `file_path_to_module_path()` infers module path from file location
74+
4. This enables `super::` resolution in relation types
4375

4476
## CONVENTIONS
4577

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,27 @@ schema_type!(UserResponse from crate::models::user::Model, omit = ["password_has
252252
schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"], add = [("id": i32)]);
253253
```
254254

255+
### Same-File Model Reference
256+
257+
When the model is in the same file, you can use a simple name with `name` parameter:
258+
259+
```rust
260+
// In src/models/user.rs
261+
pub struct Model {
262+
pub id: i32,
263+
pub name: String,
264+
pub email: String,
265+
}
266+
267+
// Simple `Model` path works when using `name` parameter
268+
vespera::schema_type!(Schema from Model, name = "UserSchema");
269+
```
270+
271+
The macro infers the module path from the file location, so relation types like `HasOne<super::user::Entity>` are resolved correctly.
272+
255273
### Cross-File References
256274

257-
Reference structs from other files using module paths:
275+
Reference structs from other files using full module paths:
258276

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

294+
### Partial Updates (PATCH)
295+
296+
Use `partial` to make fields optional for PATCH-style updates:
297+
298+
```rust
299+
// All fields become Option<T>
300+
schema_type!(UserPatch from User, partial);
301+
302+
// Only specific fields become Option<T>
303+
schema_type!(UserPatch from User, partial = ["name", "email"]);
304+
```
305+
306+
### Serde Rename All
307+
308+
Apply serde rename_all strategy:
309+
310+
```rust
311+
// Convert field names to camelCase in JSON
312+
schema_type!(UserDTO from User, rename_all = "camelCase");
313+
314+
// Available: "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", etc.
315+
```
316+
317+
### SeaORM Integration
318+
319+
`schema_type!` has first-class support for SeaORM models with relations:
320+
321+
```rust
322+
use sea_orm::entity::prelude::*;
323+
324+
#[derive(Clone, Debug, DeriveEntityModel)]
325+
#[sea_orm(table_name = "memos")]
326+
pub struct Model {
327+
#[sea_orm(primary_key)]
328+
pub id: i32,
329+
pub title: String,
330+
pub user_id: i32,
331+
pub user: BelongsTo<super::user::Entity>, // → Option<Box<UserSchema>>
332+
pub comments: HasMany<super::comment::Entity>, // → Vec<CommentSchema>
333+
}
334+
335+
// Generates Schema with proper relation types
336+
vespera::schema_type!(Schema from Model, name = "MemoSchema");
337+
```
338+
339+
**Relation Type Conversions:**
340+
341+
| SeaORM Type | Generated Schema Type |
342+
|-------------|----------------------|
343+
| `HasOne<Entity>` | `Box<Schema>` or `Option<Box<Schema>>` |
344+
| `BelongsTo<Entity>` | `Option<Box<Schema>>` |
345+
| `HasMany<Entity>` | `Vec<Schema>` |
346+
| `DateTimeWithTimeZone` | `chrono::DateTime<FixedOffset>` |
347+
348+
**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.
349+
276350
### Parameters
277351

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

286364
---
287365

SKILL.md

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,32 @@ npx @apidevtools/swagger-cli validate openapi.json
162162

163163
---
164164

165-
## schema_type! Macro
165+
## schema_type! Macro (RECOMMENDED)
166166

167-
Generate request/response types from existing structs with field filtering. Supports cross-file references and auto-generates `From` impl.
167+
> **ALWAYS prefer `schema_type!` over manually defining request/response structs.**
168+
>
169+
> Benefits:
170+
> - Single source of truth (your model)
171+
> - Auto-generated `From` impl for easy conversion
172+
> - Automatic type resolution (enums, custom types → absolute paths)
173+
> - SeaORM relation support (HasOne, BelongsTo, HasMany)
174+
> - No manual field synchronization
175+
176+
### Why Not Manual Structs?
177+
178+
```rust
179+
// ❌ BAD: Manual struct definition - requires sync with Model
180+
#[derive(Serialize, Deserialize, Schema)]
181+
pub struct UserResponse {
182+
pub id: i32,
183+
pub name: String,
184+
pub email: String,
185+
// Forgot to add new field? Schema out of sync!
186+
}
187+
188+
// ✅ GOOD: Derive from Model - always in sync
189+
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
190+
```
168191

169192
### Basic Syntax
170193

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

207+
// Partial updates (all fields become Option<T>)
208+
schema_type!(UserPatch from crate::models::user::Model, partial);
209+
210+
// Partial updates (specific fields only)
211+
schema_type!(UserPatch from crate::models::user::Model, partial = ["name", "email"]);
212+
213+
// Custom serde rename strategy
214+
schema_type!(UserSnakeCase from crate::models::user::Model, rename_all = "snake_case");
215+
216+
// Custom OpenAPI schema name
217+
schema_type!(Schema from Model, name = "UserSchema");
218+
219+
// Skip Schema derive (won't appear in OpenAPI)
220+
schema_type!(InternalDTO from Model, ignore);
221+
184222
// Disable Clone derive
185223
schema_type!(LargeResponse from SomeType, clone = false);
186224
```
187225

226+
### Same-File Model Reference
227+
228+
When the model is in the same file, use simple name with `name` parameter:
229+
230+
```rust
231+
// In src/models/user.rs
232+
pub struct Model {
233+
pub id: i32,
234+
pub name: String,
235+
pub status: UserStatus, // Custom enum - auto-resolved to absolute path
236+
}
237+
238+
pub enum UserStatus { Active, Inactive }
239+
240+
// Simple `Model` path works - module path inferred from file location
241+
vespera::schema_type!(Schema from Model, name = "UserSchema");
242+
```
243+
188244
### Cross-File References
189245

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

236-
### Use Case: Sea-ORM Models
296+
### SeaORM Integration (RECOMMENDED)
297+
298+
`schema_type!` has first-class SeaORM support with automatic relation handling:
299+
300+
```rust
301+
// src/models/memo.rs
302+
#[derive(Clone, Debug, DeriveEntityModel)]
303+
#[sea_orm(table_name = "memo")]
304+
pub struct Model {
305+
#[sea_orm(primary_key)]
306+
pub id: i32,
307+
pub title: String,
308+
pub user_id: i32,
309+
pub status: MemoStatus, // Custom enum
310+
pub user: BelongsTo<super::user::Entity>, // → Option<Box<UserSchema>>
311+
pub comments: HasMany<super::comment::Entity>, // → Vec<CommentSchema>
312+
pub created_at: DateTimeWithTimeZone, // → chrono::DateTime<FixedOffset>
313+
}
314+
315+
#[derive(EnumIter, DeriveActiveEnum, Serialize, Deserialize, Schema)]
316+
pub enum MemoStatus { Draft, Published, Archived }
317+
318+
// Generates Schema with proper types - no imports needed!
319+
vespera::schema_type!(Schema from Model, name = "MemoSchema");
320+
```
321+
322+
**Automatic Type Conversions:**
237323

238-
Perfect for creating API types from database models:
324+
| SeaORM Type | Generated Type | Notes |
325+
|-------------|---------------|-------|
326+
| `HasOne<Entity>` | `Box<Schema>` or `Option<Box<Schema>>` | Based on FK nullability |
327+
| `BelongsTo<Entity>` | `Option<Box<Schema>>` | Always optional |
328+
| `HasMany<Entity>` | `Vec<Schema>` | |
329+
| `DateTimeWithTimeZone` | `vespera::chrono::DateTime<FixedOffset>` | No SeaORM import needed |
330+
| Custom enums | `crate::module::EnumName` | Auto-resolved to absolute path |
331+
332+
**Circular Reference Handling:** Automatically detected and handled by inlining fields.
333+
334+
### Complete Example
239335

240336
```rust
241337
// src/models/user.rs (Sea-ORM entity)
@@ -246,19 +342,32 @@ pub struct Model {
246342
pub id: i32,
247343
pub name: String,
248344
pub email: String,
345+
pub status: UserStatus,
249346
pub password_hash: String, // Never expose!
250347
pub created_at: DateTimeWithTimeZone,
251348
}
252349

253-
// src/routes/users.rs
350+
// Generate Schema in same file - simple Model path
351+
vespera::schema_type!(Schema from Model, name = "UserSchema");
352+
353+
// src/routes/users.rs - use full path for cross-file reference
254354
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
255355
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
356+
schema_type!(UserPatch from crate::models::user::Model, omit = ["password_hash", "id"], partial);
256357

257358
#[vespera::route(get, path = "/{id}")]
258359
pub async fn get_user(Path(id): Path<i32>, State(db): State<DbPool>) -> Json<UserResponse> {
259360
let user = User::find_by_id(id).one(&db).await.unwrap().unwrap();
260361
Json(user.into()) // From impl handles conversion
261362
}
363+
364+
#[vespera::route(patch, path = "/{id}")]
365+
pub async fn patch_user(
366+
Path(id): Path<i32>,
367+
Json(patch): Json<UserPatch>, // All fields are Option<T>
368+
) -> Json<UserResponse> {
369+
// Apply partial update...
370+
}
262371
```
263372

264373
---

crates/vespera/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ vespera_core = { workspace = true }
1515
vespera_macro = { workspace = true }
1616
axum = "0.8"
1717
axum-extra = { version = "0.12", optional = true }
18+
chrono = { version = "0.4", features = ["serde"] }
1819
serde_json = "1"
1920
tower-layer = "0.3"
2021
tower-service = "0.3"

crates/vespera/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub use vespera_macro::{Schema, export_app, route, schema, schema_type, vespera}
2525
// Re-export serde_json for merge feature (runtime spec merging)
2626
pub use serde_json;
2727

28+
// Re-export chrono for schema_type! datetime conversion
29+
// This allows generated types to use chrono::DateTime without users adding chrono dependency
30+
pub use chrono;
31+
2832
// Re-export axum for convenience
2933
pub mod axum {
3034
pub use axum::*;

0 commit comments

Comments
 (0)