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