oas_forge/
doc_parser.rs

1use syn::punctuated::Punctuated;
2use syn::{Attribute, Expr, ExprLit, Lit, Meta};
3
4/// Helper to extract doc comments from attributes
5pub fn extract_doc_comments(attrs: &[Attribute]) -> Vec<String> {
6    let mut doc_lines = Vec::new();
7    for attr in attrs {
8        if attr.path().is_ident("doc") {
9            if let Meta::NameValue(meta) = &attr.meta {
10                if let Expr::Lit(expr_lit) = &meta.value {
11                    if let Lit::Str(lit_str) = &expr_lit.lit {
12                        doc_lines.push(lit_str.value());
13                    }
14                }
15            }
16        }
17    }
18    doc_lines
19}
20
21pub fn apply_casing(text: &str, case: &str) -> String {
22    match case {
23        "lowercase" => text.to_lowercase(),
24        "UPPERCASE" => text.to_uppercase(),
25        "PascalCase" => {
26            // Check if it contains underscores (snake_case -> PascalCase)
27            if text.contains('_') {
28                text.split('_')
29                    .map(|part| {
30                        let mut c = part.chars();
31                        match c.next() {
32                            None => String::new(),
33                            Some(f) => f.to_uppercase().to_string() + c.as_str(),
34                        }
35                    })
36                    .collect()
37            } else {
38                // Assume it is already Pascal or camel, just ensure first char is Upper
39                let mut c = text.chars();
40                match c.next() {
41                    None => String::new(),
42                    Some(f) => f.to_uppercase().to_string() + c.as_str(),
43                }
44            }
45        }
46        "camelCase" => {
47            // Check if it contains underscores (snake_case -> camelCase)
48            if text.contains('_') {
49                let parts: Vec<&str> = text.split('_').collect();
50                if parts.is_empty() {
51                    return String::new();
52                }
53                let first = parts[0].to_lowercase();
54                let rest: String = parts[1..]
55                    .iter()
56                    .map(|part| {
57                        let mut c = part.chars();
58                        match c.next() {
59                            None => String::new(),
60                            Some(f) => f.to_uppercase().to_string() + c.as_str(),
61                        }
62                    })
63                    .collect();
64                first + &rest
65            } else {
66                // Just ensure first char is Lower
67                let mut c = text.chars();
68                match c.next() {
69                    None => String::new(),
70                    Some(f) => f.to_lowercase().to_string() + c.as_str(),
71                }
72            }
73        }
74        "snake_case" => {
75            let mut s = String::new();
76            for (i, c) in text.chars().enumerate() {
77                if c.is_uppercase() && i > 0 {
78                    s.push('_');
79                }
80                if let Some(lower) = c.to_lowercase().next() {
81                    s.push(lower);
82                }
83            }
84            s
85        }
86        "SCREAMING_SNAKE_CASE" => apply_casing(text, "snake_case").to_uppercase(),
87        "kebab-case" => apply_casing(text, "snake_case").replace('_', "-"),
88        "SCREAMING-KEBAB-CASE" => apply_casing(text, "kebab-case").to_uppercase(),
89        _ => text.to_string(),
90    }
91}
92
93/// Extracts doc comments and handles "@openapi rename/rename-all" + Serde logic.
94pub fn extract_naming_and_doc(
95    attrs: &[Attribute],
96    default_name: &str,
97) -> (String, String, Option<String>, Vec<String>) {
98    let mut doc_lines = Vec::new();
99    // We collect cleaned lines here (without @openapi tags)
100    let mut clean_doc_lines = Vec::new();
101
102    let mut final_name = default_name.to_string();
103    let mut rename_rule = None;
104
105    // 1. Check Serde Attributes (Lower Precedence)
106    for attr in attrs {
107        if attr.path().is_ident("serde") {
108            if let Meta::List(list) = &attr.meta {
109                if let Ok(nested) =
110                    list.parse_args_with(Punctuated::<Meta, syn::Token![,]>::parse_terminated)
111                {
112                    for meta in nested {
113                        if let Meta::NameValue(nv) = meta {
114                            if nv.path.is_ident("rename") {
115                                if let Expr::Lit(ExprLit {
116                                    lit: Lit::Str(s), ..
117                                }) = nv.value
118                                {
119                                    final_name = s.value();
120                                }
121                            } else if nv.path.is_ident("rename_all") {
122                                if let Expr::Lit(ExprLit {
123                                    lit: Lit::Str(s), ..
124                                }) = nv.value
125                                {
126                                    rename_rule = Some(s.value());
127                                }
128                            }
129                        }
130                    }
131                }
132            }
133        }
134    }
135
136    // 2. Doc Comments (Higher Precedence)
137    for attr in attrs {
138        if attr.path().is_ident("doc") {
139            if let Meta::NameValue(meta) = &attr.meta {
140                if let Expr::Lit(expr_lit) = &meta.value {
141                    if let Lit::Str(lit_str) = &expr_lit.lit {
142                        let val = lit_str.value();
143                        doc_lines.push(val.clone());
144                        let trimmed = val.trim();
145
146                        if trimmed.starts_with("@openapi") {
147                            let rest = trimmed.strip_prefix("@openapi").unwrap().trim();
148                            if rest.starts_with("rename-all") {
149                                let rule = rest
150                                    .strip_prefix("rename-all")
151                                    .unwrap()
152                                    .trim()
153                                    .trim_matches('"');
154                                rename_rule = Some(rule.to_string());
155                            } else if rest.starts_with("rename") {
156                                let name_part = rest
157                                    .strip_prefix("rename")
158                                    .unwrap()
159                                    .trim()
160                                    .trim_matches('"');
161                                final_name = name_part.to_string();
162                            } else {
163                                // Only if not a rename directive, treat as doc content?
164                                // Actually, standard logic splits @openapi lines separate.
165                                // We just pass it through here.
166                            }
167                        } else {
168                            clean_doc_lines.push(val.trim().to_string());
169                        }
170                    }
171                }
172            }
173        }
174    }
175
176    (
177        final_name,
178        clean_doc_lines.join(" "),
179        rename_rule,
180        doc_lines,
181    )
182}