Skip to main content

oag_core/engine/
context.rs

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
25/// Build the universal template context for an IrSpec.
26///
27/// Returns a minijinja Value containing all data needed by any template pack.
28pub 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    // Build operations with deduplication
49    let (operations, used_op_indices) =
50        build_operation_contexts(ir, tm, field_casing, operation_casing);
51
52    // Build hooks (React-specific, but included universally — templates ignore if unused)
53    let (hooks, hook_op_indices) = build_hook_contexts(ir, tm, operation_casing);
54
55    // Collect imported types from surviving operations
56    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    // Model imports for Python-style generators (Ref names from operations)
75    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    // Test contexts
99    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    // Extract guard sub-fields for flat template access
112    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        // guards.ts.j2 expects top-level `imports` and `guards`
124        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 contexts
136        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
144// ─── Schema contexts ─────────────────────────────────────────────
145
146fn 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            // TS-style: literal string values
163            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            // Python-style: name/value pairs
173            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
288// ─── Guard contexts ──────────────────────────────────────────────
289
290fn 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
330// ─── Operation contexts ──────────────────────────────────────────
331
332fn 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
390/// Build structured parameter data for an operation.
391fn 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(&param.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(&param.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    // Build Python-style params (structured, not pre-formatted)
474    let py_params: Vec<Value> = op
475        .parameters
476        .iter()
477        .map(|param| {
478            let type_str = map_type(&param.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    // Build TS-style params
528    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    // Build params_signature (TS-style pre-formatted)
541    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            // SSE params use SSEOptions instead of RequestOptions
617            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
682/// Build a TS-style params signature string.
683fn 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(&param.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
746// ─── Hook contexts (React) ───────────────────────────────────────
747
748fn 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        // GET → useSWR query hook
793        (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        // POST/PUT/DELETE non-streaming → useSWRMutation
808        (_, 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        // SSE → custom streaming hook
837        (_, 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            // Dual endpoint: also generate JSON hook
875            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(&param.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(&param.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(&param.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
1100// ─── Import collection ───────────────────────────────────────────
1101
1102fn 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(&param.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(&param.param_type, &mut imports);
1187        }
1188    }
1189
1190    let mut sorted: Vec<String> = imports.into_iter().collect();
1191    sorted.sort();
1192    sorted
1193}
1194
1195// ─── Test contexts ───────────────────────────────────────────────
1196
1197fn 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(&param.param_type)),
1318            IrParameterLocation::Query | IrParameterLocation::Header => {
1319                if param.required {
1320                    args.push(mock_value_ts(&param.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(&param.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                // SSE tests only check for async iterable — event types are never used as
1396                // type annotations, so skip them to avoid unused-import lint errors.
1397                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
1423// ─── Python test contexts ────────────────────────────────────────
1424
1425fn 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 &param.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}