Skip to content

Commit f92c141

Browse files
committed
Apply camel
1 parent 0ce7dbb commit f92c141

5 files changed

Lines changed: 484 additions & 2 deletions

File tree

crates/vespera_macro/src/parser.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,86 @@ fn is_primitive_type(ty: &Type) -> bool {
182182
}
183183
}
184184

185+
/// Extract rename_all attribute from struct attributes
186+
fn extract_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
187+
for attr in attrs {
188+
if attr.path().is_ident("serde") {
189+
// Parse the attribute tokens manually
190+
// Format: #[serde(rename_all = "camelCase")]
191+
let tokens = attr.meta.require_list().ok()?;
192+
let token_str = tokens.tokens.to_string();
193+
194+
// Look for rename_all = "..." pattern
195+
if let Some(start) = token_str.find("rename_all") {
196+
let remaining = &token_str[start + "rename_all".len()..];
197+
if let Some(equals_pos) = remaining.find('=') {
198+
let value_part = &remaining[equals_pos + 1..].trim();
199+
// Extract string value (remove quotes)
200+
if value_part.starts_with('"') && value_part.ends_with('"') {
201+
let value = &value_part[1..value_part.len() - 1];
202+
return Some(value.to_string());
203+
}
204+
}
205+
}
206+
}
207+
}
208+
None
209+
}
210+
211+
/// Convert field name according to rename_all rule
212+
fn rename_field(field_name: &str, rename_all: Option<&str>) -> String {
213+
match rename_all {
214+
Some("camelCase") => {
215+
// Convert snake_case to camelCase
216+
let mut result = String::new();
217+
let mut capitalize_next = false;
218+
for ch in field_name.chars() {
219+
if ch == '_' {
220+
capitalize_next = true;
221+
} else if capitalize_next {
222+
result.push(ch.to_uppercase().next().unwrap_or(ch));
223+
capitalize_next = false;
224+
} else {
225+
result.push(ch);
226+
}
227+
}
228+
result
229+
}
230+
Some("snake_case") => {
231+
// Convert camelCase to snake_case
232+
let mut result = String::new();
233+
for (i, ch) in field_name.chars().enumerate() {
234+
if ch.is_uppercase() && i > 0 {
235+
result.push('_');
236+
}
237+
result.push(ch.to_lowercase().next().unwrap_or(ch));
238+
}
239+
result
240+
}
241+
Some("kebab-case") => {
242+
// Convert snake_case to kebab-case
243+
field_name.replace('_', "-")
244+
}
245+
Some("PascalCase") => {
246+
// Convert snake_case to PascalCase
247+
let mut result = String::new();
248+
let mut capitalize_next = true;
249+
for ch in field_name.chars() {
250+
if ch == '_' {
251+
capitalize_next = true;
252+
} else if capitalize_next {
253+
result.push(ch.to_uppercase().next().unwrap_or(ch));
254+
capitalize_next = false;
255+
} else {
256+
result.push(ch);
257+
}
258+
}
259+
result
260+
}
261+
_ => field_name.to_string(),
262+
}
263+
}
264+
185265
/// Parse struct definition to OpenAPI Schema
186266
pub fn parse_struct_to_schema(
187267
struct_item: &syn::ItemStruct,
@@ -190,15 +270,21 @@ pub fn parse_struct_to_schema(
190270
let mut properties = BTreeMap::new();
191271
let mut required = Vec::new();
192272

273+
// Extract rename_all attribute from struct
274+
let rename_all = extract_rename_all(&struct_item.attrs);
275+
193276
match &struct_item.fields {
194277
Fields::Named(fields_named) => {
195278
for field in &fields_named.named {
196-
let field_name = field
279+
let rust_field_name = field
197280
.ident
198281
.as_ref()
199282
.map(|i| i.to_string())
200283
.unwrap_or_else(|| "unknown".to_string());
201284

285+
// Apply rename_all transformation if present
286+
let field_name = rename_field(&rust_field_name, rename_all.as_deref());
287+
202288
let field_type = &field.ty;
203289
let schema_ref = parse_type_to_schema_ref(field_type, known_schemas);
204290

examples/axum-example/openapi.json

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,33 @@
5454
}
5555
}
5656
},
57+
"/complex-struct-body-with-rename": {
58+
"post": {
59+
"operationId": "mod_file_with_complex_struct_body_with_rename",
60+
"requestBody": {
61+
"required": true,
62+
"content": {
63+
"application/json": {
64+
"schema": {
65+
"$ref": "#/components/schemas/ComplexStructBodyWithRename"
66+
}
67+
}
68+
}
69+
},
70+
"responses": {
71+
"200": {
72+
"description": "Successful response",
73+
"content": {
74+
"application/json": {
75+
"schema": {
76+
"type": "string"
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
},
5784
"/health": {
5885
"get": {
5986
"operationId": "health",
@@ -358,6 +385,69 @@
358385
"nested_struct_map_array"
359386
]
360387
},
388+
"ComplexStructBodyWithRename": {
389+
"type": "object",
390+
"properties": {
391+
"age": {
392+
"type": "integer"
393+
},
394+
"array": {
395+
"type": "array",
396+
"items": {
397+
"type": "string"
398+
}
399+
},
400+
"map": {
401+
"type": "object"
402+
},
403+
"name": {
404+
"type": "string"
405+
},
406+
"nestedArray": {
407+
"type": "array",
408+
"items": {
409+
"$ref": "#/components/schemas/StructBodyWithOptional"
410+
}
411+
},
412+
"nestedMap": {
413+
"type": "object"
414+
},
415+
"nestedStruct": {
416+
"$ref": "#/components/schemas/StructBodyWithOptional"
417+
},
418+
"nestedStructArray": {
419+
"type": "array",
420+
"items": {
421+
"$ref": "#/components/schemas/StructBodyWithOptional"
422+
}
423+
},
424+
"nestedStructArrayMap": {
425+
"type": "array",
426+
"items": {
427+
"type": "object"
428+
}
429+
},
430+
"nestedStructMap": {
431+
"type": "object"
432+
},
433+
"nestedStructMapArray": {
434+
"type": "object"
435+
}
436+
},
437+
"required": [
438+
"name",
439+
"age",
440+
"nestedStruct",
441+
"array",
442+
"map",
443+
"nestedArray",
444+
"nestedMap",
445+
"nestedStructArray",
446+
"nestedStructMap",
447+
"nestedStructArrayMap",
448+
"nestedStructMapArray"
449+
]
450+
},
361451
"StructBody": {
362452
"type": "object",
363453
"properties": {

examples/axum-example/src/routes/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ pub async fn mod_file_with_struct_body_with_optional(
6464
}
6565

6666
#[derive(Deserialize, Schema)]
67-
#[serde(rename_all = "camelCase")]
6867
pub struct ComplexStructBody {
6968
pub name: String,
7069
pub age: u32,
@@ -96,3 +95,39 @@ pub async fn mod_file_with_complex_struct_body(Json(body): Json<ComplexStructBod
9695
body.nested_struct_map_array
9796
)
9897
}
98+
99+
#[derive(Deserialize, Schema)]
100+
#[serde(rename_all = "camelCase")]
101+
pub struct ComplexStructBodyWithRename {
102+
pub name: String,
103+
pub age: u32,
104+
pub nested_struct: StructBodyWithOptional,
105+
pub array: Vec<String>,
106+
pub map: HashMap<String, String>,
107+
pub nested_array: Vec<StructBodyWithOptional>,
108+
pub nested_map: HashMap<String, StructBodyWithOptional>,
109+
pub nested_struct_array: Vec<StructBodyWithOptional>,
110+
pub nested_struct_map: HashMap<String, StructBodyWithOptional>,
111+
pub nested_struct_array_map: Vec<HashMap<String, StructBodyWithOptional>>,
112+
pub nested_struct_map_array: HashMap<String, Vec<StructBodyWithOptional>>,
113+
}
114+
115+
#[vespera::route(post, path = "/complex-struct-body-with-rename")]
116+
pub async fn mod_file_with_complex_struct_body_with_rename(
117+
Json(body): Json<ComplexStructBodyWithRename>,
118+
) -> String {
119+
format!(
120+
"name: {}, age: {}, nested_struct: {:?}, array: {:?}, map: {:?}, nested_array: {:?}, nested_map: {:?}, nested_struct_array: {:?}, nested_struct_map: {:?}, nested_struct_array_map: {:?}, nested_struct_map_array: {:?}",
121+
body.name,
122+
body.age,
123+
body.nested_struct,
124+
body.array,
125+
body.map,
126+
body.nested_array,
127+
body.nested_map,
128+
body.nested_struct_array,
129+
body.nested_struct_map,
130+
body.nested_struct_array_map,
131+
body.nested_struct_map_array
132+
)
133+
}

0 commit comments

Comments
 (0)