1use crate::analysis::SchemaAnalysis;
12use crate::generator::CodeGenerator;
13use proc_macro2::TokenStream;
14use quote::quote;
15
16impl CodeGenerator {
17 pub fn generate_registry(&self, analysis: &SchemaAnalysis) -> crate::Result<String> {
19 let registry_types = Self::generate_registry_types();
20 let operation_defs = self.generate_operation_defs(analysis);
21
22 let tokens = quote! {
23 #registry_types
26 #operation_defs
27 };
28
29 let file = syn::parse2(tokens).map_err(|e| {
30 crate::GeneratorError::CodeGenError(format!("Failed to parse registry tokens: {}", e))
31 })?;
32 Ok(prettyplease::unparse(&file))
33 }
34
35 fn generate_registry_types() -> TokenStream {
37 quote! {
38 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40 pub enum HttpMethod {
41 Get,
42 Post,
43 Put,
44 Patch,
45 Delete,
46 }
47
48 impl HttpMethod {
49 pub fn as_str(&self) -> &'static str {
50 match self {
51 Self::Get => "GET",
52 Self::Post => "POST",
53 Self::Put => "PUT",
54 Self::Patch => "PATCH",
55 Self::Delete => "DELETE",
56 }
57 }
58 }
59
60 impl std::fmt::Display for HttpMethod {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(self.as_str())
63 }
64 }
65
66 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
68 pub enum ParamLocation {
69 Path,
70 Query,
71 Header,
72 }
73
74 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
76 pub enum ParamType {
77 String,
78 Integer,
79 Number,
80 Boolean,
81 }
82
83 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85 pub enum BodyContentType {
86 Json,
87 FormUrlEncoded,
88 Multipart,
89 OctetStream,
90 TextPlain,
91 }
92
93 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
95 pub struct ParamDef {
96 pub name: &'static str,
97 pub location: ParamLocation,
98 pub required: bool,
99 pub param_type: ParamType,
100 pub description: Option<&'static str>,
101 }
102
103 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
105 pub struct BodyDef {
106 pub content_type: BodyContentType,
107 pub schema_name: Option<&'static str>,
109 }
110
111 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
113 pub struct OperationDef {
114 pub id: &'static str,
116 pub method: HttpMethod,
118 pub path: &'static str,
120 pub summary: Option<&'static str>,
122 pub description: Option<&'static str>,
124 pub params: &'static [ParamDef],
126 pub body: Option<BodyDef>,
128 pub response_schema: Option<&'static str>,
130 }
131
132 pub fn find_operation(id: &str) -> Option<&'static OperationDef> {
134 OPERATIONS.iter().find(|op| op.id == id)
135 }
136
137 pub fn operation_ids() -> impl Iterator<Item = &'static str> {
139 OPERATIONS.iter().map(|op| op.id)
140 }
141 }
142 }
143
144 fn generate_operation_defs(&self, analysis: &SchemaAnalysis) -> TokenStream {
146 let mut param_statics: Vec<TokenStream> = Vec::new();
147 let mut op_entries: Vec<TokenStream> = Vec::new();
148
149 let mut sorted_ops: Vec<_> = analysis.operations.values().collect();
151 sorted_ops.sort_by_key(|op| &op.operation_id);
152
153 for op in sorted_ops {
154 let id = &op.operation_id;
155 let method = match op.method.as_str() {
156 "GET" => quote! { HttpMethod::Get },
157 "POST" => quote! { HttpMethod::Post },
158 "PUT" => quote! { HttpMethod::Put },
159 "PATCH" => quote! { HttpMethod::Patch },
160 "DELETE" => quote! { HttpMethod::Delete },
161 _ => quote! { HttpMethod::Get },
162 };
163 let path = &op.path;
164
165 let summary = match &op.summary {
166 Some(s) => quote! { Some(#s) },
167 None => quote! { None },
168 };
169 let description = match &op.description {
170 Some(d) => quote! { Some(#d) },
171 None => quote! { None },
172 };
173
174 let param_defs: Vec<TokenStream> = op
176 .parameters
177 .iter()
178 .map(|p| {
179 let name = &p.name;
180 let location = match p.location.as_str() {
181 "path" => quote! { ParamLocation::Path },
182 "query" => quote! { ParamLocation::Query },
183 "header" => quote! { ParamLocation::Header },
184 _ => quote! { ParamLocation::Query },
185 };
186 let required = p.required;
187 let param_type = match p.rust_type.as_str() {
188 "i64" | "i32" => quote! { ParamType::Integer },
189 "f64" => quote! { ParamType::Number },
190 "bool" => quote! { ParamType::Boolean },
191 _ => quote! { ParamType::String },
192 };
193 let desc = match &p.description {
194 Some(d) => quote! { Some(#d) },
195 None => quote! { None },
196 };
197 quote! {
198 ParamDef {
199 name: #name,
200 location: #location,
201 required: #required,
202 param_type: #param_type,
203 description: #desc,
204 }
205 }
206 })
207 .collect();
208
209 let sanitized_id: String = op
211 .operation_id
212 .chars()
213 .map(|c| {
214 if c.is_ascii_alphanumeric() {
215 c.to_ascii_uppercase()
216 } else {
217 '_'
218 }
219 })
220 .collect();
221 let params_static_name = syn::Ident::new(
222 &format!("PARAMS_{sanitized_id}"),
223 proc_macro2::Span::call_site(),
224 );
225 let param_count = param_defs.len();
226
227 param_statics.push(quote! {
229 static #params_static_name: [ParamDef; #param_count] = [#(#param_defs),*];
230 });
231
232 let body = match &op.request_body {
234 Some(rb) => {
235 use crate::analysis::RequestBodyContent;
236 let (content_type, schema_name) = match rb {
237 RequestBodyContent::Json { schema_name } => (
238 quote! { BodyContentType::Json },
239 quote! { Some(#schema_name) },
240 ),
241 RequestBodyContent::FormUrlEncoded { schema_name } => (
242 quote! { BodyContentType::FormUrlEncoded },
243 quote! { Some(#schema_name) },
244 ),
245 RequestBodyContent::Multipart => {
246 (quote! { BodyContentType::Multipart }, quote! { None })
247 }
248 RequestBodyContent::OctetStream => {
249 (quote! { BodyContentType::OctetStream }, quote! { None })
250 }
251 RequestBodyContent::TextPlain => {
252 (quote! { BodyContentType::TextPlain }, quote! { None })
253 }
254 };
255 quote! {
256 Some(BodyDef {
257 content_type: #content_type,
258 schema_name: #schema_name,
259 })
260 }
261 }
262 None => quote! { None },
263 };
264
265 let response_schema = op
267 .response_schemas
268 .get("200")
269 .or_else(|| op.response_schemas.get("201"))
270 .or_else(|| {
271 op.response_schemas
272 .iter()
273 .find(|(code, _)| code.starts_with('2'))
274 .map(|(_, v)| v)
275 });
276 let response_schema_token = match response_schema {
277 Some(s) => quote! { Some(#s) },
278 None => quote! { None },
279 };
280
281 op_entries.push(quote! {
282 OperationDef {
283 id: #id,
284 method: #method,
285 path: #path,
286 summary: #summary,
287 description: #description,
288 params: &#params_static_name,
289 body: #body,
290 response_schema: #response_schema_token,
291 }
292 });
293 }
294
295 let op_count = op_entries.len();
296 quote! {
297 #(#param_statics)*
298
299 pub static OPERATIONS: [OperationDef; #op_count] = [#(#op_entries),*];
300 }
301 }
302}