Skip to main content

openapi_nexus/generators/rust/common/
emit_api.rs

1//! API emission for IR operations (Rust APIs).
2//!
3//! Groups operations by tag, emits one `apis/<tag>.rs` per tag group. Each file
4//! declares a `{Tag}Api` struct holding a `&runtime::Client` and exposes one
5//! method per operation.
6//!
7//! Backend-specific method bodies are injected via a closure, keeping this module
8//! agnostic to the HTTP library (reqwest, ureq, aioduct, etc.).
9
10use std::collections::{BTreeMap, HashSet};
11
12use crate::codegen::traits::file_writer::FileInfo;
13use crate::generators::multipart::multipart_parts_for_request_body;
14pub use crate::generators::multipart::{MultipartPart, MultipartValueEncoding};
15use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation};
16use crate::generators::response_headers::{
17    ResponseHeaderPlan, ResponseHeaderValueKind, collect_response_headers,
18    unique_response_header_accessor_names,
19};
20use crate::generators::response_names::{
21    response_entry_name as response_variant_name, response_match_rank,
22};
23use crate::ir::types::{
24    IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation,
25};
26use heck::{ToPascalCase, ToSnakeCase};
27use sigil_stitch::code_block::{CodeBlock, CodeBlockBuilder};
28use sigil_stitch::lang::rust::Rust;
29use sigil_stitch::prelude::sigil_quote;
30use sigil_stitch::spec::annotation_spec::AnnotationSpec;
31use sigil_stitch::spec::field_spec::FieldSpec;
32use sigil_stitch::spec::file_spec::FileSpec;
33use sigil_stitch::spec::fun_spec::FunSpec;
34use sigil_stitch::spec::import_spec::ImportSpec;
35use sigil_stitch::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
36use sigil_stitch::spec::parameter_spec::ParameterSpec;
37use sigil_stitch::spec::type_spec::TypeSpec;
38use sigil_stitch::type_name::TypeName;
39
40use super::config::ExtraDeriveConfig;
41use super::emit_models::rust_type_str_qualified;
42
43// ---------------------------------------------------------------------------
44// Backend configuration
45// ---------------------------------------------------------------------------
46
47/// Captures the differences between Rust HTTP backends.
48pub struct RustBackendConfig {
49    /// Whether methods are async (reqwest, aioduct) or sync (ureq).
50    pub is_async: bool,
51    /// Module containing the native header-map type retained on generated responses.
52    pub response_headers_module: &'static str,
53    /// Native header-map type retained on generated responses.
54    pub response_headers_name: &'static str,
55    /// Extra generic parameters on the Api struct, e.g., `"R: aioduct::RuntimePoll"`.
56    /// `None` for reqwest and ureq.
57    pub struct_generics: Option<String>,
58    /// Extra generic args for the client field type, e.g., `"<R>"`.
59    /// `None` for reqwest and ureq.
60    pub client_type_args: Option<String>,
61}
62
63// ---------------------------------------------------------------------------
64// Public API
65// ---------------------------------------------------------------------------
66
67/// Generate every API file from the IR.
68pub fn generate_api_files(
69    ir: &IrSpec,
70    header: &str,
71    config: &RustBackendConfig,
72    response_extra_derives: Option<&ExtraDeriveConfig>,
73    request_inputs: &RequestInputPlan,
74    body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
75) -> Result<Vec<FileInfo>, String> {
76    let by_tag = group_by_tag(&ir.operations);
77    let mut files = Vec::with_capacity(by_tag.len());
78    let mut mod_entries = Vec::new();
79
80    for (tag, ops) in &by_tag {
81        let stem = tag.to_snake_case();
82        let filename = format!("{stem}.rs");
83        mod_entries.push(stem);
84        let body = emit_api_file(
85            tag,
86            ops,
87            ir,
88            config,
89            response_extra_derives,
90            request_inputs,
91            body_emitter,
92        );
93        let content = format!("{header}{body}");
94        files.push(FileInfo::api(filename, content));
95    }
96
97    // mod.rs
98    let mut mod_content = String::from(header);
99    for entry in &mod_entries {
100        mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
101    }
102    files.push(FileInfo::api("mod.rs".to_string(), mod_content));
103
104    Ok(files)
105}
106
107// ---------------------------------------------------------------------------
108// Grouping
109// ---------------------------------------------------------------------------
110
111fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation>> {
112    let mut out: BTreeMap<String, Vec<&IrOperation>> = BTreeMap::new();
113    for op in operations {
114        let tags: Vec<String> = if op.tags.is_empty() {
115            vec!["default".to_string()]
116        } else {
117            op.tags.clone()
118        };
119        for tag in tags {
120            out.entry(tag).or_default().push(op);
121        }
122    }
123    out
124}
125
126// ---------------------------------------------------------------------------
127// File assembly
128// ---------------------------------------------------------------------------
129
130fn emit_api_file(
131    tag: &str,
132    ops: &[&IrOperation],
133    ir: &IrSpec,
134    config: &RustBackendConfig,
135    response_extra_derives: Option<&ExtraDeriveConfig>,
136    request_inputs: &RequestInputPlan,
137    body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
138) -> String {
139    let struct_name = format!("{}Api", tag.to_pascal_case());
140    let plans: Vec<OpPlan> = ops
141        .iter()
142        .map(|op| plan_operation(op, ir, request_inputs))
143        .collect();
144
145    let stem = tag.to_snake_case();
146    let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
147
148    // Use imports
149    fsb = fsb.add_import(ImportSpec::named("crate::runtime::client", "Client"));
150    fsb = fsb.add_import(ImportSpec::named("crate::runtime::error", "ApiError"));
151    fsb = fsb.add_import(ImportSpec::named("crate::runtime::error", "Error"));
152
153    // Struct generics (e.g., `<'a, R: aioduct::RuntimePoll>`)
154    let (struct_gen, impl_gen, type_args, client_field_args) = match &config.struct_generics {
155        Some(g) => {
156            let client_args = config.client_type_args.as_deref().unwrap_or("");
157            let param_names = g
158                .split(',')
159                .map(|param| param.split(':').next().unwrap_or(param).trim())
160                .collect::<Vec<_>>()
161                .join(", ");
162            (
163                format!("<'a, {g}>"),
164                format!("<'a, {g}>"),
165                format!("<'a, {param_names}>"),
166                client_args.to_string(),
167            )
168        }
169        None => (
170            "<'a>".to_string(),
171            "<'a>".to_string(),
172            "<'a>".to_string(),
173            String::new(),
174        ),
175    };
176
177    // Build struct + impl as a CodeBlock (lifetimes/generics don't fit TypeSpec)
178    let mut body = CodeBlock::builder();
179
180    // Struct declaration via sigil_quote
181    let doc_struct = format!("/// API operations under the \"{tag}\" tag.");
182    let generics = struct_gen.as_str();
183    let client_type_suffix = client_field_args.as_str();
184    let client_field = format!("client: &'a Client{client_type_suffix},");
185    body.add_code(
186        sigil_quote!(RustLang {
187            $L(doc_struct)
188            pub struct $N(struct_name.as_str())$L(generics) {
189                $L(client_field)
190            }
191        })
192        .expect("struct sigil_quote builds"),
193    );
194    body.add_line();
195
196    // Impl block (kept open for method injection)
197    let impl_header = format!("impl{impl_gen} {struct_name}{type_args}");
198    body.add(&impl_header, ());
199    body.begin_control_flow("", ());
200
201    // Constructor via sigil_quote
202    let doc_ctor = format!("/// Create a new `{struct_name}` bound to the given client.");
203    body.add_code(
204        sigil_quote!(RustLang {
205            $L(doc_ctor)
206            pub fn $L("new(client: &'a Client@{client_type_suffix}) -> Self") {
207                Self {
208                    client,
209                }
210            }
211        })
212        .expect("constructor sigil_quote builds"),
213    );
214
215    // Methods
216    for plan in &plans {
217        body.add_line();
218        body.add_code(emit_operation(plan, config, body_emitter));
219    }
220
221    body.end_control_flow(); // close impl
222
223    fsb = fsb.add_code(body.build().expect("body builds"));
224
225    // Response structs -- add as TypeSpec members
226    for plan in &plans {
227        let response_headers_type =
228            TypeName::qualified(config.response_headers_module, config.response_headers_name);
229        fsb = fsb.add_type(emit_response_struct(
230            plan,
231            &response_headers_type,
232            response_extra_derives,
233        ));
234        fsb = fsb.add_code(emit_error_enum(plan, &response_headers_type));
235    }
236
237    let file = fsb.build().expect("FileSpec builds");
238    file.render(100).expect("FileSpec renders")
239}
240
241// ---------------------------------------------------------------------------
242// Operation planning (public for backend use)
243// ---------------------------------------------------------------------------
244
245pub struct OpPlan<'a> {
246    pub op: &'a IrOperation,
247    pub method_name: String,
248    pub response_type: String,
249    pub error_type: String,
250    pub path_params: Vec<ParamBinding<'a>>,
251    pub query_params: Vec<ParamBinding<'a>>,
252    pub header_params: Vec<ParamBinding<'a>>,
253    pub body: Option<BodyBinding>,
254    pub typed_responses: Vec<TypedResponse>,
255    pub error_responses: Vec<ErrorResponse>,
256    pub success_headers: Vec<ResponseHeaderPlan>,
257    pub error_headers: Vec<ResponseHeaderPlan>,
258}
259
260pub struct ParamBinding<'a> {
261    pub param: &'a IrParameter,
262    pub var_name: String,
263    pub rust_type: String,
264    pub is_optional: bool,
265}
266
267pub struct BodyBinding {
268    pub var_name: String,
269    pub rust_type: String,
270    pub media_type: String,
271    pub required: bool,
272    pub encoding: BodyEncoding,
273    pub multipart_supported: bool,
274    pub multipart_parts: Vec<MultipartPart>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum BodyEncoding {
279    Json,
280    FormUrlEncoded,
281    Multipart,
282    Xml,
283    TextPlain,
284    OctetStream,
285    Other(String),
286}
287
288pub struct TypedResponse {
289    pub status: String,
290    pub field_name: String,
291    pub rust_type: String,
292    pub decoding: ResponseDecoding,
293}
294
295pub struct ErrorResponse {
296    pub status: String,
297    pub variant_name: String,
298    pub rust_type: String,
299    pub decoding: Option<ResponseDecoding>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub enum ResponseDecoding {
304    Json,
305    Xml,
306    TextPlain,
307    OctetStream,
308    Other(String),
309}
310
311pub fn plan_operation<'a>(
312    op: &'a IrOperation,
313    ir: &'a IrSpec,
314    request_inputs: &RequestInputPlan,
315) -> OpPlan<'a> {
316    let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
317    let method_name = op_id.to_snake_case();
318    let response_type = format!("{}Response", op_id.to_pascal_case());
319    let error_type = format!("{}Error", op_id.to_pascal_case());
320
321    let mut used_names: HashSet<String> = HashSet::new();
322    used_names.insert("self".to_string());
323
324    let mut path_params = Vec::new();
325    let mut query_params = Vec::new();
326    let mut header_params = Vec::new();
327    for p in &op.parameters {
328        let var_name = unique_name(&p.name.to_snake_case(), &mut used_names);
329        let (rust_type, is_optional) = param_rust_type(p, ir);
330        let binding = ParamBinding {
331            param: p,
332            var_name,
333            rust_type,
334            is_optional,
335        };
336        match p.location {
337            ParameterLocation::Path => path_params.push(binding),
338            ParameterLocation::Query => query_params.push(binding),
339            ParameterLocation::Header => header_params.push(binding),
340            ParameterLocation::Cookie => header_params.push(binding),
341        }
342    }
343
344    let body = op
345        .request_body
346        .as_ref()
347        .and_then(|b| plan_body(op, b, &mut used_names, ir, request_inputs));
348
349    let mut typed_responses: Vec<TypedResponse> = op
350        .responses
351        .iter()
352        .filter(|r| is_success_status(&r.status))
353        .filter_map(|r| plan_response(r, ir))
354        .collect();
355    typed_responses.sort_by_key(|r| response_match_rank(&r.status));
356    let error_responses = op
357        .responses
358        .iter()
359        .filter(|r| !is_success_status(&r.status))
360        .map(|r| plan_error_response(r, ir))
361        .collect();
362    let success_headers = collect_response_headers(
363        op.responses
364            .iter()
365            .filter(|response| is_success_status(&response.status)),
366        ir,
367    );
368    let error_headers = collect_response_headers(
369        op.responses
370            .iter()
371            .filter(|response| !is_success_status(&response.status)),
372        ir,
373    );
374
375    OpPlan {
376        op,
377        method_name,
378        response_type,
379        error_type,
380        path_params,
381        query_params,
382        header_params,
383        body,
384        typed_responses,
385        error_responses,
386        success_headers,
387        error_headers,
388    }
389}
390
391pub fn plan_body(
392    op: &IrOperation,
393    b: &IrRequestBody,
394    used_names: &mut HashSet<String>,
395    ir: &IrSpec,
396    request_inputs: &RequestInputPlan,
397) -> Option<BodyBinding> {
398    let (media_type, t) = pick_body_content(b)?;
399    let encoding = body_encoding(&media_type);
400    let rust_type = match encoding {
401        BodyEncoding::OctetStream => "Vec<u8>".to_string(),
402        BodyEncoding::TextPlain => "String".to_string(),
403        BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
404            .map(|input| format!("crate::models::{}", input.name.to_pascal_case()))
405            .unwrap_or_else(|| rust_type_str_qualified(&t, ir)),
406        _ => rust_type_str_qualified(&t, ir),
407    };
408    let multipart_parts = if encoding == BodyEncoding::Multipart {
409        multipart_parts_for_request_body(b, &media_type, ir).unwrap_or_default()
410    } else {
411        Vec::new()
412    };
413    let multipart_supported = encoding != BodyEncoding::Multipart
414        || multipart_parts_for_request_body(b, &media_type, ir).is_some();
415    let var_name = unique_name("body", used_names);
416    Some(BodyBinding {
417        var_name,
418        rust_type,
419        media_type,
420        required: b.required,
421        encoding,
422        multipart_supported,
423        multipart_parts,
424    })
425}
426
427pub fn plan_response(r: &IrResponse, ir: &IrSpec) -> Option<TypedResponse> {
428    let (media_type, t) = pick_response_content(r)?;
429    let decoding = response_decoding(&media_type);
430    let rust_type = match decoding {
431        ResponseDecoding::OctetStream => "Vec<u8>".to_string(),
432        ResponseDecoding::TextPlain => "String".to_string(),
433        _ => rust_type_str_qualified(&t, ir),
434    };
435    Some(TypedResponse {
436        status: r.status.clone(),
437        field_name: response_field_name(&r.status),
438        rust_type,
439        decoding,
440    })
441}
442
443pub fn plan_error_response(r: &IrResponse, ir: &IrSpec) -> ErrorResponse {
444    let (rust_type, decoding) = match pick_response_content(r) {
445        Some((media_type, t)) => {
446            let decoding = response_decoding(&media_type);
447            let rust_type = match decoding {
448                ResponseDecoding::OctetStream => "Vec<u8>".to_string(),
449                ResponseDecoding::TextPlain => "String".to_string(),
450                _ => rust_type_str_qualified(&t, ir),
451            };
452            (rust_type, Some(decoding))
453        }
454        None => ("()".to_string(), None),
455    };
456    ErrorResponse {
457        status: r.status.clone(),
458        variant_name: response_variant_name(&r.status),
459        rust_type,
460        decoding,
461    }
462}
463
464pub fn is_success_status(status: &str) -> bool {
465    status
466        .parse::<u16>()
467        .is_ok_and(|code| (200..300).contains(&code))
468        || status.eq_ignore_ascii_case("2XX")
469}
470
471pub fn param_rust_type(p: &IrParameter, ir: &IrSpec) -> (String, bool) {
472    let base = rust_type_str_qualified(&p.type_expr, ir);
473    if p.required {
474        (base, false)
475    } else if matches!(p.type_expr, IrTypeExpr::Nullable(_)) {
476        // Already wrapped in Option by rust_type_str_qualified → avoid double-wrapping
477        (base, true)
478    } else {
479        (format!("Option<{base}>"), true)
480    }
481}
482
483pub fn unique_name(desired: &str, used: &mut HashSet<String>) -> String {
484    if used.insert(desired.to_string()) {
485        return desired.to_string();
486    }
487    for i in 2..=u32::MAX {
488        let candidate = format!("{desired}_{i}");
489        if used.insert(candidate.clone()) {
490            return candidate;
491        }
492    }
493    unreachable!("name collision space exhausted")
494}
495
496// ---------------------------------------------------------------------------
497// Per-operation emission
498// ---------------------------------------------------------------------------
499
500fn emit_operation(
501    plan: &OpPlan<'_>,
502    config: &RustBackendConfig,
503    body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
504) -> CodeBlock {
505    let OpPlan {
506        op,
507        method_name,
508        response_type,
509        error_type,
510        ..
511    } = plan;
512
513    let mut b = CodeBlock::builder();
514
515    // Doc comment
516    if let Some(summary) = &op.summary {
517        for line in summary.lines() {
518            if line.is_empty() {
519                b.add("///\n", ());
520            } else {
521                b.add(&format!("/// {line}\n"), ());
522            }
523        }
524    } else {
525        b.add(
526            &format!("/// {} {}\n", op.method.to_uppercase(), op.path),
527            (),
528        );
529    }
530    if let Some(desc) = &op.description {
531        b.add("///\n", ());
532        for line in desc.lines() {
533            if line.is_empty() {
534                b.add("///\n", ());
535            } else {
536                b.add(&format!("/// {line}\n"), ());
537            }
538        }
539    }
540
541    // Method signature
542    let mut params = Vec::new();
543    params.push("&self".to_string());
544    for p in plan
545        .path_params
546        .iter()
547        .chain(&plan.query_params)
548        .chain(&plan.header_params)
549    {
550        let ty = if is_copy_type(&p.rust_type) {
551            p.rust_type.clone()
552        } else if p.rust_type == "String" {
553            "&str".to_string()
554        } else if let Some(inner) = p
555            .rust_type
556            .strip_prefix("Vec<")
557            .and_then(|s| s.strip_suffix('>'))
558        {
559            format!("&[{inner}]")
560        } else {
561            format!("&{}", p.rust_type)
562        };
563        params.push(format!("{}: {ty}", p.var_name));
564    }
565    if let Some(body) = &plan.body {
566        let ty = if body.required {
567            format!("&{}", body.rust_type)
568        } else {
569            format!("Option<&{}>", body.rust_type)
570        };
571        params.push(format!("{}: {ty}", body.var_name));
572    }
573
574    let async_kw = if config.is_async { "async " } else { "" };
575    b.add(
576        &format!(
577            "pub {async_kw}fn {method_name}(\n    {},\n) -> Result<{response_type}, {error_type}>",
578            params.join(",\n    "),
579        ),
580        (),
581    );
582    b.begin_control_flow("", ());
583
584    // Method body from backend
585    b.add_code(body_emitter(plan));
586
587    b.end_control_flow();
588    b.build().unwrap()
589}
590
591pub fn emit_response_struct(
592    plan: &OpPlan<'_>,
593    response_headers_type: &TypeName,
594    extra: Option<&ExtraDeriveConfig>,
595) -> TypeSpec {
596    let mut tb = TypeSpec::builder(&plan.response_type, TypeKind::Struct);
597    tb = tb.visibility(Visibility::Public);
598    tb = tb.doc(&format!("Response from `{}`.", plan.method_name));
599
600    let mut ann = AnnotationSpec::new("derive").args(["Debug"]);
601    if let Some(cfg) = extra {
602        ann = ann.args(cfg.derives.iter().map(|s| s.as_str()));
603    }
604    tb = tb.annotate(ann);
605
606    // status_code field
607    {
608        let fb = FieldSpec::builder("status_code", TypeName::primitive("u16"));
609        let fb = fb.visibility(Visibility::Public);
610        tb = tb.add_field(fb.build().expect("FieldSpec builds"));
611    }
612
613    // native response headers
614    {
615        let fb = FieldSpec::builder("headers", response_headers_type.clone());
616        let fb = fb.visibility(Visibility::Public);
617        tb = tb.add_field(fb.build().expect("FieldSpec builds"));
618    }
619
620    // typed response fields
621    let mut seen: HashSet<String> = HashSet::new();
622    for tr in &plan.typed_responses {
623        if !seen.insert(tr.field_name.clone()) {
624            continue;
625        }
626        let fb = FieldSpec::builder(
627            &tr.field_name,
628            TypeName::raw(&format!("Option<{}>", tr.rust_type)),
629        );
630        let fb = fb.visibility(Visibility::Public);
631        tb = tb.add_field(fb.build().expect("FieldSpec builds"));
632    }
633
634    let method_names = rust_header_accessor_names(&plan.success_headers);
635    for (header, method_name) in plan.success_headers.iter().zip(method_names) {
636        tb = tb.add_method(build_rust_header_accessor(
637            header,
638            &method_name,
639            "self.headers",
640        ));
641    }
642
643    tb.build().expect("TypeSpec builds")
644}
645
646pub fn emit_error_enum(plan: &OpPlan<'_>, response_headers_type: &TypeName) -> CodeBlock {
647    let mut cb = CodeBlock::builder();
648    let mut variants = Vec::new();
649    let mut seen: HashSet<String> = HashSet::new();
650    for er in &plan.error_responses {
651        let variant = unique_variant_name(&er.variant_name, &mut seen);
652        variants.push((variant, er.rust_type.clone()));
653    }
654    let unexpected_variant = unique_variant_name("Unexpected", &mut seen);
655    let transport_variant = unique_variant_name("Transport", &mut seen);
656
657    cb.add(&format!("/// Error from `{}`.\n", plan.method_name), ());
658    cb.add("#[derive(Debug)]\n", ());
659    cb.add(&format!("pub enum {} {{\n", plan.error_type), ());
660    for (variant, rust_type) in &variants {
661        cb.add(&format!("    {variant}(ApiError<{rust_type}>),\n"), ());
662    }
663    cb.add(
664        &format!("    {unexpected_variant}(ApiError<Vec<u8>>),\n"),
665        (),
666    );
667    cb.add(&format!("    {transport_variant}(Error),\n"), ());
668    cb.add("}\n\n", ());
669
670    cb.add(
671        &format!("impl From<Error> for {} {{\n", plan.error_type),
672        (),
673    );
674    cb.add("    fn from(error: Error) -> Self {\n", ());
675    cb.add(&format!("        Self::{transport_variant}(error)\n"), ());
676    cb.add("    }\n", ());
677    cb.add("}\n\n", ());
678
679    let operation_id = if plan.op.operation_id.is_empty() {
680        plan.method_name.as_str()
681    } else {
682        plan.op.operation_id.as_str()
683    };
684    cb.add_code(emit_api_call_error_conversion(
685        &plan.error_type,
686        operation_id,
687        &variants,
688        &unexpected_variant,
689        &transport_variant,
690    ));
691    cb.add_line();
692
693    cb.add_code(emit_rust_error_header_impl(
694        plan,
695        response_headers_type,
696        &variants,
697        &unexpected_variant,
698        &transport_variant,
699    ));
700
701    cb.add(
702        &format!("impl std::fmt::Display for {} {{\n", plan.error_type),
703        (),
704    );
705    cb.add(
706        "    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n",
707        (),
708    );
709    cb.add("        match self {\n", ());
710    for (variant, _) in &variants {
711        cb.add(
712            &format!("            Self::{variant}(err) => write!(f, \"HTTP error {{}}\", err.status_code()),\n"),
713            (),
714        );
715    }
716    cb.add(&format!(
717        "            Self::{unexpected_variant}(err) => write!(f, \"unexpected HTTP error {{}}\", err.status_code()),\n"
718    ), ());
719    cb.add(
720        &format!("            Self::{transport_variant}(err) => std::fmt::Display::fmt(err, f),\n"),
721        (),
722    );
723    cb.add("        }\n", ());
724    cb.add("    }\n", ());
725    cb.add("}\n\n", ());
726
727    cb.add(
728        &format!("impl std::error::Error for {} {{\n", plan.error_type),
729        (),
730    );
731    cb.add(
732        "    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {\n",
733        (),
734    );
735    cb.add("        match self {\n", ());
736    cb.add(
737        &format!("            Self::{transport_variant}(err) => Some(err),\n"),
738        (),
739    );
740    cb.add("            _ => None,\n", ());
741    cb.add("        }\n", ());
742    cb.add("    }\n", ());
743    cb.add("}\n", ());
744
745    cb.build().expect("error enum builds")
746}
747
748fn emit_api_call_error_conversion(
749    operation_error_type: &str,
750    operation_id: &str,
751    variants: &[(String, String)],
752    unexpected_variant: &str,
753    transport_variant: &str,
754) -> CodeBlock {
755    let operation_error_type = TypeName::primitive(operation_error_type);
756    let api_call_error_type = TypeName::importable("crate::runtime::error", "ApiCallError");
757
758    sigil_quote!(RustLang {
759        impl From<$T(operation_error_type.clone())> for $T(api_call_error_type) {
760            fn from(error: $T(operation_error_type.clone())) -> Self {
761                match error {
762                    $for((variant, _) in variants) {
763                        $T(operation_error_type.clone())::$N(variant.as_str())(error) => Self::from_api_error($S(operation_id), error),
764                    }
765                    $T(operation_error_type.clone())::$N(unexpected_variant)(error) => Self::from_api_error($S(operation_id), error),
766                    $T(operation_error_type)::$N(transport_variant)(error) => Self::from_runtime_error($S(operation_id), error),
767                }
768            }
769        }
770    })
771    .expect("Rust API call error conversion builds")
772}
773
774fn emit_rust_error_header_impl(
775    plan: &OpPlan<'_>,
776    response_headers_type: &TypeName,
777    variants: &[(String, String)],
778    unexpected_variant: &str,
779    transport_variant: &str,
780) -> CodeBlock {
781    if plan.error_headers.is_empty() {
782        return sigil_quote!(RustLang {}).expect("empty Rust error header impl builds");
783    }
784
785    let response_headers_body = sigil_quote!(RustLang {
786        match self {
787            $for((variant, _) in variants) {
788                Self::$N(variant.as_str())(err) => Some(err.headers()),
789            }
790            Self::$N(unexpected_variant)(err) => Some(err.headers()),
791            Self::$N(transport_variant)(_) => None,
792        }
793    })
794    .expect("Rust response headers body builds");
795    let response_headers = FunSpec::builder("response_headers")
796        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
797        .returns(TypeName::optional(TypeName::reference(
798            response_headers_type.clone(),
799        )))
800        .body(response_headers_body)
801        .build()
802        .expect("Rust response headers method builds");
803
804    let lang = Rust::new();
805    let response_headers = response_headers
806        .emit(&lang, DeclarationContext::Member)
807        .expect("Rust response headers method emits");
808    let method_names = rust_header_accessor_names(&plan.error_headers);
809    let header_accessors = plan
810        .error_headers
811        .iter()
812        .zip(method_names)
813        .map(|(header, method_name)| {
814            build_rust_header_accessor(header, &method_name, "self.response_headers()?")
815                .emit(&lang, DeclarationContext::Member)
816                .expect("Rust error header accessor emits")
817        })
818        .collect::<Vec<_>>();
819    sigil_quote!(RustLang {
820        impl $N(plan.error_type.as_str()) {
821            $L(response_headers)
822            $for(accessor in &header_accessors) {
823                $L((*accessor).clone())
824            }
825        }
826    })
827    .expect("Rust error header impl builds")
828}
829
830fn build_rust_header_accessor(
831    header: &ResponseHeaderPlan,
832    method_name: &str,
833    headers_expr: &str,
834) -> FunSpec {
835    FunSpec::builder(method_name)
836        .visibility(Visibility::Public)
837        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
838        .returns(rust_header_return_type(header.value_kind))
839        .body(rust_header_accessor_body(header, headers_expr))
840        .build()
841        .expect("Rust header accessor builds")
842}
843
844fn rust_header_accessor_body(header: &ResponseHeaderPlan, headers_expr: &str) -> CodeBlock {
845    let wire_name = header.wire_name.as_str();
846    match header.value_kind {
847        ResponseHeaderValueKind::String => sigil_quote!(RustLang {
848            $L(headers_expr).get($S(wire_name))?.to_str().ok()
849        }),
850        ResponseHeaderValueKind::Integer | ResponseHeaderValueKind::Boolean => {
851            sigil_quote!(RustLang {
852                $L(headers_expr).get($S(wire_name))?.to_str().ok()?.parse().ok()
853            })
854        }
855        ResponseHeaderValueKind::Number => sigil_quote!(RustLang {
856            $L(headers_expr).get($S(wire_name))?.to_str().ok()?.parse().ok().filter(|value: &f64| value.is_finite())
857        }),
858    }
859    .expect("Rust header accessor body builds")
860}
861
862fn rust_header_return_type(kind: ResponseHeaderValueKind) -> TypeName {
863    let inner = match kind {
864        ResponseHeaderValueKind::String => TypeName::reference(TypeName::primitive("str")),
865        ResponseHeaderValueKind::Integer => TypeName::primitive("i64"),
866        ResponseHeaderValueKind::Number => TypeName::primitive("f64"),
867        ResponseHeaderValueKind::Boolean => TypeName::primitive("bool"),
868    };
869    TypeName::optional(inner)
870}
871
872fn rust_header_accessor_names(headers: &[ResponseHeaderPlan]) -> Vec<String> {
873    unique_response_header_accessor_names(headers, |wire_name| {
874        let mut base = wire_name.to_snake_case();
875        if base.is_empty() || base.starts_with(|character: char| character.is_ascii_digit()) {
876            base = format!("header_{base}");
877        }
878        format!("{base}_header")
879    })
880}
881
882fn unique_variant_name(desired: &str, used: &mut HashSet<String>) -> String {
883    if used.insert(desired.to_string()) {
884        return desired.to_string();
885    }
886    for i in 2..=u32::MAX {
887        let candidate = format!("{desired}{i}");
888        if used.insert(candidate.clone()) {
889            return candidate;
890        }
891    }
892    unreachable!("variant collision space exhausted")
893}
894
895// ---------------------------------------------------------------------------
896// Helpers (public for backend use)
897// ---------------------------------------------------------------------------
898
899pub fn sanitize_operation_id(id: &str, method: &str, path: &str) -> String {
900    if !id.is_empty() {
901        return id.to_string();
902    }
903    format!(
904        "{}_{}",
905        method,
906        path.replace('/', "_").replace(['{', '}'], "")
907    )
908}
909
910pub fn response_field_name(status: &str) -> String {
911    match status {
912        "200" => "data".to_string(),
913        "201" => "created".to_string(),
914        "204" => "no_content".to_string(),
915        "default" => "error_body".to_string(),
916        s if s.ends_with("XX") => {
917            let prefix = &s[..s.len() - 2];
918            format!("status_{prefix}xx")
919        }
920        s => format!("status_{s}"),
921    }
922}
923
924/// Convert an OpenAPI status code string to a Rust match pattern.
925pub fn status_match_pattern(status: &str) -> String {
926    match status {
927        "default" => "_".to_string(),
928        s if s.ends_with("XX") => {
929            let prefix: u16 = s[..s.len() - 2].parse().unwrap_or(0);
930            let lo = prefix * 100;
931            let hi = lo + 99;
932            format!("{lo}..={hi}")
933        }
934        s => s.to_string(),
935    }
936}
937
938pub fn pick_body_type(b: &IrRequestBody) -> Option<IrTypeExpr> {
939    pick_body_content(b).map(|(_, t)| t)
940}
941
942pub fn pick_response_type(r: &IrResponse) -> Option<IrTypeExpr> {
943    pick_response_content(r).map(|(_, t)| t)
944}
945
946fn pick_body_content(b: &IrRequestBody) -> Option<(String, IrTypeExpr)> {
947    pick_media_type(&b.content, |media_type| {
948        media_type_base(media_type) == "application/json"
949    })
950    .or_else(|| pick_media_type(&b.content, is_json_media_type))
951    .or_else(|| {
952        pick_media_type(&b.content, |media_type| {
953            media_type_base(media_type) == "multipart/form-data"
954        })
955    })
956    .or_else(|| {
957        pick_media_type(&b.content, |media_type| {
958            media_type_base(media_type) == "application/x-www-form-urlencoded"
959        })
960    })
961    .or_else(|| pick_media_type(&b.content, is_xml_media_type))
962    .or_else(|| {
963        pick_media_type(&b.content, |media_type| {
964            media_type_base(media_type) == "text/plain"
965        })
966    })
967    .or_else(|| {
968        pick_media_type(&b.content, |media_type| {
969            media_type_base(media_type) == "application/octet-stream"
970        })
971    })
972    .or_else(|| pick_first_content(&b.content))
973}
974
975fn pick_response_content(r: &IrResponse) -> Option<(String, IrTypeExpr)> {
976    pick_media_type(&r.content, |media_type| {
977        media_type_base(media_type) == "application/json"
978    })
979    .or_else(|| pick_media_type(&r.content, is_json_media_type))
980    .or_else(|| {
981        pick_media_type(&r.content, |media_type| {
982            media_type_base(media_type) == "application/octet-stream"
983        })
984    })
985    .or_else(|| {
986        pick_media_type(&r.content, |media_type| {
987            media_type_base(media_type) == "text/plain"
988        })
989    })
990    .or_else(|| pick_media_type(&r.content, is_xml_media_type))
991    .or_else(|| pick_first_content(&r.content))
992}
993
994fn pick_media_type(
995    content: &indexmap::IndexMap<String, IrTypeExpr>,
996    predicate: impl Fn(&str) -> bool,
997) -> Option<(String, IrTypeExpr)> {
998    content
999        .iter()
1000        .find(|(media_type, _)| predicate(media_type))
1001        .map(|(media_type, t)| (media_type.clone(), t.clone()))
1002}
1003
1004fn pick_first_content(
1005    content: &indexmap::IndexMap<String, IrTypeExpr>,
1006) -> Option<(String, IrTypeExpr)> {
1007    content
1008        .iter()
1009        .next()
1010        .map(|(media_type, t)| (media_type.clone(), t.clone()))
1011}
1012
1013fn body_encoding(media_type: &str) -> BodyEncoding {
1014    let base = media_type_base(media_type);
1015    match base.as_str() {
1016        "application/json" => BodyEncoding::Json,
1017        "application/x-www-form-urlencoded" => BodyEncoding::FormUrlEncoded,
1018        "multipart/form-data" => BodyEncoding::Multipart,
1019        "application/xml" | "text/xml" => BodyEncoding::Xml,
1020        "text/plain" => BodyEncoding::TextPlain,
1021        "application/octet-stream" => BodyEncoding::OctetStream,
1022        _ if is_json_media_type(media_type) => BodyEncoding::Json,
1023        _ if is_xml_media_type(media_type) => BodyEncoding::Xml,
1024        _ => BodyEncoding::Other(media_type.to_string()),
1025    }
1026}
1027
1028fn response_decoding(media_type: &str) -> ResponseDecoding {
1029    let base = media_type_base(media_type);
1030    match base.as_str() {
1031        "application/json" => ResponseDecoding::Json,
1032        "application/xml" | "text/xml" => ResponseDecoding::Xml,
1033        "text/plain" => ResponseDecoding::TextPlain,
1034        "application/octet-stream" => ResponseDecoding::OctetStream,
1035        _ if is_json_media_type(media_type) => ResponseDecoding::Json,
1036        _ if is_xml_media_type(media_type) => ResponseDecoding::Xml,
1037        _ => ResponseDecoding::Other(media_type.to_string()),
1038    }
1039}
1040
1041fn media_type_base(media_type: &str) -> String {
1042    media_type
1043        .split(';')
1044        .next()
1045        .unwrap_or(media_type)
1046        .trim()
1047        .to_ascii_lowercase()
1048}
1049
1050fn is_json_media_type(media_type: &str) -> bool {
1051    let base = media_type_base(media_type);
1052    base == "application/json" || base.ends_with("+json")
1053}
1054
1055fn is_xml_media_type(media_type: &str) -> bool {
1056    let base = media_type_base(media_type);
1057    base == "application/xml" || base == "text/xml" || base.ends_with("+xml")
1058}
1059
1060pub fn rust_field_name(wire_name: &str) -> String {
1061    escape_rust_keyword(&wire_name.to_snake_case())
1062}
1063
1064fn escape_rust_keyword(name: &str) -> String {
1065    const KEYWORDS: &[&str] = &[
1066        "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
1067        "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
1068        "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
1069        "true", "type", "union", "unsafe", "use", "where", "while", "yield",
1070    ];
1071    if KEYWORDS.contains(&name) {
1072        format!("r#{name}")
1073    } else {
1074        name.to_string()
1075    }
1076}
1077
1078pub fn rust_string_literal(value: &str) -> String {
1079    format!("{value:?}")
1080}
1081
1082pub fn text_field_expr(base: &str, part: &MultipartPart) -> String {
1083    let field_name = rust_field_name(&part.wire_name);
1084    match part.value_encoding {
1085        MultipartValueEncoding::Text => format!("{base}.{field_name}.to_string()"),
1086        MultipartValueEncoding::Json => {
1087            format!("serde_json::to_string(&{base}.{field_name}).map_err(Error::Deserialize)?")
1088        }
1089        MultipartValueEncoding::Unsupported => {
1090            unreachable!("unsupported multipart parts are emitted before value expressions")
1091        }
1092    }
1093}
1094
1095pub fn binary_field_expr(base: &str, part: &MultipartPart) -> String {
1096    format!("{base}.{}.data.clone()", rust_field_name(&part.wire_name))
1097}
1098
1099pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String {
1100    match part.value_encoding {
1101        MultipartValueEncoding::Text => format!("{value}.to_string()"),
1102        MultipartValueEncoding::Json => {
1103            format!("serde_json::to_string({value}).map_err(Error::Deserialize)?")
1104        }
1105        MultipartValueEncoding::Unsupported => {
1106            unreachable!("unsupported multipart parts are emitted before value expressions")
1107        }
1108    }
1109}
1110
1111pub fn optional_binary_field_expr(value: &str) -> String {
1112    format!("{value}.data.clone()")
1113}
1114
1115pub fn binary_filename_expr(base: &str, part: &MultipartPart) -> String {
1116    format!(
1117        "{base}.{}.filename_or_default({}).to_string()",
1118        rust_field_name(&part.wire_name),
1119        rust_string_literal(&part.default_filename)
1120    )
1121}
1122
1123pub fn optional_binary_filename_expr(value: &str, part: &MultipartPart) -> String {
1124    format!(
1125        "{value}.filename_or_default({}).to_string()",
1126        rust_string_literal(&part.default_filename)
1127    )
1128}
1129
1130pub fn response_value_expr(tr: &TypedResponse, bytes_var: &str) -> String {
1131    let owned_bytes_expr = bytes_var.strip_prefix('&').unwrap_or(bytes_var);
1132    match tr.decoding {
1133        ResponseDecoding::Json => {
1134            format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1135        }
1136        ResponseDecoding::Xml => {
1137            format!(
1138                "serde_xml_rs::from_reader(std::io::Cursor::new({bytes_var})).map_err(Error::Xml)"
1139            )
1140        }
1141        ResponseDecoding::TextPlain => {
1142            format!("Ok::<String, Error>(String::from_utf8_lossy({bytes_var}).into_owned())")
1143        }
1144        ResponseDecoding::OctetStream => {
1145            format!("Ok::<Vec<u8>, Error>({owned_bytes_expr}.to_vec())")
1146        }
1147        ResponseDecoding::Other(_) => {
1148            format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1149        }
1150    }
1151}
1152
1153pub fn response_value_expr_from_str(tr: &TypedResponse, body_var: &str) -> String {
1154    match tr.decoding {
1155        ResponseDecoding::Json => {
1156            format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
1157        }
1158        ResponseDecoding::Xml => {
1159            format!("serde_xml_rs::from_str({body_var}).map_err(Error::Xml)")
1160        }
1161        ResponseDecoding::TextPlain => format!("Ok::<String, Error>({body_var})"),
1162        ResponseDecoding::OctetStream => {
1163            format!("Ok::<Vec<u8>, Error>({body_var}.into_bytes())")
1164        }
1165        ResponseDecoding::Other(_) => {
1166            format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
1167        }
1168    }
1169}
1170
1171pub fn response_needs_bytes(typed_responses: &[TypedResponse]) -> bool {
1172    typed_responses
1173        .iter()
1174        .any(|tr| matches!(tr.decoding, ResponseDecoding::OctetStream))
1175}
1176
1177pub fn render_to_string(var: &str, type_expr: &IrTypeExpr, _is_optional: bool) -> String {
1178    match type_expr {
1179        IrTypeExpr::Array(_) => {
1180            format!("{var}.iter().map(ToString::to_string).collect::<Vec<_>>().join(\",\")")
1181        }
1182        _ => format!("{var}.to_string()"),
1183    }
1184}
1185
1186pub fn is_copy_type(ty: &str) -> bool {
1187    matches!(
1188        ty,
1189        "bool" | "i32" | "i64" | "f32" | "f64" | "u8" | "u16" | "u32" | "u64"
1190    ) || ty.starts_with("Option<")
1191        && is_copy_type(
1192            ty.strip_prefix("Option<")
1193                .unwrap()
1194                .strip_suffix('>')
1195                .unwrap_or(""),
1196        )
1197}
1198
1199// ---------------------------------------------------------------------------
1200// Shared body-emission helpers (used by all Rust backends)
1201// ---------------------------------------------------------------------------
1202
1203/// Emit `let mut result = FooResponse { status_code, field1: None, ... };`
1204pub fn emit_result_init(
1205    b: &mut CodeBlockBuilder,
1206    response_type: &str,
1207    typed_responses: &[TypedResponse],
1208) {
1209    let mut field_names = Vec::new();
1210    let mut seen: HashSet<String> = HashSet::new();
1211    for tr in typed_responses {
1212        if seen.insert(tr.field_name.clone()) {
1213            field_names.push(tr.field_name.as_str());
1214        }
1215    }
1216    b.add_code(
1217        sigil_quote!(RustLang {
1218            let mut result = $N(response_type)$L(" { status_code, headers: response_headers.clone()")$for(field_name in &field_names) { $L(", ")$N(*field_name)$L(": None") }$L(" }");
1219        })
1220        .expect("response result initializer builds"),
1221    );
1222}
1223
1224pub fn emit_empty_result_init(b: &mut CodeBlockBuilder, response_type: &str) {
1225    b.add_code(
1226        sigil_quote!(RustLang {
1227            let result = $N(response_type)$L(" { status_code, headers: response_headers.clone() }");
1228        })
1229        .expect("empty response result initializer builds"),
1230    );
1231}
1232
1233/// Clone the native response header map before the response body is consumed.
1234pub fn response_headers_init() -> CodeBlock {
1235    sigil_quote!(RustLang {
1236        let response_headers = resp.headers().clone();
1237    })
1238    .expect("response headers init builds")
1239}
1240
1241/// Emit `match status_code { ... }` dispatching deserialized bodies into result fields.
1242pub fn emit_response_match(
1243    b: &mut CodeBlockBuilder,
1244    typed_responses: &[TypedResponse],
1245    value_expr: &dyn Fn(&TypedResponse) -> String,
1246) {
1247    b.begin_control_flow("match status_code", ());
1248    let mut seen: HashSet<String> = HashSet::new();
1249    for tr in typed_responses {
1250        if !seen.insert(format!("{}-{}", tr.status, tr.field_name)) {
1251            continue;
1252        }
1253        let status_pattern = status_match_pattern(&tr.status);
1254        let value_expr = value_expr(tr);
1255        b.begin_control_flow(&format!("{status_pattern} =>"), ());
1256        b.add(
1257            &format!("result.{} = Some({value_expr}?);\n", tr.field_name),
1258            (),
1259        );
1260        b.end_control_flow();
1261    }
1262    if !typed_responses.iter().any(|tr| tr.status == "default") {
1263        b.add("_ => {}\n", ());
1264    }
1265    b.end_control_flow();
1266}
1267
1268pub fn emit_error_response_match(
1269    b: &mut CodeBlockBuilder,
1270    error_type: &str,
1271    error_responses: &[ErrorResponse],
1272    value_expr: &dyn Fn(&ErrorResponse) -> String,
1273) {
1274    b.begin_control_flow("if !(200..300).contains(&status_code)", ());
1275    b.begin_control_flow("match status_code", ());
1276
1277    let mut seen: HashSet<String> = HashSet::new();
1278    for er in error_responses
1279        .iter()
1280        .filter(|er| er.status.parse::<u16>().is_ok())
1281    {
1282        let key = format!("{}-{}", er.status, er.variant_name);
1283        if !seen.insert(key) {
1284            continue;
1285        }
1286        let pattern = status_match_pattern(&er.status);
1287        let body_expr = value_expr(er);
1288        b.begin_control_flow(&format!("{pattern} =>"), ());
1289        b.add(&format!("let body = {body_expr};\n"), ());
1290        b.add(&format!(
1291            "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1292            er.variant_name
1293        ), ());
1294        b.end_control_flow();
1295    }
1296
1297    for er in error_responses
1298        .iter()
1299        .filter(|er| er.status.ends_with("XX") && er.status.parse::<u16>().is_err())
1300    {
1301        let key = format!("{}-{}", er.status, er.variant_name);
1302        if !seen.insert(key) {
1303            continue;
1304        }
1305        let pattern = status_match_pattern(&er.status);
1306        let body_expr = value_expr(er);
1307        b.begin_control_flow(&format!("{pattern} =>"), ());
1308        b.add(&format!("let body = {body_expr};\n"), ());
1309        b.add(&format!(
1310            "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1311            er.variant_name
1312        ), ());
1313        b.end_control_flow();
1314    }
1315
1316    if let Some(er) = error_responses
1317        .iter()
1318        .find(|er| er.status.eq_ignore_ascii_case("default"))
1319    {
1320        let body_expr = value_expr(er);
1321        b.begin_control_flow("_ =>", ());
1322        b.add(&format!("let body = {body_expr};\n"), ());
1323        b.add(&format!(
1324            "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1325            er.variant_name
1326        ), ());
1327        b.end_control_flow();
1328    } else {
1329        b.begin_control_flow("_ =>", ());
1330        b.add(
1331            "let body = Ok::<Vec<u8>, Error>(body_bytes.to_vec());\n",
1332            (),
1333        );
1334        b.add(&format!(
1335            "return Err({error_type}::Unexpected(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n"
1336        ), ());
1337        b.end_control_flow();
1338    }
1339
1340    b.end_control_flow();
1341    b.end_control_flow();
1342}
1343
1344pub fn error_response_value_expr(er: &ErrorResponse, bytes_var: &str) -> String {
1345    let owned_bytes_expr = bytes_var.strip_prefix('&').unwrap_or(bytes_var);
1346    match er.decoding {
1347        Some(ResponseDecoding::Json) => {
1348            format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1349        }
1350        Some(ResponseDecoding::Xml) => {
1351            format!(
1352                "serde_xml_rs::from_reader(std::io::Cursor::new({bytes_var})).map_err(Error::Xml)"
1353            )
1354        }
1355        Some(ResponseDecoding::TextPlain) => {
1356            format!("Ok::<String, Error>(String::from_utf8_lossy({bytes_var}).into_owned())")
1357        }
1358        Some(ResponseDecoding::OctetStream) => {
1359            format!("Ok::<Vec<u8>, Error>({owned_bytes_expr}.to_vec())")
1360        }
1361        Some(ResponseDecoding::Other(_)) => {
1362            format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1363        }
1364        None => "Ok::<(), Error>(())".to_string(),
1365    }
1366}
1367
1368#[cfg(test)]
1369mod tests {
1370    use sigil_stitch::assert_rendered;
1371
1372    use super::*;
1373
1374    #[test]
1375    fn api_call_error_conversion_renders_structured_rust() {
1376        let variants = vec![
1377            ("BadRequest".to_string(), "BadRequestError".to_string()),
1378            ("Conflict".to_string(), "ConflictError".to_string()),
1379        ];
1380        let conversion = emit_api_call_error_conversion(
1381            "CreateResourceError",
1382            "createResource",
1383            &variants,
1384            "Unexpected",
1385            "Transport",
1386        );
1387
1388        assert_rendered!(
1389            Rust::new(),
1390            width = 100,
1391            conversion,
1392            r#"use crate::runtime::error::ApiCallError;
1393
1394impl From<CreateResourceError> for ApiCallError {
1395    fn from(error: CreateResourceError) -> Self {
1396        match error {
1397            CreateResourceError::BadRequest(error) => Self::from_api_error("createResource", error),
1398            CreateResourceError::Conflict(error) => Self::from_api_error("createResource", error),
1399            CreateResourceError::Unexpected(error) => Self::from_api_error("createResource", error),
1400            CreateResourceError::Transport(error) => Self::from_runtime_error("createResource", error),
1401        }
1402    }
1403}
1404"#,
1405        );
1406    }
1407}