1use 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::ir::types::{
17 IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation,
18};
19use heck::{ToPascalCase, ToSnakeCase};
20use sigil_stitch::code_block::{CodeBlock, CodeBlockBuilder};
21use sigil_stitch::prelude::sigil_quote;
22use sigil_stitch::spec::annotation_spec::AnnotationSpec;
23use sigil_stitch::spec::field_spec::FieldSpec;
24use sigil_stitch::spec::file_spec::FileSpec;
25use sigil_stitch::spec::import_spec::ImportSpec;
26use sigil_stitch::spec::modifiers::{TypeKind, Visibility};
27use sigil_stitch::spec::type_spec::TypeSpec;
28use sigil_stitch::type_name::TypeName;
29
30use super::config::ExtraDeriveConfig;
31use super::emit_models::rust_type_str_qualified;
32
33pub struct RustBackendConfig {
39 pub is_async: bool,
41 pub struct_generics: Option<String>,
44 pub client_type_args: Option<String>,
47}
48
49pub fn generate_api_files(
55 ir: &IrSpec,
56 header: &str,
57 config: &RustBackendConfig,
58 response_extra_derives: Option<&ExtraDeriveConfig>,
59 request_inputs: &RequestInputPlan,
60 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
61) -> Result<Vec<FileInfo>, String> {
62 let by_tag = group_by_tag(&ir.operations);
63 let mut files = Vec::with_capacity(by_tag.len());
64 let mut mod_entries = Vec::new();
65
66 for (tag, ops) in &by_tag {
67 let stem = tag.to_snake_case();
68 let filename = format!("{stem}.rs");
69 mod_entries.push(stem);
70 let body = emit_api_file(
71 tag,
72 ops,
73 ir,
74 config,
75 response_extra_derives,
76 request_inputs,
77 body_emitter,
78 );
79 let content = format!("{header}{body}");
80 files.push(FileInfo::api(filename, content));
81 }
82
83 let mut mod_content = String::from(header);
85 for entry in &mod_entries {
86 mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
87 }
88 files.push(FileInfo::api("mod.rs".to_string(), mod_content));
89
90 Ok(files)
91}
92
93fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation>> {
98 let mut out: BTreeMap<String, Vec<&IrOperation>> = BTreeMap::new();
99 for op in operations {
100 let tags: Vec<String> = if op.tags.is_empty() {
101 vec!["default".to_string()]
102 } else {
103 op.tags.clone()
104 };
105 for tag in tags {
106 out.entry(tag).or_default().push(op);
107 }
108 }
109 out
110}
111
112fn emit_api_file(
117 tag: &str,
118 ops: &[&IrOperation],
119 ir: &IrSpec,
120 config: &RustBackendConfig,
121 response_extra_derives: Option<&ExtraDeriveConfig>,
122 request_inputs: &RequestInputPlan,
123 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
124) -> String {
125 let struct_name = format!("{}Api", tag.to_pascal_case());
126 let plans: Vec<OpPlan> = ops
127 .iter()
128 .map(|op| plan_operation(op, ir, request_inputs))
129 .collect();
130
131 let stem = tag.to_snake_case();
132 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
133
134 fsb = fsb.add_import(ImportSpec::named("crate::runtime::client", "Client"));
136 fsb = fsb.add_import(ImportSpec::named("crate::runtime::error", "Error"));
137
138 let (struct_gen, impl_gen, type_args, client_field_args) = match &config.struct_generics {
140 Some(g) => {
141 let client_args = config.client_type_args.as_deref().unwrap_or("");
142 let param_name = g.split(':').next().unwrap_or(g).trim();
143 (
144 format!("<'a, {g}>"),
145 format!("<'a, {g}>"),
146 format!("<'a, {param_name}>"),
147 client_args.to_string(),
148 )
149 }
150 None => (
151 "<'a>".to_string(),
152 "<'a>".to_string(),
153 "<'a>".to_string(),
154 String::new(),
155 ),
156 };
157
158 let mut body = CodeBlock::builder();
160
161 let doc_struct = format!("/// API operations under the \"{tag}\" tag.");
163 let generics = struct_gen.as_str();
164 let client_type_suffix = client_field_args.as_str();
165 let client_field = format!("client: &'a Client{client_type_suffix},");
166 body.add_code(
167 sigil_quote!(RustLang {
168 $L(doc_struct)
169 pub struct $N(struct_name.as_str())$L(generics) {
170 $L(client_field)
171 }
172 })
173 .expect("struct sigil_quote builds"),
174 );
175 body.add_line();
176
177 let impl_header = format!("impl{impl_gen} {struct_name}{type_args}");
179 body.add(&impl_header, ());
180 body.begin_control_flow("", ());
181
182 let doc_ctor = format!("/// Create a new `{struct_name}` bound to the given client.");
184 body.add_code(
185 sigil_quote!(RustLang {
186 $L(doc_ctor)
187 pub fn $L("new(client: &'a Client@{client_type_suffix}) -> Self") {
188 Self {
189 client,
190 }
191 }
192 })
193 .expect("constructor sigil_quote builds"),
194 );
195
196 for plan in &plans {
198 body.add_line();
199 body.add_code(emit_operation(plan, config, body_emitter));
200 }
201
202 body.end_control_flow(); fsb = fsb.add_code(body.build().expect("body builds"));
205
206 for plan in &plans {
208 fsb = fsb.add_type(emit_response_struct(plan, response_extra_derives));
209 }
210
211 let file = fsb.build().expect("FileSpec builds");
212 file.render(100).expect("FileSpec renders")
213}
214
215pub struct OpPlan<'a> {
220 pub op: &'a IrOperation,
221 pub method_name: String,
222 pub response_type: String,
223 pub path_params: Vec<ParamBinding<'a>>,
224 pub query_params: Vec<ParamBinding<'a>>,
225 pub header_params: Vec<ParamBinding<'a>>,
226 pub body: Option<BodyBinding>,
227 pub typed_responses: Vec<TypedResponse>,
228}
229
230pub struct ParamBinding<'a> {
231 pub param: &'a IrParameter,
232 pub var_name: String,
233 pub rust_type: String,
234 pub is_optional: bool,
235}
236
237pub struct BodyBinding {
238 pub var_name: String,
239 pub rust_type: String,
240 pub media_type: String,
241 pub required: bool,
242 pub encoding: BodyEncoding,
243 pub multipart_supported: bool,
244 pub multipart_parts: Vec<MultipartPart>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum BodyEncoding {
249 Json,
250 FormUrlEncoded,
251 Multipart,
252 Xml,
253 TextPlain,
254 OctetStream,
255 Other(String),
256}
257
258pub struct TypedResponse {
259 pub status: String,
260 pub field_name: String,
261 pub rust_type: String,
262 pub decoding: ResponseDecoding,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum ResponseDecoding {
267 Json,
268 Xml,
269 TextPlain,
270 OctetStream,
271 Other(String),
272}
273
274pub fn plan_operation<'a>(
275 op: &'a IrOperation,
276 ir: &'a IrSpec,
277 request_inputs: &RequestInputPlan,
278) -> OpPlan<'a> {
279 let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
280 let method_name = op_id.to_snake_case();
281 let response_type = format!("{}Response", op_id.to_pascal_case());
282
283 let mut used_names: HashSet<String> = HashSet::new();
284 used_names.insert("self".to_string());
285
286 let mut path_params = Vec::new();
287 let mut query_params = Vec::new();
288 let mut header_params = Vec::new();
289 for p in &op.parameters {
290 let var_name = unique_name(&p.name.to_snake_case(), &mut used_names);
291 let (rust_type, is_optional) = param_rust_type(p, ir);
292 let binding = ParamBinding {
293 param: p,
294 var_name,
295 rust_type,
296 is_optional,
297 };
298 match p.location {
299 ParameterLocation::Path => path_params.push(binding),
300 ParameterLocation::Query => query_params.push(binding),
301 ParameterLocation::Header => header_params.push(binding),
302 ParameterLocation::Cookie => header_params.push(binding),
303 }
304 }
305
306 let body = op
307 .request_body
308 .as_ref()
309 .and_then(|b| plan_body(op, b, &mut used_names, ir, request_inputs));
310
311 let typed_responses = op
312 .responses
313 .iter()
314 .filter_map(|r| plan_response(r, ir))
315 .collect();
316
317 OpPlan {
318 op,
319 method_name,
320 response_type,
321 path_params,
322 query_params,
323 header_params,
324 body,
325 typed_responses,
326 }
327}
328
329pub fn plan_body(
330 op: &IrOperation,
331 b: &IrRequestBody,
332 used_names: &mut HashSet<String>,
333 ir: &IrSpec,
334 request_inputs: &RequestInputPlan,
335) -> Option<BodyBinding> {
336 let (media_type, t) = pick_body_content(b)?;
337 let encoding = body_encoding(&media_type);
338 let rust_type = match encoding {
339 BodyEncoding::OctetStream => "Vec<u8>".to_string(),
340 BodyEncoding::TextPlain => "String".to_string(),
341 BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
342 .map(|input| format!("crate::models::{}", input.name.to_pascal_case()))
343 .unwrap_or_else(|| rust_type_str_qualified(&t, ir)),
344 _ => rust_type_str_qualified(&t, ir),
345 };
346 let multipart_parts = if encoding == BodyEncoding::Multipart {
347 multipart_parts_for_request_body(b, &media_type, ir).unwrap_or_default()
348 } else {
349 Vec::new()
350 };
351 let multipart_supported = encoding != BodyEncoding::Multipart
352 || multipart_parts_for_request_body(b, &media_type, ir).is_some();
353 let var_name = unique_name("body", used_names);
354 Some(BodyBinding {
355 var_name,
356 rust_type,
357 media_type,
358 required: b.required,
359 encoding,
360 multipart_supported,
361 multipart_parts,
362 })
363}
364
365pub fn plan_response(r: &IrResponse, ir: &IrSpec) -> Option<TypedResponse> {
366 let (media_type, t) = pick_response_content(r)?;
367 let decoding = response_decoding(&media_type);
368 let rust_type = match decoding {
369 ResponseDecoding::OctetStream => "Vec<u8>".to_string(),
370 ResponseDecoding::TextPlain => "String".to_string(),
371 _ => rust_type_str_qualified(&t, ir),
372 };
373 Some(TypedResponse {
374 status: r.status.clone(),
375 field_name: response_field_name(&r.status),
376 rust_type,
377 decoding,
378 })
379}
380
381pub fn param_rust_type(p: &IrParameter, ir: &IrSpec) -> (String, bool) {
382 let base = rust_type_str_qualified(&p.type_expr, ir);
383 if p.required {
384 (base, false)
385 } else if matches!(p.type_expr, IrTypeExpr::Nullable(_)) {
386 (base, true)
388 } else {
389 (format!("Option<{base}>"), true)
390 }
391}
392
393pub fn unique_name(desired: &str, used: &mut HashSet<String>) -> String {
394 if used.insert(desired.to_string()) {
395 return desired.to_string();
396 }
397 for i in 2..=u32::MAX {
398 let candidate = format!("{desired}_{i}");
399 if used.insert(candidate.clone()) {
400 return candidate;
401 }
402 }
403 unreachable!("name collision space exhausted")
404}
405
406fn emit_operation(
411 plan: &OpPlan<'_>,
412 config: &RustBackendConfig,
413 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
414) -> CodeBlock {
415 let OpPlan {
416 op,
417 method_name,
418 response_type,
419 ..
420 } = plan;
421
422 let mut b = CodeBlock::builder();
423
424 if let Some(summary) = &op.summary {
426 for line in summary.lines() {
427 if line.is_empty() {
428 b.add("///\n", ());
429 } else {
430 b.add(&format!("/// {line}\n"), ());
431 }
432 }
433 } else {
434 b.add(
435 &format!("/// {} {}\n", op.method.to_uppercase(), op.path),
436 (),
437 );
438 }
439 if let Some(desc) = &op.description {
440 b.add("///\n", ());
441 for line in desc.lines() {
442 if line.is_empty() {
443 b.add("///\n", ());
444 } else {
445 b.add(&format!("/// {line}\n"), ());
446 }
447 }
448 }
449
450 let mut params = Vec::new();
452 params.push("&self".to_string());
453 for p in plan
454 .path_params
455 .iter()
456 .chain(&plan.query_params)
457 .chain(&plan.header_params)
458 {
459 let ty = if is_copy_type(&p.rust_type) {
460 p.rust_type.clone()
461 } else if p.rust_type == "String" {
462 "&str".to_string()
463 } else if let Some(inner) = p
464 .rust_type
465 .strip_prefix("Vec<")
466 .and_then(|s| s.strip_suffix('>'))
467 {
468 format!("&[{inner}]")
469 } else {
470 format!("&{}", p.rust_type)
471 };
472 params.push(format!("{}: {ty}", p.var_name));
473 }
474 if let Some(body) = &plan.body {
475 let ty = if body.required {
476 format!("&{}", body.rust_type)
477 } else {
478 format!("Option<&{}>", body.rust_type)
479 };
480 params.push(format!("{}: {ty}", body.var_name));
481 }
482
483 let async_kw = if config.is_async { "async " } else { "" };
484 b.add(
485 &format!(
486 "pub {async_kw}fn {method_name}(\n {},\n) -> Result<{response_type}, Error>",
487 params.join(",\n "),
488 ),
489 (),
490 );
491 b.begin_control_flow("", ());
492
493 b.add_code(body_emitter(plan));
495
496 b.end_control_flow();
497 b.build().unwrap()
498}
499
500pub fn emit_response_struct(plan: &OpPlan<'_>, extra: Option<&ExtraDeriveConfig>) -> TypeSpec {
501 let mut tb = TypeSpec::builder(&plan.response_type, TypeKind::Struct);
502 tb = tb.visibility(Visibility::Public);
503 tb = tb.doc(&format!("Response from `{}`.", plan.method_name));
504
505 let mut ann = AnnotationSpec::new("derive").args(["Debug"]);
506 if let Some(cfg) = extra {
507 ann = ann.args(cfg.derives.iter().map(|s| s.as_str()));
508 }
509 tb = tb.annotate(ann);
510
511 {
513 let fb = FieldSpec::builder("status_code", TypeName::primitive("u16"));
514 let fb = fb.visibility(Visibility::Public);
515 tb = tb.add_field(fb.build().expect("FieldSpec builds"));
516 }
517
518 let mut seen: HashSet<String> = HashSet::new();
520 for tr in &plan.typed_responses {
521 if !seen.insert(tr.field_name.clone()) {
522 continue;
523 }
524 let fb = FieldSpec::builder(
525 &tr.field_name,
526 TypeName::raw(&format!("Option<{}>", tr.rust_type)),
527 );
528 let fb = fb.visibility(Visibility::Public);
529 tb = tb.add_field(fb.build().expect("FieldSpec builds"));
530 }
531
532 tb.build().expect("TypeSpec builds")
533}
534
535pub fn sanitize_operation_id(id: &str, method: &str, path: &str) -> String {
540 if !id.is_empty() {
541 return id.to_string();
542 }
543 format!(
544 "{}_{}",
545 method,
546 path.replace('/', "_").replace(['{', '}'], "")
547 )
548}
549
550pub fn response_field_name(status: &str) -> String {
551 match status {
552 "200" => "data".to_string(),
553 "201" => "created".to_string(),
554 "204" => "no_content".to_string(),
555 "default" => "error_body".to_string(),
556 s if s.ends_with("XX") => {
557 let prefix = &s[..s.len() - 2];
558 format!("status_{prefix}xx")
559 }
560 s => format!("status_{s}"),
561 }
562}
563
564pub fn status_match_pattern(status: &str) -> String {
566 match status {
567 "default" => "_".to_string(),
568 s if s.ends_with("XX") => {
569 let prefix: u16 = s[..s.len() - 2].parse().unwrap_or(0);
570 let lo = prefix * 100;
571 let hi = lo + 99;
572 format!("{lo}..={hi}")
573 }
574 s => s.to_string(),
575 }
576}
577
578pub fn pick_body_type(b: &IrRequestBody) -> Option<IrTypeExpr> {
579 pick_body_content(b).map(|(_, t)| t)
580}
581
582pub fn pick_response_type(r: &IrResponse) -> Option<IrTypeExpr> {
583 pick_response_content(r).map(|(_, t)| t)
584}
585
586fn pick_body_content(b: &IrRequestBody) -> Option<(String, IrTypeExpr)> {
587 pick_media_type(&b.content, |media_type| {
588 media_type_base(media_type) == "application/json"
589 })
590 .or_else(|| pick_media_type(&b.content, is_json_media_type))
591 .or_else(|| {
592 pick_media_type(&b.content, |media_type| {
593 media_type_base(media_type) == "multipart/form-data"
594 })
595 })
596 .or_else(|| {
597 pick_media_type(&b.content, |media_type| {
598 media_type_base(media_type) == "application/x-www-form-urlencoded"
599 })
600 })
601 .or_else(|| pick_media_type(&b.content, is_xml_media_type))
602 .or_else(|| {
603 pick_media_type(&b.content, |media_type| {
604 media_type_base(media_type) == "text/plain"
605 })
606 })
607 .or_else(|| {
608 pick_media_type(&b.content, |media_type| {
609 media_type_base(media_type) == "application/octet-stream"
610 })
611 })
612 .or_else(|| pick_first_content(&b.content))
613}
614
615fn pick_response_content(r: &IrResponse) -> Option<(String, IrTypeExpr)> {
616 pick_media_type(&r.content, |media_type| {
617 media_type_base(media_type) == "application/json"
618 })
619 .or_else(|| pick_media_type(&r.content, is_json_media_type))
620 .or_else(|| {
621 pick_media_type(&r.content, |media_type| {
622 media_type_base(media_type) == "application/octet-stream"
623 })
624 })
625 .or_else(|| {
626 pick_media_type(&r.content, |media_type| {
627 media_type_base(media_type) == "text/plain"
628 })
629 })
630 .or_else(|| pick_media_type(&r.content, is_xml_media_type))
631 .or_else(|| pick_first_content(&r.content))
632}
633
634fn pick_media_type(
635 content: &indexmap::IndexMap<String, IrTypeExpr>,
636 predicate: impl Fn(&str) -> bool,
637) -> Option<(String, IrTypeExpr)> {
638 content
639 .iter()
640 .find(|(media_type, _)| predicate(media_type))
641 .map(|(media_type, t)| (media_type.clone(), t.clone()))
642}
643
644fn pick_first_content(
645 content: &indexmap::IndexMap<String, IrTypeExpr>,
646) -> Option<(String, IrTypeExpr)> {
647 content
648 .iter()
649 .next()
650 .map(|(media_type, t)| (media_type.clone(), t.clone()))
651}
652
653fn body_encoding(media_type: &str) -> BodyEncoding {
654 let base = media_type_base(media_type);
655 match base.as_str() {
656 "application/json" => BodyEncoding::Json,
657 "application/x-www-form-urlencoded" => BodyEncoding::FormUrlEncoded,
658 "multipart/form-data" => BodyEncoding::Multipart,
659 "application/xml" | "text/xml" => BodyEncoding::Xml,
660 "text/plain" => BodyEncoding::TextPlain,
661 "application/octet-stream" => BodyEncoding::OctetStream,
662 _ if is_json_media_type(media_type) => BodyEncoding::Json,
663 _ if is_xml_media_type(media_type) => BodyEncoding::Xml,
664 _ => BodyEncoding::Other(media_type.to_string()),
665 }
666}
667
668fn response_decoding(media_type: &str) -> ResponseDecoding {
669 let base = media_type_base(media_type);
670 match base.as_str() {
671 "application/json" => ResponseDecoding::Json,
672 "application/xml" | "text/xml" => ResponseDecoding::Xml,
673 "text/plain" => ResponseDecoding::TextPlain,
674 "application/octet-stream" => ResponseDecoding::OctetStream,
675 _ if is_json_media_type(media_type) => ResponseDecoding::Json,
676 _ if is_xml_media_type(media_type) => ResponseDecoding::Xml,
677 _ => ResponseDecoding::Other(media_type.to_string()),
678 }
679}
680
681fn media_type_base(media_type: &str) -> String {
682 media_type
683 .split(';')
684 .next()
685 .unwrap_or(media_type)
686 .trim()
687 .to_ascii_lowercase()
688}
689
690fn is_json_media_type(media_type: &str) -> bool {
691 let base = media_type_base(media_type);
692 base == "application/json" || base.ends_with("+json")
693}
694
695fn is_xml_media_type(media_type: &str) -> bool {
696 let base = media_type_base(media_type);
697 base == "application/xml" || base == "text/xml" || base.ends_with("+xml")
698}
699
700pub fn rust_field_name(wire_name: &str) -> String {
701 escape_rust_keyword(&wire_name.to_snake_case())
702}
703
704fn escape_rust_keyword(name: &str) -> String {
705 const KEYWORDS: &[&str] = &[
706 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
707 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
708 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
709 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
710 ];
711 if KEYWORDS.contains(&name) {
712 format!("r#{name}")
713 } else {
714 name.to_string()
715 }
716}
717
718pub fn rust_string_literal(value: &str) -> String {
719 format!("{value:?}")
720}
721
722pub fn text_field_expr(base: &str, part: &MultipartPart) -> String {
723 let field_name = rust_field_name(&part.wire_name);
724 match part.value_encoding {
725 MultipartValueEncoding::Text => format!("{base}.{field_name}.to_string()"),
726 MultipartValueEncoding::Json => format!("serde_json::to_string(&{base}.{field_name})?"),
727 MultipartValueEncoding::Unsupported => {
728 unreachable!("unsupported multipart parts are emitted before value expressions")
729 }
730 }
731}
732
733pub fn binary_field_expr(base: &str, part: &MultipartPart) -> String {
734 format!("{base}.{}.data.clone()", rust_field_name(&part.wire_name))
735}
736
737pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String {
738 match part.value_encoding {
739 MultipartValueEncoding::Text => format!("{value}.to_string()"),
740 MultipartValueEncoding::Json => format!("serde_json::to_string({value})?"),
741 MultipartValueEncoding::Unsupported => {
742 unreachable!("unsupported multipart parts are emitted before value expressions")
743 }
744 }
745}
746
747pub fn optional_binary_field_expr(value: &str) -> String {
748 format!("{value}.data.clone()")
749}
750
751pub fn binary_filename_expr(base: &str, part: &MultipartPart) -> String {
752 format!(
753 "{base}.{}.filename_or_default({}).to_string()",
754 rust_field_name(&part.wire_name),
755 rust_string_literal(&part.default_filename)
756 )
757}
758
759pub fn optional_binary_filename_expr(value: &str, part: &MultipartPart) -> String {
760 format!(
761 "{value}.filename_or_default({}).to_string()",
762 rust_string_literal(&part.default_filename)
763 )
764}
765
766pub fn response_value_expr(tr: &TypedResponse, bytes_var: &str) -> String {
767 let owned_bytes_expr = bytes_var.strip_prefix('&').unwrap_or(bytes_var);
768 match tr.decoding {
769 ResponseDecoding::Json => {
770 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
771 }
772 ResponseDecoding::Xml => {
773 format!(
774 "serde_xml_rs::from_reader(std::io::Cursor::new({bytes_var})).map_err(Error::Xml)"
775 )
776 }
777 ResponseDecoding::TextPlain => {
778 format!("Ok::<String, Error>(String::from_utf8_lossy({bytes_var}).into_owned())")
779 }
780 ResponseDecoding::OctetStream => {
781 format!("Ok::<Vec<u8>, Error>({owned_bytes_expr}.to_vec())")
782 }
783 ResponseDecoding::Other(_) => {
784 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
785 }
786 }
787}
788
789pub fn response_value_expr_from_str(tr: &TypedResponse, body_var: &str) -> String {
790 match tr.decoding {
791 ResponseDecoding::Json => {
792 format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
793 }
794 ResponseDecoding::Xml => {
795 format!("serde_xml_rs::from_str({body_var}).map_err(Error::Xml)")
796 }
797 ResponseDecoding::TextPlain => format!("Ok::<String, Error>({body_var})"),
798 ResponseDecoding::OctetStream => {
799 format!("Ok::<Vec<u8>, Error>({body_var}.into_bytes())")
800 }
801 ResponseDecoding::Other(_) => {
802 format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
803 }
804 }
805}
806
807pub fn response_needs_bytes(typed_responses: &[TypedResponse]) -> bool {
808 typed_responses
809 .iter()
810 .any(|tr| matches!(tr.decoding, ResponseDecoding::OctetStream))
811}
812
813pub fn render_to_string(var: &str, type_expr: &IrTypeExpr, _is_optional: bool) -> String {
814 match type_expr {
815 IrTypeExpr::Array(_) => {
816 format!("{var}.iter().map(ToString::to_string).collect::<Vec<_>>().join(\",\")")
817 }
818 _ => format!("{var}.to_string()"),
819 }
820}
821
822pub fn is_copy_type(ty: &str) -> bool {
823 matches!(
824 ty,
825 "bool" | "i32" | "i64" | "f32" | "f64" | "u8" | "u16" | "u32" | "u64"
826 ) || ty.starts_with("Option<")
827 && is_copy_type(
828 ty.strip_prefix("Option<")
829 .unwrap()
830 .strip_suffix('>')
831 .unwrap_or(""),
832 )
833}
834
835pub fn emit_result_init(
841 b: &mut CodeBlockBuilder,
842 response_type: &str,
843 typed_responses: &[TypedResponse],
844) {
845 let mut fields = vec!["status_code".to_string()];
846 let mut seen: HashSet<String> = HashSet::new();
847 for tr in typed_responses {
848 if seen.insert(tr.field_name.clone()) {
849 fields.push(format!("{}: None", tr.field_name));
850 }
851 }
852 b.add(
853 &format!(
854 "let mut result = {response_type} {{ {} }};\n",
855 fields.join(", ")
856 ),
857 (),
858 );
859}
860
861pub fn emit_response_match(
863 b: &mut CodeBlockBuilder,
864 typed_responses: &[TypedResponse],
865 value_expr: &dyn Fn(&TypedResponse) -> String,
866) {
867 b.begin_control_flow("match status_code", ());
868 let mut seen: HashSet<String> = HashSet::new();
869 for tr in typed_responses {
870 if !seen.insert(format!("{}-{}", tr.status, tr.field_name)) {
871 continue;
872 }
873 let status_pattern = status_match_pattern(&tr.status);
874 let value_expr = value_expr(tr);
875 b.begin_control_flow(&format!("{status_pattern} =>"), ());
876 b.add(
877 &format!("result.{} = Some({value_expr}?);\n", tr.field_name),
878 (),
879 );
880 b.end_control_flow();
881 }
882 if !typed_responses.iter().any(|tr| tr.status == "default") {
883 b.add("_ => {}\n", ());
884 }
885 b.end_control_flow();
886}