1use std::collections::HashSet;
2
3use minijinja::{Value, context};
4
5use crate::ir::{
6 HttpMethod, IrEnumVariant, IrObjectSchema, IrOperation, IrParameterLocation, IrReturnType,
7 IrSchema, IrSpec, IrType,
8};
9
10use super::pack::TypeMapConfig;
11use super::type_map::{map_field_type, map_type};
12
13struct ParamsResult {
14 path_params: Vec<Value>,
15 query_params: Vec<Value>,
16 py_params: Vec<Value>,
17 header_params_obj: String,
18 has_body: bool,
19 body_content_type: String,
20 has_path_params: bool,
21 has_query_params: bool,
22 has_header_params: bool,
23}
24
25pub fn build_context(
29 ir: &IrSpec,
30 tm: &TypeMapConfig,
31 field_casing: &str,
32 operation_casing: &str,
33) -> Value {
34 let schemas = build_schema_contexts(ir, tm, field_casing);
35 let schema_names: HashSet<String> = ir
36 .schemas
37 .iter()
38 .map(|s| match s {
39 IrSchema::Object(o) => o.name.pascal_case.clone(),
40 IrSchema::Enum(e) => e.name.pascal_case.clone(),
41 IrSchema::Alias(a) => a.name.pascal_case.clone(),
42 IrSchema::Union(u) => u.name.pascal_case.clone(),
43 })
44 .collect();
45 let sse_event_types = collect_sse_event_types(ir, &schema_names, tm);
46 let guards = build_guard_contexts(ir);
47
48 let (operations, used_op_indices) =
50 build_operation_contexts(ir, tm, field_casing, operation_casing);
51
52 let (hooks, hook_op_indices) = build_hook_contexts(ir, tm, operation_casing);
54
55 let imported_types = collect_imported_types(
57 ir.operations
58 .iter()
59 .enumerate()
60 .filter(|(i, _)| used_op_indices.contains(i))
61 .map(|(_, op)| op),
62 tm,
63 );
64
65 let hook_imported_types = collect_imported_types(
66 ir.operations
67 .iter()
68 .enumerate()
69 .filter(|(i, _)| hook_op_indices.contains(i))
70 .map(|(_, op)| op),
71 tm,
72 );
73
74 let model_imports = collect_model_imports(ir);
76
77 let has_sse = operations.iter().any(|op| {
78 op.get_attr("kind")
79 .ok()
80 .is_some_and(|v| v.as_str() == Some("sse"))
81 });
82 let has_queries = hooks.iter().any(|h| {
83 h.get_attr("kind")
84 .ok()
85 .is_some_and(|v| v.as_str() == Some("query"))
86 });
87 let has_mutations = hooks.iter().any(|h| {
88 h.get_attr("kind")
89 .ok()
90 .is_some_and(|v| v.as_str() == Some("mutation"))
91 });
92 let has_hook_sse = hooks.iter().any(|h| {
93 h.get_attr("kind")
94 .ok()
95 .is_some_and(|v| v.as_str() == Some("sse"))
96 });
97
98 let (test_operations, test_op_indices) =
100 build_test_operation_contexts(ir, tm, operation_casing);
101 let test_type_imports = collect_test_type_imports(
102 ir.operations
103 .iter()
104 .enumerate()
105 .filter(|(i, _)| test_op_indices.contains(i))
106 .map(|(_, op)| op),
107 );
108 let (py_test_operations, py_model_imports) = build_python_test_contexts(ir);
109 let hook_names = build_hook_names(ir);
110
111 let guard_imports = guards
113 .get_attr("imports")
114 .unwrap_or(Value::from(Vec::<String>::new()));
115 let guard_items = guards
116 .get_attr("guards")
117 .unwrap_or(Value::from(Vec::<Value>::new()));
118
119 context! {
120 title => ir.info.title.clone(),
121 schemas => schemas,
122 sse_event_types => sse_event_types,
123 imports => guard_imports,
125 guards => guard_items,
126 operations => operations,
127 hooks => hooks,
128 imported_types => imported_types,
129 hook_imported_types => hook_imported_types,
130 model_imports => model_imports,
131 has_sse => has_sse,
132 has_queries => has_queries,
133 has_mutations => has_mutations,
134 has_hook_sse => has_hook_sse,
135 test_operations => test_operations,
137 test_type_imports => test_type_imports,
138 py_test_operations => py_test_operations,
139 py_model_imports => py_model_imports,
140 hook_names => hook_names,
141 }
142}
143
144fn build_schema_contexts(ir: &IrSpec, tm: &TypeMapConfig, field_casing: &str) -> Vec<Value> {
147 ir.schemas
148 .iter()
149 .map(|s| schema_to_ctx(s, tm, field_casing))
150 .collect()
151}
152
153fn schema_to_ctx(schema: &IrSchema, tm: &TypeMapConfig, field_casing: &str) -> Value {
154 match schema {
155 IrSchema::Object(obj) => object_to_ctx(obj, tm, field_casing),
156 IrSchema::Enum(e) => {
157 let is_integer = !e.variants.is_empty()
158 && e.variants
159 .iter()
160 .all(|v| matches!(v, IrEnumVariant::Integer(_)));
161
162 let ts_variants: Vec<String> = e
164 .variants
165 .iter()
166 .map(|v| match v {
167 IrEnumVariant::String(s) => format!("\"{s}\""),
168 IrEnumVariant::Integer(i) => i.to_string(),
169 })
170 .collect();
171
172 let py_variants: Vec<Value> = e
174 .variants
175 .iter()
176 .map(|v| match v {
177 IrEnumVariant::String(s) => context! {
178 name => heck::AsUpperCamelCase(s).to_string(),
179 value => format!("\"{}\"", s),
180 },
181 IrEnumVariant::Integer(i) => {
182 let name = if *i < 0 {
183 format!("Neg{}", i.unsigned_abs())
184 } else {
185 format!("Value{i}")
186 };
187 context! {
188 name => name,
189 value => i.to_string(),
190 }
191 }
192 })
193 .collect();
194
195 context! {
196 kind => "enum",
197 name => e.name.pascal_case.clone(),
198 description => e.description.clone(),
199 variants => ts_variants,
200 py_variants => py_variants,
201 is_integer => is_integer,
202 }
203 }
204 IrSchema::Alias(a) => {
205 context! {
206 kind => "alias",
207 name => a.name.pascal_case.clone(),
208 description => a.description.clone(),
209 target => map_type(&a.target, tm),
210 }
211 }
212 IrSchema::Union(u) => {
213 let variants: Vec<String> = u.variants.iter().map(|v| map_type(v, tm)).collect();
214 context! {
215 kind => "union",
216 name => u.name.pascal_case.clone(),
217 description => u.description.clone(),
218 variants => variants,
219 }
220 }
221 }
222}
223
224fn object_to_ctx(obj: &IrObjectSchema, tm: &TypeMapConfig, field_casing: &str) -> Value {
225 let fields: Vec<Value> = obj
226 .fields
227 .iter()
228 .map(|f| {
229 let field_name = match field_casing {
230 "snake" => f.name.snake_case.clone(),
231 "pascal" => f.name.pascal_case.clone(),
232 _ => f.name.camel_case.clone(),
233 };
234 let type_str = map_type(&f.field_type, tm);
235 let field_type_str = map_field_type(&f.field_type, f.required, tm);
236 context! {
237 name => field_name,
238 original_name => f.original_name.clone(),
239 type_str => type_str,
240 field_type_str => field_type_str,
241 required => f.required,
242 description => f.description.clone(),
243 needs_alias => f.name.snake_case != f.original_name,
244 }
245 })
246 .collect();
247
248 let additional = obj.additional_properties.as_ref().map(|t| map_type(t, tm));
249 let has_additional_properties = obj.additional_properties.is_some();
250
251 context! {
252 kind => "object",
253 name => obj.name.pascal_case.clone(),
254 description => obj.description.clone(),
255 fields => fields,
256 additional_properties => additional,
257 has_additional_properties => has_additional_properties,
258 }
259}
260
261fn collect_sse_event_types(
262 ir: &IrSpec,
263 schema_names: &HashSet<String>,
264 tm: &TypeMapConfig,
265) -> Vec<Value> {
266 let mut event_types = Vec::new();
267 let mut seen = HashSet::new();
268 for op in &ir.operations {
269 if let IrReturnType::Sse(sse) = &op.return_type
270 && let Some(ref event_name) = sse.event_type_name
271 {
272 if seen.contains(event_name) || schema_names.contains(event_name) {
273 continue;
274 }
275 let variants: Vec<String> = sse.variants.iter().map(|v| map_type(v, tm)).collect();
276 if !variants.is_empty() {
277 seen.insert(event_name.clone());
278 event_types.push(context! {
279 name => event_name.clone(),
280 variants => variants,
281 });
282 }
283 }
284 }
285 event_types
286}
287
288fn build_guard_contexts(ir: &IrSpec) -> Value {
291 let mut guards = Vec::new();
292 let mut import_names: Vec<String> = Vec::new();
293
294 for schema in &ir.schemas {
295 if let IrSchema::Union(u) = schema
296 && let Some(disc) = &u.discriminator
297 {
298 let union_name = u.name.pascal_case.clone();
299 if !import_names.contains(&union_name) {
300 import_names.push(union_name.clone());
301 }
302
303 for (disc_value, schema_name) in &disc.mapping {
304 if u.variants
305 .iter()
306 .any(|v| matches!(v, IrType::Ref(n) if n == schema_name))
307 {
308 if !import_names.contains(schema_name) {
309 import_names.push(schema_name.clone());
310 }
311 let is_integer_discriminator = disc_value.parse::<i64>().is_ok();
312 guards.push(context! {
313 union_name => union_name.clone(),
314 variant_name => schema_name.clone(),
315 property_name => disc.property_name.clone(),
316 discriminator_value => disc_value.clone(),
317 is_integer_discriminator => is_integer_discriminator,
318 });
319 }
320 }
321 }
322 }
323
324 context! {
325 imports => import_names,
326 guards => guards,
327 }
328}
329
330fn op_name(op: &IrOperation, casing: &str) -> String {
333 match casing {
334 "snake" => op.name.snake_case.clone(),
335 "pascal" => op.name.pascal_case.clone(),
336 _ => op.name.camel_case.clone(),
337 }
338}
339
340fn build_operation_contexts(
341 ir: &IrSpec,
342 tm: &TypeMapConfig,
343 field_casing: &str,
344 operation_casing: &str,
345) -> (Vec<Value>, HashSet<usize>) {
346 let mut seen_methods = HashSet::new();
347 let mut used_op_indices = HashSet::new();
348
349 let operations: Vec<Value> = ir
350 .operations
351 .iter()
352 .enumerate()
353 .flat_map(|(idx, op)| {
354 build_single_operation_contexts(op, tm, field_casing, operation_casing)
355 .into_iter()
356 .map(move |ctx| (idx, ctx))
357 })
358 .filter(|(idx, ctx)| {
359 let name = ctx
360 .get_attr("method_name")
361 .ok()
362 .and_then(|v| v.as_str().map(String::from));
363 match name {
364 Some(n) => {
365 if seen_methods.insert(n) {
366 used_op_indices.insert(*idx);
367 true
368 } else {
369 false
370 }
371 }
372 None => true,
373 }
374 })
375 .map(|(_, ctx)| ctx)
376 .collect();
377
378 (operations, used_op_indices)
379}
380
381fn classify_param_type(ir_type: &IrType) -> &'static str {
382 match ir_type {
383 IrType::Array(_) => "array",
384 IrType::Map(_) => "map",
385 IrType::Object(_) | IrType::Ref(_) => "object",
386 _ => "primitive",
387 }
388}
389
390fn build_params(op: &IrOperation, tm: &TypeMapConfig, field_casing: &str) -> ParamsResult {
392 let mut required_parts = Vec::new();
393 let mut optional_parts = Vec::new();
394 let mut path_params = Vec::new();
395 let mut query_params = Vec::new();
396 let mut header_parts = Vec::new();
397
398 let param_name = |op_param: &crate::ir::IrParameter| -> String {
399 match field_casing {
400 "snake" => op_param.name.snake_case.clone(),
401 "pascal" => op_param.name.pascal_case.clone(),
402 _ => op_param.name.camel_case.clone(),
403 }
404 };
405
406 for param in &op.parameters {
407 let type_str = map_type(¶m.param_type, tm);
408 let name = param_name(param);
409 match param.location {
410 IrParameterLocation::Path => {
411 required_parts.push(format!("{name}: {type_str}"));
412 path_params.push(context! {
413 name => name,
414 original_name => param.original_name.clone(),
415 });
416 }
417 IrParameterLocation::Query => {
418 if param.required {
419 required_parts.push(format!("{name}: {type_str}"));
420 } else {
421 optional_parts.push(format!("{name}?: {type_str}"));
422 }
423 let type_kind = classify_param_type(¶m.param_type);
424 let style = param.style.clone().unwrap_or_else(|| "form".to_string());
425 let explode = param.explode.unwrap_or(style == "form");
426 query_params.push(context! {
427 name => name,
428 original_name => param.original_name.clone(),
429 style => style,
430 explode => explode,
431 type_kind => type_kind,
432 });
433 }
434 IrParameterLocation::Header => {
435 if param.required {
436 required_parts.push(format!("{name}: {type_str}"));
437 } else {
438 optional_parts.push(format!("{name}?: {type_str}"));
439 }
440 header_parts.push(format!("\"{}\": {}", param.original_name, name));
441 }
442 _ => {}
443 }
444 }
445
446 let has_body = op.request_body.is_some();
447 let body_content_type = op
448 .request_body
449 .as_ref()
450 .map(|b| b.content_type.clone())
451 .unwrap_or_else(|| "application/json".to_string());
452
453 if let Some(ref body) = op.request_body {
454 let type_str = map_type(&body.body_type, tm);
455 if body.required {
456 required_parts.push(format!("body: {type_str}"));
457 } else {
458 optional_parts.push(format!("body?: {type_str}"));
459 }
460 }
461
462 optional_parts.push("options?: RequestOptions".to_string());
463
464 let mut parts = required_parts;
465 parts.extend(optional_parts);
466
467 let has_path_params = !path_params.is_empty();
468 let has_query_params = !query_params.is_empty();
469 let has_header_params = !header_parts.is_empty();
470 let header_params_obj = header_parts.join(", ");
471 let _params_signature = parts.join(", ");
472
473 let py_params: Vec<Value> = op
475 .parameters
476 .iter()
477 .map(|param| {
478 let type_str = map_type(¶m.param_type, tm);
479 let location = match param.location {
480 IrParameterLocation::Path => "path",
481 IrParameterLocation::Query => "query",
482 IrParameterLocation::Header => "header",
483 IrParameterLocation::Cookie => "cookie",
484 };
485 let name = param_name(param);
486 context! {
487 name => name,
488 original_name => param.original_name.clone(),
489 type_str => type_str,
490 location => location,
491 required => param.required,
492 needs_alias => param.name.snake_case != param.original_name,
493 }
494 })
495 .collect();
496
497 ParamsResult {
498 path_params,
499 query_params,
500 py_params,
501 header_params_obj,
502 has_body,
503 body_content_type,
504 has_path_params,
505 has_query_params,
506 has_header_params,
507 }
508}
509
510fn is_multipart_op(op: &IrOperation) -> bool {
511 op.request_body
512 .as_ref()
513 .is_some_and(|b| b.content_type == "multipart/form-data")
514}
515
516fn build_single_operation_contexts(
517 op: &IrOperation,
518 tm: &TypeMapConfig,
519 field_casing: &str,
520 operation_casing: &str,
521) -> Vec<Value> {
522 let mut results = Vec::new();
523 let method_name = op_name(op, operation_casing);
524 let http_method = op.method.as_str();
525 let path = op.path.clone();
526
527 let ParamsResult {
529 path_params,
530 query_params,
531 py_params,
532 header_params_obj,
533 has_body,
534 body_content_type,
535 has_path_params,
536 has_query_params,
537 has_header_params,
538 } = build_params(op, tm, field_casing);
539
540 let params_signature = build_ts_params_signature(op, tm, field_casing, false);
542
543 let body_type = op
544 .request_body
545 .as_ref()
546 .map(|b| map_type(&b.body_type, tm))
547 .unwrap_or_default();
548 let body_param_name = "body".to_string();
549
550 match &op.return_type {
551 IrReturnType::Standard(resp) => {
552 let return_type = map_type(&resp.response_type, tm);
553 results.push(context! {
554 kind => "standard",
555 method_name => method_name,
556 name => op.name.snake_case.clone(),
557 http_method => http_method,
558 path => path,
559 params_signature => params_signature,
560 return_type => return_type,
561 path_params => path_params,
562 query_params => query_params,
563 params => py_params,
564 header_params_obj => header_params_obj,
565 has_body => has_body,
566 body_type => body_type,
567 body_param_name => body_param_name,
568 body_content_type => body_content_type,
569 is_multipart => is_multipart_op(op),
570 has_path_params => has_path_params,
571 has_query_params => has_query_params,
572 has_header_params => has_header_params,
573 summary => op.summary.clone(),
574 description => op.description.clone(),
575 deprecated => op.deprecated,
576 });
577 }
578 IrReturnType::Void => {
579 results.push(context! {
580 kind => "void",
581 method_name => method_name,
582 name => op.name.snake_case.clone(),
583 http_method => http_method,
584 path => path,
585 params_signature => params_signature,
586 return_type => map_type(&IrType::Void, tm),
587 path_params => path_params,
588 query_params => query_params,
589 params => py_params,
590 header_params_obj => header_params_obj,
591 has_body => has_body,
592 body_type => body_type,
593 body_param_name => body_param_name,
594 body_content_type => body_content_type,
595 is_multipart => is_multipart_op(op),
596 has_path_params => has_path_params,
597 has_query_params => has_query_params,
598 has_header_params => has_header_params,
599 summary => op.summary.clone(),
600 description => op.description.clone(),
601 deprecated => op.deprecated,
602 });
603 }
604 IrReturnType::Sse(sse) => {
605 let return_type = if let Some(ref name) = sse.event_type_name {
606 name.clone()
607 } else {
608 map_type(&sse.event_type, tm)
609 };
610 let sse_name = if sse.also_has_json {
611 format!("{}Stream", op.name.camel_case)
612 } else {
613 op.name.camel_case.clone()
614 };
615
616 let sse_params_sig = build_ts_params_signature(op, tm, field_casing, true);
618
619 results.push(context! {
620 kind => "sse",
621 method_name => sse_name,
622 name => op.name.snake_case.clone(),
623 http_method => http_method,
624 path => path,
625 params_signature => sse_params_sig,
626 return_type => return_type,
627 event_type => return_type,
628 path_params => path_params,
629 query_params => query_params.clone(),
630 params => py_params.clone(),
631 header_params_obj => header_params_obj.clone(),
632 has_body => has_body,
633 body_type => body_type.clone(),
634 body_param_name => body_param_name.clone(),
635 body_content_type => body_content_type.clone(),
636 is_multipart => is_multipart_op(op),
637 has_path_params => has_path_params,
638 has_query_params => has_query_params,
639 has_header_params => has_header_params,
640 summary => op.summary.clone(),
641 description => op.description.clone(),
642 deprecated => op.deprecated,
643 });
644
645 if let Some(ref json_resp) = sse.json_response {
646 let json_return_type = map_type(&json_resp.response_type, tm);
647 let json_desc = format!(
648 "{} (JSON response)",
649 op.description.as_deref().unwrap_or("")
650 );
651 results.push(context! {
652 kind => "standard",
653 method_name => method_name,
654 name => op.name.snake_case.clone(),
655 http_method => http_method,
656 path => path,
657 params_signature => params_signature,
658 return_type => json_return_type,
659 path_params => path_params.clone(),
660 query_params => query_params,
661 params => py_params,
662 header_params_obj => header_params_obj,
663 has_body => has_body,
664 body_type => body_type,
665 body_param_name => body_param_name,
666 body_content_type => body_content_type,
667 is_multipart => is_multipart_op(op),
668 has_path_params => has_path_params,
669 has_query_params => has_query_params,
670 has_header_params => has_header_params,
671 summary => op.summary.clone(),
672 description => json_desc,
673 deprecated => op.deprecated,
674 });
675 }
676 }
677 }
678
679 results
680}
681
682fn build_ts_params_signature(
684 op: &IrOperation,
685 tm: &TypeMapConfig,
686 field_casing: &str,
687 is_sse: bool,
688) -> String {
689 let param_name = |p: &crate::ir::IrParameter| -> String {
690 match field_casing {
691 "snake" => p.name.snake_case.clone(),
692 "pascal" => p.name.pascal_case.clone(),
693 _ => p.name.camel_case.clone(),
694 }
695 };
696
697 let mut required_parts = Vec::new();
698 let mut optional_parts = Vec::new();
699
700 for param in &op.parameters {
701 let type_str = map_type(¶m.param_type, tm);
702 let name = param_name(param);
703 match param.location {
704 IrParameterLocation::Path => {
705 required_parts.push(format!("{name}: {type_str}"));
706 }
707 IrParameterLocation::Query => {
708 if param.required {
709 required_parts.push(format!("{name}: {type_str}"));
710 } else {
711 optional_parts.push(format!("{name}?: {type_str}"));
712 }
713 }
714 IrParameterLocation::Header => {
715 if param.required {
716 required_parts.push(format!("{name}: {type_str}"));
717 } else {
718 optional_parts.push(format!("{name}?: {type_str}"));
719 }
720 }
721 _ => {}
722 }
723 }
724
725 if let Some(ref body) = op.request_body {
726 let type_str = map_type(&body.body_type, tm);
727 if body.required {
728 required_parts.push(format!("body: {type_str}"));
729 } else {
730 optional_parts.push(format!("body?: {type_str}"));
731 }
732 }
733
734 let options_type = if is_sse {
735 "options?: SSEOptions"
736 } else {
737 "options?: RequestOptions"
738 };
739 optional_parts.push(options_type.to_string());
740
741 let mut parts = required_parts;
742 parts.extend(optional_parts);
743 parts.join(", ")
744}
745
746fn build_hook_contexts(
749 ir: &IrSpec,
750 tm: &TypeMapConfig,
751 _operation_casing: &str,
752) -> (Vec<Value>, HashSet<usize>) {
753 let mut seen_hooks = HashSet::new();
754 let mut used_op_indices = HashSet::new();
755
756 let hooks: Vec<Value> = ir
757 .operations
758 .iter()
759 .enumerate()
760 .flat_map(|(idx, op)| {
761 build_single_hook_contexts(op, tm)
762 .into_iter()
763 .map(move |ctx| (idx, ctx))
764 })
765 .filter(|(idx, h)| {
766 let name = h
767 .get_attr("hook_name")
768 .ok()
769 .and_then(|v| v.as_str().map(String::from));
770 match name {
771 Some(n) => {
772 if seen_hooks.insert(n) {
773 used_op_indices.insert(*idx);
774 true
775 } else {
776 false
777 }
778 }
779 None => true,
780 }
781 })
782 .map(|(_, ctx)| ctx)
783 .collect();
784
785 (hooks, used_op_indices)
786}
787
788fn build_single_hook_contexts(op: &IrOperation, tm: &TypeMapConfig) -> Vec<Value> {
789 let mut results = Vec::new();
790
791 match (&op.method, &op.return_type) {
792 (HttpMethod::Get, IrReturnType::Standard(resp)) => {
794 let return_type = map_type(&resp.response_type, tm);
795 let (params_sig, swr_key, call_args) = build_hook_query_params(op, tm);
796 results.push(context! {
797 kind => "query",
798 hook_name => format!("use{}", op.name.pascal_case),
799 method_name => op.name.camel_case.clone(),
800 params_signature => params_sig,
801 return_type => return_type,
802 swr_key => swr_key,
803 call_args => call_args,
804 description => op.summary.clone().or(op.description.clone()),
805 });
806 }
807 (_, IrReturnType::Standard(_)) | (_, IrReturnType::Void) => {
809 let return_type = match &op.return_type {
810 IrReturnType::Standard(r) => map_type(&r.response_type, tm),
811 _ => map_type(&IrType::Void, tm),
812 };
813 let has_body = op.request_body.is_some();
814 let body_type = op
815 .request_body
816 .as_ref()
817 .map(|b| map_type(&b.body_type, tm))
818 .unwrap_or_else(|| map_type(&IrType::Void, tm));
819
820 let (path_params_sig, swr_key, call_args, swr_key_type) =
821 build_hook_mutation_params(op, tm);
822 results.push(context! {
823 kind => "mutation",
824 hook_name => format!("use{}", op.name.pascal_case),
825 method_name => op.name.camel_case.clone(),
826 path_params_signature => path_params_sig,
827 return_type => return_type,
828 has_body => has_body,
829 body_type => body_type,
830 swr_key => swr_key,
831 swr_key_type => swr_key_type,
832 call_args => call_args,
833 description => op.summary.clone().or(op.description.clone()),
834 });
835 }
836 (_, IrReturnType::Sse(sse)) => {
838 let event_type = if let Some(ref name) = sse.event_type_name {
839 name.clone()
840 } else {
841 map_type(&sse.event_type, tm)
842 };
843 let event_type_array = if event_type.contains('|') {
844 format!("({event_type})[]")
845 } else {
846 format!("{event_type}[]")
847 };
848 let method_name = if sse.also_has_json {
849 format!("{}Stream", op.name.camel_case)
850 } else {
851 op.name.camel_case.clone()
852 };
853 let hook_name = if sse.also_has_json {
854 format!("use{}Stream", op.name.pascal_case)
855 } else {
856 format!("use{}", op.name.pascal_case)
857 };
858 let (path_params_sig, trigger_params, stream_call_args, deps) =
859 build_hook_sse_params(op, tm);
860
861 results.push(context! {
862 kind => "sse",
863 hook_name => hook_name,
864 method_name => method_name,
865 path_params_signature => path_params_sig,
866 event_type => event_type,
867 event_type_array => event_type_array,
868 trigger_params => trigger_params,
869 stream_call_args => stream_call_args,
870 deps => deps,
871 description => op.summary.clone().or(op.description.clone()),
872 });
873
874 if let Some(ref json_resp) = sse.json_response {
876 let return_type = map_type(&json_resp.response_type, tm);
877 match op.method {
878 HttpMethod::Get => {
879 let (params_sig, swr_key, call_args) = build_hook_query_params(op, tm);
880 results.push(context! {
881 kind => "query",
882 hook_name => format!("use{}", op.name.pascal_case),
883 method_name => op.name.camel_case.clone(),
884 params_signature => params_sig,
885 return_type => return_type,
886 swr_key => swr_key,
887 call_args => call_args,
888 description => op.summary.clone().or(op.description.clone()),
889 });
890 }
891 _ => {
892 let has_body = op.request_body.is_some();
893 let body_type = op
894 .request_body
895 .as_ref()
896 .map(|b| map_type(&b.body_type, tm))
897 .unwrap_or_else(|| map_type(&IrType::Void, tm));
898 let (path_params_sig, swr_key, call_args, swr_key_type) =
899 build_hook_mutation_params(op, tm);
900 results.push(context! {
901 kind => "mutation",
902 hook_name => format!("use{}", op.name.pascal_case),
903 method_name => op.name.camel_case.clone(),
904 path_params_signature => path_params_sig,
905 return_type => return_type,
906 has_body => has_body,
907 body_type => body_type,
908 swr_key => swr_key,
909 swr_key_type => swr_key_type,
910 call_args => call_args,
911 description => op.summary.clone().or(op.description.clone()),
912 });
913 }
914 }
915 }
916 }
917 }
918
919 results
920}
921
922fn build_hook_query_params(op: &IrOperation, tm: &TypeMapConfig) -> (String, String, String) {
923 let mut required_sig = Vec::new();
924 let mut optional_sig = Vec::new();
925 let mut required_call = Vec::new();
926 let mut optional_call = Vec::new();
927 let mut key_parts = Vec::new();
928
929 for param in &op.parameters {
930 match param.location {
931 IrParameterLocation::Path
932 | IrParameterLocation::Query
933 | IrParameterLocation::Header => {
934 let ts = map_type(¶m.param_type, tm);
935 let is_required = param.required || param.location == IrParameterLocation::Path;
936 if is_required {
937 required_sig.push(format!("{}: {}", param.name.camel_case, ts));
938 required_call.push(param.name.camel_case.clone());
939 } else {
940 optional_sig.push(format!("{}?: {}", param.name.camel_case, ts));
941 optional_call.push(param.name.camel_case.clone());
942 }
943 key_parts.push(param.name.camel_case.clone());
944 }
945 _ => {}
946 }
947 }
948
949 let mut sig_parts = required_sig;
950 sig_parts.extend(optional_sig);
951 let mut call_parts = required_call;
952 call_parts.extend(optional_call);
953
954 let swr_key = if key_parts.is_empty() {
955 format!("\"{}\"", op.path)
956 } else {
957 format!("[\"{}\", {}] as const", op.path, key_parts.join(", "))
958 };
959
960 (sig_parts.join(", "), swr_key, call_parts.join(", "))
961}
962
963fn build_hook_mutation_params(
964 op: &IrOperation,
965 tm: &TypeMapConfig,
966) -> (String, String, String, String) {
967 let mut required_sig = Vec::new();
968 let mut optional_sig = Vec::new();
969 let mut required_call = Vec::new();
970 let mut optional_call = Vec::new();
971 let mut key_parts = Vec::new();
972 let mut key_type_parts = Vec::new();
973
974 for param in &op.parameters {
975 match param.location {
976 IrParameterLocation::Path
977 | IrParameterLocation::Query
978 | IrParameterLocation::Header => {
979 let ts = map_type(¶m.param_type, tm);
980 let is_required = param.required || param.location == IrParameterLocation::Path;
981 if is_required {
982 required_sig.push(format!("{}: {}", param.name.camel_case, ts));
983 required_call.push(param.name.camel_case.clone());
984 } else {
985 optional_sig.push(format!("{}?: {}", param.name.camel_case, ts));
986 optional_call.push(param.name.camel_case.clone());
987 }
988 key_parts.push(param.name.camel_case.clone());
989 key_type_parts.push(ts);
990 }
991 _ => {}
992 }
993 }
994
995 let mut sig_parts = required_sig;
996 sig_parts.extend(optional_sig);
997 let mut call_parts = required_call;
998 call_parts.extend(optional_call);
999
1000 if op.request_body.is_some() {
1001 call_parts.push("arg".to_string());
1002 }
1003
1004 let swr_key = if key_parts.is_empty() {
1005 format!("\"{}\"", op.path)
1006 } else {
1007 format!("[\"{}\", {}] as const", op.path, key_parts.join(", "))
1008 };
1009 let swr_key_type = if key_type_parts.is_empty() {
1010 "string".to_string()
1011 } else {
1012 format!("readonly [string, {}]", key_type_parts.join(", "))
1013 };
1014
1015 (
1016 sig_parts.join(", "),
1017 swr_key,
1018 call_parts.join(", "),
1019 swr_key_type,
1020 )
1021}
1022
1023fn build_hook_sse_params(op: &IrOperation, tm: &TypeMapConfig) -> (String, String, String, String) {
1024 let mut required_sig = Vec::new();
1025 let mut optional_sig = Vec::new();
1026 let mut required_call = Vec::new();
1027 let mut optional_call = Vec::new();
1028 let mut deps_parts = Vec::new();
1029
1030 for param in &op.parameters {
1031 match param.location {
1032 IrParameterLocation::Path
1033 | IrParameterLocation::Query
1034 | IrParameterLocation::Header => {
1035 let ts = map_type(¶m.param_type, tm);
1036 let is_required = param.required || param.location == IrParameterLocation::Path;
1037 if is_required {
1038 required_sig.push(format!("{}: {}", param.name.camel_case, ts));
1039 required_call.push(param.name.camel_case.clone());
1040 } else {
1041 optional_sig.push(format!("{}?: {}", param.name.camel_case, ts));
1042 optional_call.push(param.name.camel_case.clone());
1043 }
1044 deps_parts.push(format!(", {}", param.name.camel_case));
1045 }
1046 _ => {}
1047 }
1048 }
1049
1050 let mut sig_parts = required_sig;
1051 sig_parts.extend(optional_sig);
1052 let mut stream_call_parts = required_call;
1053 stream_call_parts.extend(optional_call);
1054
1055 let trigger_params = if let Some(ref body) = op.request_body {
1056 let ts = map_type(&body.body_type, tm);
1057 stream_call_parts.push("body".to_string());
1058 if body.required {
1059 format!("body: {}", ts)
1060 } else {
1061 format!("body?: {}", ts)
1062 }
1063 } else {
1064 String::new()
1065 };
1066
1067 (
1068 sig_parts.join(", "),
1069 trigger_params,
1070 stream_call_parts.join(", "),
1071 deps_parts.join(""),
1072 )
1073}
1074
1075fn build_hook_names(ir: &IrSpec) -> Vec<String> {
1076 let mut seen = HashSet::new();
1077 ir.operations
1078 .iter()
1079 .flat_map(|op| {
1080 let mut names = Vec::new();
1081 match &op.return_type {
1082 IrReturnType::Sse(sse) => {
1083 if sse.also_has_json {
1084 names.push(format!("use{}Stream", op.name.pascal_case));
1085 names.push(format!("use{}", op.name.pascal_case));
1086 } else {
1087 names.push(format!("use{}", op.name.pascal_case));
1088 }
1089 }
1090 _ => {
1091 names.push(format!("use{}", op.name.pascal_case));
1092 }
1093 }
1094 names
1095 })
1096 .filter(|n| seen.insert(n.clone()))
1097 .collect()
1098}
1099
1100fn collect_imported_types<'a>(
1103 ops: impl Iterator<Item = &'a IrOperation>,
1104 _tm: &TypeMapConfig,
1105) -> Vec<String> {
1106 let mut types = HashSet::new();
1107
1108 for op in ops {
1109 collect_types_from_return(&op.return_type, &mut types);
1110 if let Some(ref body) = op.request_body {
1111 collect_refs_from_ir_type(&body.body_type, &mut types);
1112 }
1113 for param in &op.parameters {
1114 collect_refs_from_ir_type(¶m.param_type, &mut types);
1115 }
1116 }
1117
1118 let mut sorted: Vec<String> = types.into_iter().collect();
1119 sorted.sort();
1120 sorted
1121}
1122
1123fn collect_types_from_return(ret: &IrReturnType, types: &mut HashSet<String>) {
1124 match ret {
1125 IrReturnType::Standard(resp) => {
1126 collect_refs_from_ir_type(&resp.response_type, types);
1127 }
1128 IrReturnType::Sse(sse) => {
1129 if let Some(ref name) = sse.event_type_name {
1130 types.insert(name.clone());
1131 } else {
1132 collect_refs_from_ir_type(&sse.event_type, types);
1133 }
1134 if let Some(ref json) = sse.json_response {
1135 collect_refs_from_ir_type(&json.response_type, types);
1136 }
1137 }
1138 IrReturnType::Void => {}
1139 }
1140}
1141
1142fn collect_refs_from_ir_type(ir_type: &IrType, types: &mut HashSet<String>) {
1143 match ir_type {
1144 IrType::Ref(name) => {
1145 types.insert(name.clone());
1146 }
1147 IrType::Array(inner) | IrType::Map(inner) => collect_refs_from_ir_type(inner, types),
1148 IrType::Union(variants) | IrType::Intersection(variants) => {
1149 for v in variants {
1150 collect_refs_from_ir_type(v, types);
1151 }
1152 }
1153 IrType::Object(fields) => {
1154 for (_, ty, _) in fields {
1155 collect_refs_from_ir_type(ty, types);
1156 }
1157 }
1158 _ => {}
1159 }
1160}
1161
1162fn collect_model_imports(ir: &IrSpec) -> Vec<String> {
1163 let mut imports = HashSet::new();
1164
1165 for op in &ir.operations {
1166 match &op.return_type {
1167 IrReturnType::Standard(resp) => {
1168 collect_refs_from_ir_type(&resp.response_type, &mut imports);
1169 }
1170 IrReturnType::Sse(sse) => {
1171 if let Some(ref name) = sse.event_type_name {
1172 imports.insert(name.clone());
1173 } else {
1174 collect_refs_from_ir_type(&sse.event_type, &mut imports);
1175 }
1176 if let Some(ref json) = sse.json_response {
1177 collect_refs_from_ir_type(&json.response_type, &mut imports);
1178 }
1179 }
1180 IrReturnType::Void => {}
1181 }
1182 if let Some(ref body) = op.request_body {
1183 collect_refs_from_ir_type(&body.body_type, &mut imports);
1184 }
1185 for param in &op.parameters {
1186 collect_refs_from_ir_type(¶m.param_type, &mut imports);
1187 }
1188 }
1189
1190 let mut sorted: Vec<String> = imports.into_iter().collect();
1191 sorted.sort();
1192 sorted
1193}
1194
1195fn build_test_operation_contexts(
1198 ir: &IrSpec,
1199 tm: &TypeMapConfig,
1200 _operation_casing: &str,
1201) -> (Vec<Value>, HashSet<usize>) {
1202 let mut seen_methods = HashSet::new();
1203 let mut used_op_indices = HashSet::new();
1204
1205 let operations: Vec<Value> = ir
1206 .operations
1207 .iter()
1208 .enumerate()
1209 .flat_map(|(idx, op)| {
1210 build_single_test_contexts(op, tm)
1211 .into_iter()
1212 .map(move |ctx| (idx, ctx))
1213 })
1214 .filter(|(idx, op)| {
1215 let name = op
1216 .get_attr("method_name")
1217 .ok()
1218 .and_then(|v| v.as_str().map(String::from));
1219 match name {
1220 Some(n) => {
1221 if seen_methods.insert(n) {
1222 used_op_indices.insert(*idx);
1223 true
1224 } else {
1225 false
1226 }
1227 }
1228 None => true,
1229 }
1230 })
1231 .map(|(_, ctx)| ctx)
1232 .collect();
1233
1234 (operations, used_op_indices)
1235}
1236
1237fn build_single_test_contexts(op: &IrOperation, tm: &TypeMapConfig) -> Vec<Value> {
1238 let mut results = Vec::new();
1239
1240 match &op.return_type {
1241 IrReturnType::Standard(resp) => {
1242 let return_type = map_type(&resp.response_type, tm);
1243 results.push(build_ts_test_context(
1244 op,
1245 "standard",
1246 &op.name.camel_case,
1247 &return_type,
1248 ));
1249 }
1250 IrReturnType::Void => {
1251 results.push(build_ts_test_context(
1252 op,
1253 "void",
1254 &op.name.camel_case,
1255 "void",
1256 ));
1257 }
1258 IrReturnType::Sse(sse) => {
1259 let sse_name = if sse.also_has_json {
1260 format!("{}Stream", op.name.camel_case)
1261 } else {
1262 op.name.camel_case.clone()
1263 };
1264 let return_type = if let Some(ref name) = sse.event_type_name {
1265 name.clone()
1266 } else {
1267 map_type(&sse.event_type, tm)
1268 };
1269 results.push(build_ts_test_context(op, "sse", &sse_name, &return_type));
1270
1271 if let Some(ref json_resp) = sse.json_response {
1272 let rt = map_type(&json_resp.response_type, tm);
1273 results.push(build_ts_test_context(
1274 op,
1275 "standard",
1276 &op.name.camel_case,
1277 &rt,
1278 ));
1279 }
1280 }
1281 }
1282
1283 results
1284}
1285
1286fn build_ts_test_context(
1287 op: &IrOperation,
1288 kind: &str,
1289 method_name: &str,
1290 return_type: &str,
1291) -> Value {
1292 let has_body = op.request_body.is_some();
1293 let test_call_args = build_ts_test_call_args(op);
1294 let expected_url_pattern = build_ts_expected_url_pattern(op);
1295 let mock_response = mock_value_ts(&if return_type == "void" {
1296 IrType::Void
1297 } else {
1298 guess_mock_type(return_type)
1299 });
1300
1301 context! {
1302 kind => kind,
1303 method_name => method_name,
1304 http_method => op.method.as_str(),
1305 return_type => return_type,
1306 has_body => has_body,
1307 test_call_args => test_call_args,
1308 expected_url_pattern => expected_url_pattern,
1309 mock_response => mock_response,
1310 }
1311}
1312
1313fn build_ts_test_call_args(op: &IrOperation) -> String {
1314 let mut args = Vec::new();
1315 for param in &op.parameters {
1316 match param.location {
1317 IrParameterLocation::Path => args.push(mock_value_ts(¶m.param_type)),
1318 IrParameterLocation::Query | IrParameterLocation::Header => {
1319 if param.required {
1320 args.push(mock_value_ts(¶m.param_type));
1321 }
1322 }
1323 _ => {}
1324 }
1325 }
1326 if let Some(ref body) = op.request_body {
1327 args.push(mock_value_ts(&body.body_type));
1328 }
1329 args.join(", ")
1330}
1331
1332fn build_ts_expected_url_pattern(op: &IrOperation) -> String {
1333 let mut path = op.path.clone();
1334 for param in &op.parameters {
1335 if param.location == IrParameterLocation::Path {
1336 let placeholder = format!("{{{}}}", param.original_name);
1337 path = path.replace(&placeholder, &mock_path_value_ts(¶m.param_type));
1338 }
1339 }
1340 path
1341}
1342
1343fn mock_value_ts(ir_type: &IrType) -> String {
1344 match ir_type {
1345 IrType::String | IrType::DateTime => "\"test\"".to_string(),
1346 IrType::StringLiteral(s) => format!("\"{s}\""),
1347 IrType::Number | IrType::Integer => "1".to_string(),
1348 IrType::IntegerLiteral(i) => i.to_string(),
1349 IrType::Boolean => "true".to_string(),
1350 IrType::Null | IrType::Void => "undefined".to_string(),
1351 IrType::Array(_) => "[]".to_string(),
1352 IrType::Object(_) | IrType::Map(_) | IrType::Any => "{}".to_string(),
1353 IrType::Ref(name) => format!("{{}} as {}", name),
1354 IrType::Binary => "new Blob()".to_string(),
1355 IrType::Union(variants) | IrType::Intersection(variants) => {
1356 if let Some(first) = variants.first() {
1357 mock_value_ts(first)
1358 } else {
1359 "{}".to_string()
1360 }
1361 }
1362 }
1363}
1364
1365fn mock_path_value_ts(ir_type: &IrType) -> String {
1366 match ir_type {
1367 IrType::Integer | IrType::Number => "1".to_string(),
1368 _ => "test".to_string(),
1369 }
1370}
1371
1372fn guess_mock_type(return_type: &str) -> IrType {
1373 match return_type {
1374 "string" => IrType::String,
1375 "number" => IrType::Number,
1376 "boolean" => IrType::Boolean,
1377 "void" => IrType::Void,
1378 t if t.ends_with("[]") => IrType::Array(Box::new(IrType::Any)),
1379 _ => IrType::Ref(return_type.to_string()),
1380 }
1381}
1382
1383fn collect_test_type_imports<'a>(ops: impl Iterator<Item = &'a IrOperation>) -> Vec<String> {
1384 let mut names = std::collections::BTreeSet::new();
1385
1386 for op in ops {
1387 if let Some(ref body) = op.request_body {
1388 collect_test_ref_names(&body.body_type, &mut names);
1389 }
1390 match &op.return_type {
1391 IrReturnType::Standard(resp) => {
1392 collect_test_ref_names(&resp.response_type, &mut names);
1393 }
1394 IrReturnType::Sse(sse) => {
1395 if let Some(ref json_resp) = sse.json_response {
1398 collect_test_ref_names(&json_resp.response_type, &mut names);
1399 }
1400 }
1401 IrReturnType::Void => {}
1402 }
1403 }
1404
1405 names.into_iter().collect()
1406}
1407
1408fn collect_test_ref_names(ir_type: &IrType, names: &mut std::collections::BTreeSet<String>) {
1409 match ir_type {
1410 IrType::Ref(name) => {
1411 names.insert(name.clone());
1412 }
1413 IrType::Array(inner) => collect_test_ref_names(inner, names),
1414 IrType::Union(variants) | IrType::Intersection(variants) => {
1415 for v in variants {
1416 collect_test_ref_names(v, names);
1417 }
1418 }
1419 _ => {}
1420 }
1421}
1422
1423fn build_python_test_contexts(ir: &IrSpec) -> (Vec<Value>, Vec<String>) {
1426 let model_imports: Vec<String> = ir
1427 .operations
1428 .iter()
1429 .filter_map(|op| {
1430 op.request_body.as_ref().and_then(|b| match &b.body_type {
1431 IrType::Ref(name) => Some(name.clone()),
1432 _ => None,
1433 })
1434 })
1435 .collect::<std::collections::BTreeSet<_>>()
1436 .into_iter()
1437 .collect();
1438
1439 let operations: Vec<Value> = ir
1440 .operations
1441 .iter()
1442 .flat_map(build_single_python_test_context)
1443 .collect();
1444
1445 (operations, model_imports)
1446}
1447
1448fn build_single_python_test_context(op: &IrOperation) -> Vec<Value> {
1449 let mut results = Vec::new();
1450
1451 let http_method = match op.method {
1452 HttpMethod::Get => "get",
1453 HttpMethod::Post => "post",
1454 HttpMethod::Put => "put",
1455 HttpMethod::Delete => "delete",
1456 HttpMethod::Patch => "patch",
1457 _ => "get",
1458 };
1459
1460 let test_path = build_python_test_path(&op.path, op);
1461 let has_body = op.request_body.is_some();
1462 let mock_body = op
1463 .request_body
1464 .as_ref()
1465 .map(|b| mock_value_python(&b.body_type))
1466 .unwrap_or_else(|| "{}".to_string());
1467
1468 match &op.return_type {
1469 IrReturnType::Standard(_) => {
1470 results.push(context! {
1471 kind => "standard",
1472 name => op.name.snake_case.clone(),
1473 http_method => http_method,
1474 path => op.path.clone(),
1475 test_path => test_path,
1476 has_body => has_body,
1477 mock_body => mock_body,
1478 });
1479 }
1480 IrReturnType::Void => {
1481 results.push(context! {
1482 kind => "void",
1483 name => op.name.snake_case.clone(),
1484 http_method => http_method,
1485 path => op.path.clone(),
1486 test_path => test_path,
1487 has_body => has_body,
1488 mock_body => mock_body,
1489 });
1490 }
1491 IrReturnType::Sse(sse) => {
1492 results.push(context! {
1493 kind => "sse",
1494 name => op.name.snake_case.clone(),
1495 http_method => http_method,
1496 path => op.path.clone(),
1497 test_path => test_path,
1498 has_body => has_body,
1499 mock_body => mock_body,
1500 });
1501 if sse.json_response.is_some() {
1502 results.push(context! {
1503 kind => "standard",
1504 name => op.name.snake_case.clone(),
1505 http_method => http_method,
1506 path => op.path.clone(),
1507 test_path => test_path,
1508 has_body => has_body,
1509 mock_body => mock_body,
1510 });
1511 }
1512 }
1513 }
1514
1515 results
1516}
1517
1518fn build_python_test_path(path: &str, op: &IrOperation) -> String {
1519 let mut result = path.to_string();
1520 for param in &op.parameters {
1521 if param.location == IrParameterLocation::Path {
1522 let placeholder = format!("{{{}}}", param.original_name);
1523 let test_value = match ¶m.param_type {
1524 IrType::Integer | IrType::Number => "1".to_string(),
1525 _ => "test".to_string(),
1526 };
1527 result = result.replace(&placeholder, &test_value);
1528 }
1529 }
1530 result
1531}
1532
1533fn mock_value_python(ir_type: &IrType) -> String {
1534 match ir_type {
1535 IrType::String | IrType::DateTime => "\"test\"".to_string(),
1536 IrType::StringLiteral(s) => format!("\"{s}\""),
1537 IrType::Number | IrType::Integer => "1".to_string(),
1538 IrType::IntegerLiteral(i) => i.to_string(),
1539 IrType::Boolean => "True".to_string(),
1540 IrType::Null | IrType::Void => "None".to_string(),
1541 IrType::Array(_) => "[]".to_string(),
1542 IrType::Ref(name) => format!("{}.model_construct()", name),
1543 IrType::Object(_) | IrType::Map(_) | IrType::Any => "{}".to_string(),
1544 IrType::Binary => "b\"test\"".to_string(),
1545 IrType::Union(variants) | IrType::Intersection(variants) => {
1546 if let Some(first) = variants.first() {
1547 mock_value_python(first)
1548 } else {
1549 "{}".to_string()
1550 }
1551 }
1552 }
1553}