1use crate::analysis::SchemaAnalysis;
12use crate::generator::CodeGenerator;
13use proc_macro2::TokenStream;
14use quote::{format_ident, quote};
15use std::collections::BTreeMap;
16
17impl CodeGenerator {
18 pub fn generate_registry(&self, analysis: &SchemaAnalysis) -> crate::Result<String> {
20 let provenance_attribute = self.provenance_attribute();
21 let custom_methods = registry_custom_methods(analysis);
22 let registry_types = Self::generate_registry_types(&custom_methods);
23 let operation_defs = self.generate_operation_defs(analysis, &custom_methods);
24
25 let tokens = quote! {
26 #provenance_attribute
29
30 #registry_types
31 #operation_defs
32 };
33
34 let file = syn::parse2(tokens).map_err(|e| {
35 crate::GeneratorError::CodeGenError(format!("Failed to parse registry tokens: {}", e))
36 })?;
37 Ok(prettyplease::unparse(&file))
38 }
39
40 fn generate_registry_types(custom_methods: &BTreeMap<String, syn::Ident>) -> TokenStream {
42 let custom_variants = custom_methods.values();
43 let custom_as_str = custom_methods.iter().map(|(method, variant)| {
44 quote! { Self::#variant => #method }
45 });
46 quote! {
47 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49 pub enum HttpMethod {
50 Get,
51 Post,
52 Put,
53 Patch,
54 Delete,
55 Head,
56 Options,
57 Trace,
58 Connect,
59 Query,
60 #(#custom_variants),*
61 }
62
63 impl HttpMethod {
64 pub fn as_str(&self) -> &'static str {
65 match self {
66 Self::Get => "GET",
67 Self::Post => "POST",
68 Self::Put => "PUT",
69 Self::Patch => "PATCH",
70 Self::Delete => "DELETE",
71 Self::Head => "HEAD",
72 Self::Options => "OPTIONS",
73 Self::Trace => "TRACE",
74 Self::Connect => "CONNECT",
75 Self::Query => "QUERY",
76 #(#custom_as_str),*
77 }
78 }
79 }
80
81 impl std::fmt::Display for HttpMethod {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.write_str(self.as_str())
84 }
85 }
86
87 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
89 pub enum ParamLocation {
90 Path,
91 Query,
92 Header,
93 }
94
95 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97 pub enum ParamType {
98 String,
99 Integer,
100 Number,
101 Boolean,
102 }
103
104 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106 pub enum BodyContentType {
107 Json,
108 FormUrlEncoded,
109 Multipart,
110 OctetStream,
111 TextPlain,
112 }
113
114 #[derive(Debug, Clone, serde::Serialize)]
120 pub struct ParamDef {
121 pub name: &'static str,
122 pub location: ParamLocation,
123 pub required: bool,
124 pub param_type: ParamType,
125 pub description: Option<&'static str>,
126 }
127
128 #[derive(Debug, Clone, serde::Serialize)]
132 pub struct BodyDef {
133 pub content_type: BodyContentType,
134 pub schema_name: Option<&'static str>,
136 }
137
138 #[derive(Debug, Clone, serde::Serialize)]
142 pub struct OperationDef {
143 pub id: &'static str,
145 pub method: HttpMethod,
147 pub path: &'static str,
149 pub summary: Option<&'static str>,
151 pub description: Option<&'static str>,
153 pub params: &'static [ParamDef],
155 pub body: Option<BodyDef>,
157 pub response_schema: Option<&'static str>,
159 }
160
161 pub fn find_operation(id: &str) -> Option<&'static OperationDef> {
163 OPERATIONS.iter().find(|op| op.id == id)
164 }
165
166 pub fn operation_ids() -> impl Iterator<Item = &'static str> {
168 OPERATIONS.iter().map(|op| op.id)
169 }
170 }
171 }
172
173 fn generate_operation_defs(
175 &self,
176 analysis: &SchemaAnalysis,
177 custom_methods: &BTreeMap<String, syn::Ident>,
178 ) -> TokenStream {
179 let mut param_statics: Vec<TokenStream> = Vec::new();
180 let mut op_entries: Vec<TokenStream> = Vec::new();
181
182 let mut sorted_ops: Vec<_> = analysis.operations.values().collect();
184 sorted_ops.sort_by_key(|op| &op.operation_id);
185
186 for op in sorted_ops {
187 let id = &op.operation_id;
188 let method = match op.method.to_uppercase().as_str() {
189 "GET" => quote! { HttpMethod::Get },
190 "POST" => quote! { HttpMethod::Post },
191 "PUT" => quote! { HttpMethod::Put },
192 "PATCH" => quote! { HttpMethod::Patch },
193 "DELETE" => quote! { HttpMethod::Delete },
194 "HEAD" => quote! { HttpMethod::Head },
195 "OPTIONS" => quote! { HttpMethod::Options },
196 "TRACE" => quote! { HttpMethod::Trace },
197 "CONNECT" => quote! { HttpMethod::Connect },
198 "QUERY" => quote! { HttpMethod::Query },
199 other => {
200 let variant = &custom_methods[other];
201 quote! { HttpMethod::#variant }
202 }
203 };
204 let path = &op.path;
205
206 let summary = match &op.summary {
207 Some(s) => quote! { Some(#s) },
208 None => quote! { None },
209 };
210 let description = match &op.description {
211 Some(d) => quote! { Some(#d) },
212 None => quote! { None },
213 };
214
215 let param_defs: Vec<TokenStream> = op
217 .parameters
218 .iter()
219 .map(|p| {
220 let name = &p.name;
221 let location = match p.location.as_str() {
222 "path" => quote! { ParamLocation::Path },
223 "query" => quote! { ParamLocation::Query },
224 "header" => quote! { ParamLocation::Header },
225 _ => quote! { ParamLocation::Query },
226 };
227 let required = p.required;
228 let param_type = match p.rust_type.as_str() {
229 "i64" | "i32" => quote! { ParamType::Integer },
230 "f64" => quote! { ParamType::Number },
231 "bool" => quote! { ParamType::Boolean },
232 _ => quote! { ParamType::String },
233 };
234 let desc = match &p.description {
235 Some(d) => quote! { Some(#d) },
236 None => quote! { None },
237 };
238 quote! {
239 ParamDef {
240 name: #name,
241 location: #location,
242 required: #required,
243 param_type: #param_type,
244 description: #desc,
245 }
246 }
247 })
248 .collect();
249
250 let sanitized_id: String = op
252 .operation_id
253 .chars()
254 .map(|c| {
255 if c.is_ascii_alphanumeric() {
256 c.to_ascii_uppercase()
257 } else {
258 '_'
259 }
260 })
261 .collect();
262 let params_static_name = syn::Ident::new(
263 &format!("PARAMS_{sanitized_id}"),
264 proc_macro2::Span::call_site(),
265 );
266 let param_count = param_defs.len();
267
268 param_statics.push(quote! {
270 static #params_static_name: [ParamDef; #param_count] = [#(#param_defs),*];
271 });
272
273 let body = match &op.request_body {
275 Some(rb) => {
276 use crate::analysis::RequestBodyContent;
277 let (content_type, schema_name) = match rb {
278 RequestBodyContent::Json { schema_name } => (
279 quote! { BodyContentType::Json },
280 quote! { Some(#schema_name) },
281 ),
282 RequestBodyContent::FormUrlEncoded { schema_name } => (
283 quote! { BodyContentType::FormUrlEncoded },
284 quote! { Some(#schema_name) },
285 ),
286 RequestBodyContent::Multipart => {
287 (quote! { BodyContentType::Multipart }, quote! { None })
288 }
289 RequestBodyContent::OctetStream => {
290 (quote! { BodyContentType::OctetStream }, quote! { None })
291 }
292 RequestBodyContent::TextPlain => {
293 (quote! { BodyContentType::TextPlain }, quote! { None })
294 }
295 };
296 quote! {
297 Some(BodyDef {
298 content_type: #content_type,
299 schema_name: #schema_name,
300 })
301 }
302 }
303 None => quote! { None },
304 };
305
306 let response_schema = op
308 .response_schemas
309 .get("200")
310 .or_else(|| op.response_schemas.get("201"))
311 .or_else(|| {
312 op.response_schemas
313 .iter()
314 .find(|(code, _)| code.starts_with('2'))
315 .map(|(_, v)| v)
316 });
317 let response_schema_token = match response_schema {
318 Some(s) => quote! { Some(#s) },
319 None => quote! { None },
320 };
321
322 op_entries.push(quote! {
323 OperationDef {
324 id: #id,
325 method: #method,
326 path: #path,
327 summary: #summary,
328 description: #description,
329 params: &#params_static_name,
330 body: #body,
331 response_schema: #response_schema_token,
332 }
333 });
334 }
335
336 let op_count = op_entries.len();
337 quote! {
338 #(#param_statics)*
339
340 pub static OPERATIONS: [OperationDef; #op_count] = [#(#op_entries),*];
341 }
342 }
343}
344
345fn registry_custom_methods(analysis: &SchemaAnalysis) -> BTreeMap<String, syn::Ident> {
349 const STANDARD: &[&str] = &[
350 "CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY", "TRACE",
351 ];
352 analysis
353 .operations
354 .values()
355 .map(|operation| operation.method.to_ascii_uppercase())
356 .filter(|method| !STANDARD.contains(&method.as_str()))
357 .collect::<std::collections::BTreeSet<_>>()
358 .into_iter()
359 .enumerate()
360 .map(|(index, method)| (method, format_ident!("Custom{index}")))
361 .collect()
362}