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