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 Head,
47 Options,
48 Trace,
49 }
50
51 impl HttpMethod {
52 pub fn as_str(&self) -> &'static str {
53 match self {
54 Self::Get => "GET",
55 Self::Post => "POST",
56 Self::Put => "PUT",
57 Self::Patch => "PATCH",
58 Self::Delete => "DELETE",
59 Self::Head => "HEAD",
60 Self::Options => "OPTIONS",
61 Self::Trace => "TRACE",
62 }
63 }
64 }
65
66 impl std::fmt::Display for HttpMethod {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.write_str(self.as_str())
69 }
70 }
71
72 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
74 pub enum ParamLocation {
75 Path,
76 Query,
77 Header,
78 }
79
80 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82 pub enum ParamType {
83 String,
84 Integer,
85 Number,
86 Boolean,
87 }
88
89 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91 pub enum BodyContentType {
92 Json,
93 FormUrlEncoded,
94 Multipart,
95 OctetStream,
96 TextPlain,
97 }
98
99 #[derive(Debug, Clone, serde::Serialize)]
105 pub struct ParamDef {
106 pub name: &'static str,
107 pub location: ParamLocation,
108 pub required: bool,
109 pub param_type: ParamType,
110 pub description: Option<&'static str>,
111 }
112
113 #[derive(Debug, Clone, serde::Serialize)]
117 pub struct BodyDef {
118 pub content_type: BodyContentType,
119 pub schema_name: Option<&'static str>,
121 }
122
123 #[derive(Debug, Clone, serde::Serialize)]
127 pub struct OperationDef {
128 pub id: &'static str,
130 pub method: HttpMethod,
132 pub path: &'static str,
134 pub summary: Option<&'static str>,
136 pub description: Option<&'static str>,
138 pub params: &'static [ParamDef],
140 pub body: Option<BodyDef>,
142 pub response_schema: Option<&'static str>,
144 }
145
146 pub fn find_operation(id: &str) -> Option<&'static OperationDef> {
148 OPERATIONS.iter().find(|op| op.id == id)
149 }
150
151 pub fn operation_ids() -> impl Iterator<Item = &'static str> {
153 OPERATIONS.iter().map(|op| op.id)
154 }
155 }
156 }
157
158 fn generate_operation_defs(&self, analysis: &SchemaAnalysis) -> TokenStream {
160 let mut param_statics: Vec<TokenStream> = Vec::new();
161 let mut op_entries: Vec<TokenStream> = Vec::new();
162
163 let mut sorted_ops: Vec<_> = analysis.operations.values().collect();
165 sorted_ops.sort_by_key(|op| &op.operation_id);
166
167 for op in sorted_ops {
168 let id = &op.operation_id;
169 let method = match op.method.to_uppercase().as_str() {
170 "GET" => quote! { HttpMethod::Get },
171 "POST" => quote! { HttpMethod::Post },
172 "PUT" => quote! { HttpMethod::Put },
173 "PATCH" => quote! { HttpMethod::Patch },
174 "DELETE" => quote! { HttpMethod::Delete },
175 "HEAD" => quote! { HttpMethod::Head },
176 "OPTIONS" => quote! { HttpMethod::Options },
177 "TRACE" => quote! { HttpMethod::Trace },
178 other => panic!(
179 "unsupported HTTP method `{other}` for op `{}`",
180 op.operation_id
181 ),
182 };
183 let path = &op.path;
184
185 let summary = match &op.summary {
186 Some(s) => quote! { Some(#s) },
187 None => quote! { None },
188 };
189 let description = match &op.description {
190 Some(d) => quote! { Some(#d) },
191 None => quote! { None },
192 };
193
194 let param_defs: Vec<TokenStream> = op
196 .parameters
197 .iter()
198 .map(|p| {
199 let name = &p.name;
200 let location = match p.location.as_str() {
201 "path" => quote! { ParamLocation::Path },
202 "query" => quote! { ParamLocation::Query },
203 "header" => quote! { ParamLocation::Header },
204 _ => quote! { ParamLocation::Query },
205 };
206 let required = p.required;
207 let param_type = match p.rust_type.as_str() {
208 "i64" | "i32" => quote! { ParamType::Integer },
209 "f64" => quote! { ParamType::Number },
210 "bool" => quote! { ParamType::Boolean },
211 _ => quote! { ParamType::String },
212 };
213 let desc = match &p.description {
214 Some(d) => quote! { Some(#d) },
215 None => quote! { None },
216 };
217 quote! {
218 ParamDef {
219 name: #name,
220 location: #location,
221 required: #required,
222 param_type: #param_type,
223 description: #desc,
224 }
225 }
226 })
227 .collect();
228
229 let sanitized_id: String = op
231 .operation_id
232 .chars()
233 .map(|c| {
234 if c.is_ascii_alphanumeric() {
235 c.to_ascii_uppercase()
236 } else {
237 '_'
238 }
239 })
240 .collect();
241 let params_static_name = syn::Ident::new(
242 &format!("PARAMS_{sanitized_id}"),
243 proc_macro2::Span::call_site(),
244 );
245 let param_count = param_defs.len();
246
247 param_statics.push(quote! {
249 static #params_static_name: [ParamDef; #param_count] = [#(#param_defs),*];
250 });
251
252 let body = match &op.request_body {
254 Some(rb) => {
255 use crate::analysis::RequestBodyContent;
256 let (content_type, schema_name) = match rb {
257 RequestBodyContent::Json { schema_name } => (
258 quote! { BodyContentType::Json },
259 quote! { Some(#schema_name) },
260 ),
261 RequestBodyContent::FormUrlEncoded { schema_name } => (
262 quote! { BodyContentType::FormUrlEncoded },
263 quote! { Some(#schema_name) },
264 ),
265 RequestBodyContent::Multipart => {
266 (quote! { BodyContentType::Multipart }, quote! { None })
267 }
268 RequestBodyContent::OctetStream => {
269 (quote! { BodyContentType::OctetStream }, quote! { None })
270 }
271 RequestBodyContent::TextPlain => {
272 (quote! { BodyContentType::TextPlain }, quote! { None })
273 }
274 };
275 quote! {
276 Some(BodyDef {
277 content_type: #content_type,
278 schema_name: #schema_name,
279 })
280 }
281 }
282 None => quote! { None },
283 };
284
285 let response_schema = op
287 .response_schemas
288 .get("200")
289 .or_else(|| op.response_schemas.get("201"))
290 .or_else(|| {
291 op.response_schemas
292 .iter()
293 .find(|(code, _)| code.starts_with('2'))
294 .map(|(_, v)| v)
295 });
296 let response_schema_token = match response_schema {
297 Some(s) => quote! { Some(#s) },
298 None => quote! { None },
299 };
300
301 op_entries.push(quote! {
302 OperationDef {
303 id: #id,
304 method: #method,
305 path: #path,
306 summary: #summary,
307 description: #description,
308 params: &#params_static_name,
309 body: #body,
310 response_schema: #response_schema_token,
311 }
312 });
313 }
314
315 let op_count = op_entries.len();
316 quote! {
317 #(#param_statics)*
318
319 pub static OPERATIONS: [OperationDef; #op_count] = [#(#op_entries),*];
320 }
321 }
322}