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::generators::response_names::{
17 response_entry_name as response_variant_name, response_match_rank,
18};
19use crate::ir::types::{
20 IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation,
21};
22use heck::{ToPascalCase, ToSnakeCase};
23use sigil_stitch::code_block::{CodeBlock, CodeBlockBuilder};
24use sigil_stitch::prelude::sigil_quote;
25use sigil_stitch::spec::annotation_spec::AnnotationSpec;
26use sigil_stitch::spec::field_spec::FieldSpec;
27use sigil_stitch::spec::file_spec::FileSpec;
28use sigil_stitch::spec::import_spec::ImportSpec;
29use sigil_stitch::spec::modifiers::{TypeKind, Visibility};
30use sigil_stitch::spec::type_spec::TypeSpec;
31use sigil_stitch::type_name::TypeName;
32
33use super::config::ExtraDeriveConfig;
34use super::emit_models::rust_type_str_qualified;
35
36pub struct RustBackendConfig {
42 pub is_async: bool,
44 pub struct_generics: Option<String>,
47 pub client_type_args: Option<String>,
50}
51
52pub fn generate_api_files(
58 ir: &IrSpec,
59 header: &str,
60 config: &RustBackendConfig,
61 response_extra_derives: Option<&ExtraDeriveConfig>,
62 request_inputs: &RequestInputPlan,
63 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
64) -> Result<Vec<FileInfo>, String> {
65 let by_tag = group_by_tag(&ir.operations);
66 let mut files = Vec::with_capacity(by_tag.len());
67 let mut mod_entries = Vec::new();
68
69 for (tag, ops) in &by_tag {
70 let stem = tag.to_snake_case();
71 let filename = format!("{stem}.rs");
72 mod_entries.push(stem);
73 let body = emit_api_file(
74 tag,
75 ops,
76 ir,
77 config,
78 response_extra_derives,
79 request_inputs,
80 body_emitter,
81 );
82 let content = format!("{header}{body}");
83 files.push(FileInfo::api(filename, content));
84 }
85
86 let mut mod_content = String::from(header);
88 for entry in &mod_entries {
89 mod_content.push_str(&format!("mod {entry};\npub use {entry}::*;\n"));
90 }
91 files.push(FileInfo::api("mod.rs".to_string(), mod_content));
92
93 Ok(files)
94}
95
96fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation>> {
101 let mut out: BTreeMap<String, Vec<&IrOperation>> = BTreeMap::new();
102 for op in operations {
103 let tags: Vec<String> = if op.tags.is_empty() {
104 vec!["default".to_string()]
105 } else {
106 op.tags.clone()
107 };
108 for tag in tags {
109 out.entry(tag).or_default().push(op);
110 }
111 }
112 out
113}
114
115fn emit_api_file(
120 tag: &str,
121 ops: &[&IrOperation],
122 ir: &IrSpec,
123 config: &RustBackendConfig,
124 response_extra_derives: Option<&ExtraDeriveConfig>,
125 request_inputs: &RequestInputPlan,
126 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
127) -> String {
128 let struct_name = format!("{}Api", tag.to_pascal_case());
129 let plans: Vec<OpPlan> = ops
130 .iter()
131 .map(|op| plan_operation(op, ir, request_inputs))
132 .collect();
133
134 let stem = tag.to_snake_case();
135 let mut fsb = FileSpec::builder(&format!("{stem}.rs"));
136
137 fsb = fsb.add_import(ImportSpec::named("crate::runtime::client", "Client"));
139 fsb = fsb.add_import(ImportSpec::named("crate::runtime::error", "ApiError"));
140 fsb = fsb.add_import(ImportSpec::named("crate::runtime::error", "Error"));
141
142 let (struct_gen, impl_gen, type_args, client_field_args) = match &config.struct_generics {
144 Some(g) => {
145 let client_args = config.client_type_args.as_deref().unwrap_or("");
146 let param_names = g
147 .split(',')
148 .map(|param| param.split(':').next().unwrap_or(param).trim())
149 .collect::<Vec<_>>()
150 .join(", ");
151 (
152 format!("<'a, {g}>"),
153 format!("<'a, {g}>"),
154 format!("<'a, {param_names}>"),
155 client_args.to_string(),
156 )
157 }
158 None => (
159 "<'a>".to_string(),
160 "<'a>".to_string(),
161 "<'a>".to_string(),
162 String::new(),
163 ),
164 };
165
166 let mut body = CodeBlock::builder();
168
169 let doc_struct = format!("/// API operations under the \"{tag}\" tag.");
171 let generics = struct_gen.as_str();
172 let client_type_suffix = client_field_args.as_str();
173 let client_field = format!("client: &'a Client{client_type_suffix},");
174 body.add_code(
175 sigil_quote!(RustLang {
176 $L(doc_struct)
177 pub struct $N(struct_name.as_str())$L(generics) {
178 $L(client_field)
179 }
180 })
181 .expect("struct sigil_quote builds"),
182 );
183 body.add_line();
184
185 let impl_header = format!("impl{impl_gen} {struct_name}{type_args}");
187 body.add(&impl_header, ());
188 body.begin_control_flow("", ());
189
190 let doc_ctor = format!("/// Create a new `{struct_name}` bound to the given client.");
192 body.add_code(
193 sigil_quote!(RustLang {
194 $L(doc_ctor)
195 pub fn $L("new(client: &'a Client@{client_type_suffix}) -> Self") {
196 Self {
197 client,
198 }
199 }
200 })
201 .expect("constructor sigil_quote builds"),
202 );
203
204 for plan in &plans {
206 body.add_line();
207 body.add_code(emit_operation(plan, config, body_emitter));
208 }
209
210 body.end_control_flow(); fsb = fsb.add_code(body.build().expect("body builds"));
213
214 for plan in &plans {
216 fsb = fsb.add_type(emit_response_struct(plan, response_extra_derives));
217 fsb = fsb.add_code(emit_error_enum(plan));
218 }
219
220 let file = fsb.build().expect("FileSpec builds");
221 file.render(100).expect("FileSpec renders")
222}
223
224pub struct OpPlan<'a> {
229 pub op: &'a IrOperation,
230 pub method_name: String,
231 pub response_type: String,
232 pub error_type: String,
233 pub path_params: Vec<ParamBinding<'a>>,
234 pub query_params: Vec<ParamBinding<'a>>,
235 pub header_params: Vec<ParamBinding<'a>>,
236 pub body: Option<BodyBinding>,
237 pub typed_responses: Vec<TypedResponse>,
238 pub error_responses: Vec<ErrorResponse>,
239}
240
241pub struct ParamBinding<'a> {
242 pub param: &'a IrParameter,
243 pub var_name: String,
244 pub rust_type: String,
245 pub is_optional: bool,
246}
247
248pub struct BodyBinding {
249 pub var_name: String,
250 pub rust_type: String,
251 pub media_type: String,
252 pub required: bool,
253 pub encoding: BodyEncoding,
254 pub multipart_supported: bool,
255 pub multipart_parts: Vec<MultipartPart>,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub enum BodyEncoding {
260 Json,
261 FormUrlEncoded,
262 Multipart,
263 Xml,
264 TextPlain,
265 OctetStream,
266 Other(String),
267}
268
269pub struct TypedResponse {
270 pub status: String,
271 pub field_name: String,
272 pub rust_type: String,
273 pub decoding: ResponseDecoding,
274}
275
276pub struct ErrorResponse {
277 pub status: String,
278 pub variant_name: String,
279 pub rust_type: String,
280 pub decoding: Option<ResponseDecoding>,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub enum ResponseDecoding {
285 Json,
286 Xml,
287 TextPlain,
288 OctetStream,
289 Other(String),
290}
291
292pub fn plan_operation<'a>(
293 op: &'a IrOperation,
294 ir: &'a IrSpec,
295 request_inputs: &RequestInputPlan,
296) -> OpPlan<'a> {
297 let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
298 let method_name = op_id.to_snake_case();
299 let response_type = format!("{}Response", op_id.to_pascal_case());
300 let error_type = format!("{}Error", op_id.to_pascal_case());
301
302 let mut used_names: HashSet<String> = HashSet::new();
303 used_names.insert("self".to_string());
304
305 let mut path_params = Vec::new();
306 let mut query_params = Vec::new();
307 let mut header_params = Vec::new();
308 for p in &op.parameters {
309 let var_name = unique_name(&p.name.to_snake_case(), &mut used_names);
310 let (rust_type, is_optional) = param_rust_type(p, ir);
311 let binding = ParamBinding {
312 param: p,
313 var_name,
314 rust_type,
315 is_optional,
316 };
317 match p.location {
318 ParameterLocation::Path => path_params.push(binding),
319 ParameterLocation::Query => query_params.push(binding),
320 ParameterLocation::Header => header_params.push(binding),
321 ParameterLocation::Cookie => header_params.push(binding),
322 }
323 }
324
325 let body = op
326 .request_body
327 .as_ref()
328 .and_then(|b| plan_body(op, b, &mut used_names, ir, request_inputs));
329
330 let mut typed_responses: Vec<TypedResponse> = op
331 .responses
332 .iter()
333 .filter(|r| is_success_status(&r.status))
334 .filter_map(|r| plan_response(r, ir))
335 .collect();
336 typed_responses.sort_by_key(|r| response_match_rank(&r.status));
337 let error_responses = op
338 .responses
339 .iter()
340 .filter(|r| !is_success_status(&r.status))
341 .map(|r| plan_error_response(r, ir))
342 .collect();
343
344 OpPlan {
345 op,
346 method_name,
347 response_type,
348 error_type,
349 path_params,
350 query_params,
351 header_params,
352 body,
353 typed_responses,
354 error_responses,
355 }
356}
357
358pub fn plan_body(
359 op: &IrOperation,
360 b: &IrRequestBody,
361 used_names: &mut HashSet<String>,
362 ir: &IrSpec,
363 request_inputs: &RequestInputPlan,
364) -> Option<BodyBinding> {
365 let (media_type, t) = pick_body_content(b)?;
366 let encoding = body_encoding(&media_type);
367 let rust_type = match encoding {
368 BodyEncoding::OctetStream => "Vec<u8>".to_string(),
369 BodyEncoding::TextPlain => "String".to_string(),
370 BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
371 .map(|input| format!("crate::models::{}", input.name.to_pascal_case()))
372 .unwrap_or_else(|| rust_type_str_qualified(&t, ir)),
373 _ => rust_type_str_qualified(&t, ir),
374 };
375 let multipart_parts = if encoding == BodyEncoding::Multipart {
376 multipart_parts_for_request_body(b, &media_type, ir).unwrap_or_default()
377 } else {
378 Vec::new()
379 };
380 let multipart_supported = encoding != BodyEncoding::Multipart
381 || multipart_parts_for_request_body(b, &media_type, ir).is_some();
382 let var_name = unique_name("body", used_names);
383 Some(BodyBinding {
384 var_name,
385 rust_type,
386 media_type,
387 required: b.required,
388 encoding,
389 multipart_supported,
390 multipart_parts,
391 })
392}
393
394pub fn plan_response(r: &IrResponse, ir: &IrSpec) -> Option<TypedResponse> {
395 let (media_type, t) = pick_response_content(r)?;
396 let decoding = response_decoding(&media_type);
397 let rust_type = match decoding {
398 ResponseDecoding::OctetStream => "Vec<u8>".to_string(),
399 ResponseDecoding::TextPlain => "String".to_string(),
400 _ => rust_type_str_qualified(&t, ir),
401 };
402 Some(TypedResponse {
403 status: r.status.clone(),
404 field_name: response_field_name(&r.status),
405 rust_type,
406 decoding,
407 })
408}
409
410pub fn plan_error_response(r: &IrResponse, ir: &IrSpec) -> ErrorResponse {
411 let (rust_type, decoding) = match pick_response_content(r) {
412 Some((media_type, t)) => {
413 let decoding = response_decoding(&media_type);
414 let rust_type = match decoding {
415 ResponseDecoding::OctetStream => "Vec<u8>".to_string(),
416 ResponseDecoding::TextPlain => "String".to_string(),
417 _ => rust_type_str_qualified(&t, ir),
418 };
419 (rust_type, Some(decoding))
420 }
421 None => ("()".to_string(), None),
422 };
423 ErrorResponse {
424 status: r.status.clone(),
425 variant_name: response_variant_name(&r.status),
426 rust_type,
427 decoding,
428 }
429}
430
431pub fn is_success_status(status: &str) -> bool {
432 status
433 .parse::<u16>()
434 .is_ok_and(|code| (200..300).contains(&code))
435 || status.eq_ignore_ascii_case("2XX")
436}
437
438pub fn param_rust_type(p: &IrParameter, ir: &IrSpec) -> (String, bool) {
439 let base = rust_type_str_qualified(&p.type_expr, ir);
440 if p.required {
441 (base, false)
442 } else if matches!(p.type_expr, IrTypeExpr::Nullable(_)) {
443 (base, true)
445 } else {
446 (format!("Option<{base}>"), true)
447 }
448}
449
450pub fn unique_name(desired: &str, used: &mut HashSet<String>) -> String {
451 if used.insert(desired.to_string()) {
452 return desired.to_string();
453 }
454 for i in 2..=u32::MAX {
455 let candidate = format!("{desired}_{i}");
456 if used.insert(candidate.clone()) {
457 return candidate;
458 }
459 }
460 unreachable!("name collision space exhausted")
461}
462
463fn emit_operation(
468 plan: &OpPlan<'_>,
469 config: &RustBackendConfig,
470 body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock,
471) -> CodeBlock {
472 let OpPlan {
473 op,
474 method_name,
475 response_type,
476 error_type,
477 ..
478 } = plan;
479
480 let mut b = CodeBlock::builder();
481
482 if let Some(summary) = &op.summary {
484 for line in summary.lines() {
485 if line.is_empty() {
486 b.add("///\n", ());
487 } else {
488 b.add(&format!("/// {line}\n"), ());
489 }
490 }
491 } else {
492 b.add(
493 &format!("/// {} {}\n", op.method.to_uppercase(), op.path),
494 (),
495 );
496 }
497 if let Some(desc) = &op.description {
498 b.add("///\n", ());
499 for line in desc.lines() {
500 if line.is_empty() {
501 b.add("///\n", ());
502 } else {
503 b.add(&format!("/// {line}\n"), ());
504 }
505 }
506 }
507
508 let mut params = Vec::new();
510 params.push("&self".to_string());
511 for p in plan
512 .path_params
513 .iter()
514 .chain(&plan.query_params)
515 .chain(&plan.header_params)
516 {
517 let ty = if is_copy_type(&p.rust_type) {
518 p.rust_type.clone()
519 } else if p.rust_type == "String" {
520 "&str".to_string()
521 } else if let Some(inner) = p
522 .rust_type
523 .strip_prefix("Vec<")
524 .and_then(|s| s.strip_suffix('>'))
525 {
526 format!("&[{inner}]")
527 } else {
528 format!("&{}", p.rust_type)
529 };
530 params.push(format!("{}: {ty}", p.var_name));
531 }
532 if let Some(body) = &plan.body {
533 let ty = if body.required {
534 format!("&{}", body.rust_type)
535 } else {
536 format!("Option<&{}>", body.rust_type)
537 };
538 params.push(format!("{}: {ty}", body.var_name));
539 }
540
541 let async_kw = if config.is_async { "async " } else { "" };
542 b.add(
543 &format!(
544 "pub {async_kw}fn {method_name}(\n {},\n) -> Result<{response_type}, {error_type}>",
545 params.join(",\n "),
546 ),
547 (),
548 );
549 b.begin_control_flow("", ());
550
551 b.add_code(body_emitter(plan));
553
554 b.end_control_flow();
555 b.build().unwrap()
556}
557
558pub fn emit_response_struct(plan: &OpPlan<'_>, extra: Option<&ExtraDeriveConfig>) -> TypeSpec {
559 let mut tb = TypeSpec::builder(&plan.response_type, TypeKind::Struct);
560 tb = tb.visibility(Visibility::Public);
561 tb = tb.doc(&format!("Response from `{}`.", plan.method_name));
562
563 let mut ann = AnnotationSpec::new("derive").args(["Debug"]);
564 if let Some(cfg) = extra {
565 ann = ann.args(cfg.derives.iter().map(|s| s.as_str()));
566 }
567 tb = tb.annotate(ann);
568
569 {
571 let fb = FieldSpec::builder("status_code", TypeName::primitive("u16"));
572 let fb = fb.visibility(Visibility::Public);
573 tb = tb.add_field(fb.build().expect("FieldSpec builds"));
574 }
575
576 let mut seen: HashSet<String> = HashSet::new();
578 for tr in &plan.typed_responses {
579 if !seen.insert(tr.field_name.clone()) {
580 continue;
581 }
582 let fb = FieldSpec::builder(
583 &tr.field_name,
584 TypeName::raw(&format!("Option<{}>", tr.rust_type)),
585 );
586 let fb = fb.visibility(Visibility::Public);
587 tb = tb.add_field(fb.build().expect("FieldSpec builds"));
588 }
589
590 tb.build().expect("TypeSpec builds")
591}
592
593pub fn emit_error_enum(plan: &OpPlan<'_>) -> CodeBlock {
594 let mut cb = CodeBlock::builder();
595 let mut variants = Vec::new();
596 let mut seen: HashSet<String> = HashSet::new();
597 for er in &plan.error_responses {
598 let variant = unique_variant_name(&er.variant_name, &mut seen);
599 variants.push((variant, er.rust_type.clone()));
600 }
601 let unexpected_variant = unique_variant_name("Unexpected", &mut seen);
602 let transport_variant = unique_variant_name("Transport", &mut seen);
603
604 cb.add(&format!("/// Error from `{}`.\n", plan.method_name), ());
605 cb.add("#[derive(Debug)]\n", ());
606 cb.add(&format!("pub enum {} {{\n", plan.error_type), ());
607 for (variant, rust_type) in &variants {
608 cb.add(&format!(" {variant}(ApiError<{rust_type}>),\n"), ());
609 }
610 cb.add(
611 &format!(" {unexpected_variant}(ApiError<Vec<u8>>),\n"),
612 (),
613 );
614 cb.add(&format!(" {transport_variant}(Error),\n"), ());
615 cb.add("}\n\n", ());
616
617 cb.add(
618 &format!("impl From<Error> for {} {{\n", plan.error_type),
619 (),
620 );
621 cb.add(" fn from(error: Error) -> Self {\n", ());
622 cb.add(&format!(" Self::{transport_variant}(error)\n"), ());
623 cb.add(" }\n", ());
624 cb.add("}\n\n", ());
625
626 cb.add(
627 &format!("impl std::fmt::Display for {} {{\n", plan.error_type),
628 (),
629 );
630 cb.add(
631 " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n",
632 (),
633 );
634 cb.add(" match self {\n", ());
635 for (variant, _) in &variants {
636 cb.add(
637 &format!(" Self::{variant}(err) => write!(f, \"HTTP error {{}}\", err.status_code()),\n"),
638 (),
639 );
640 }
641 cb.add(&format!(
642 " Self::{unexpected_variant}(err) => write!(f, \"unexpected HTTP error {{}}\", err.status_code()),\n"
643 ), ());
644 cb.add(
645 &format!(" Self::{transport_variant}(err) => std::fmt::Display::fmt(err, f),\n"),
646 (),
647 );
648 cb.add(" }\n", ());
649 cb.add(" }\n", ());
650 cb.add("}\n\n", ());
651
652 cb.add(
653 &format!("impl std::error::Error for {} {{\n", plan.error_type),
654 (),
655 );
656 cb.add(
657 " fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {\n",
658 (),
659 );
660 cb.add(" match self {\n", ());
661 cb.add(
662 &format!(" Self::{transport_variant}(err) => Some(err),\n"),
663 (),
664 );
665 cb.add(" _ => None,\n", ());
666 cb.add(" }\n", ());
667 cb.add(" }\n", ());
668 cb.add("}\n", ());
669
670 cb.build().expect("error enum builds")
671}
672
673fn unique_variant_name(desired: &str, used: &mut HashSet<String>) -> String {
674 if used.insert(desired.to_string()) {
675 return desired.to_string();
676 }
677 for i in 2..=u32::MAX {
678 let candidate = format!("{desired}{i}");
679 if used.insert(candidate.clone()) {
680 return candidate;
681 }
682 }
683 unreachable!("variant collision space exhausted")
684}
685
686pub fn sanitize_operation_id(id: &str, method: &str, path: &str) -> String {
691 if !id.is_empty() {
692 return id.to_string();
693 }
694 format!(
695 "{}_{}",
696 method,
697 path.replace('/', "_").replace(['{', '}'], "")
698 )
699}
700
701pub fn response_field_name(status: &str) -> String {
702 match status {
703 "200" => "data".to_string(),
704 "201" => "created".to_string(),
705 "204" => "no_content".to_string(),
706 "default" => "error_body".to_string(),
707 s if s.ends_with("XX") => {
708 let prefix = &s[..s.len() - 2];
709 format!("status_{prefix}xx")
710 }
711 s => format!("status_{s}"),
712 }
713}
714
715pub fn status_match_pattern(status: &str) -> String {
717 match status {
718 "default" => "_".to_string(),
719 s if s.ends_with("XX") => {
720 let prefix: u16 = s[..s.len() - 2].parse().unwrap_or(0);
721 let lo = prefix * 100;
722 let hi = lo + 99;
723 format!("{lo}..={hi}")
724 }
725 s => s.to_string(),
726 }
727}
728
729pub fn pick_body_type(b: &IrRequestBody) -> Option<IrTypeExpr> {
730 pick_body_content(b).map(|(_, t)| t)
731}
732
733pub fn pick_response_type(r: &IrResponse) -> Option<IrTypeExpr> {
734 pick_response_content(r).map(|(_, t)| t)
735}
736
737fn pick_body_content(b: &IrRequestBody) -> Option<(String, IrTypeExpr)> {
738 pick_media_type(&b.content, |media_type| {
739 media_type_base(media_type) == "application/json"
740 })
741 .or_else(|| pick_media_type(&b.content, is_json_media_type))
742 .or_else(|| {
743 pick_media_type(&b.content, |media_type| {
744 media_type_base(media_type) == "multipart/form-data"
745 })
746 })
747 .or_else(|| {
748 pick_media_type(&b.content, |media_type| {
749 media_type_base(media_type) == "application/x-www-form-urlencoded"
750 })
751 })
752 .or_else(|| pick_media_type(&b.content, is_xml_media_type))
753 .or_else(|| {
754 pick_media_type(&b.content, |media_type| {
755 media_type_base(media_type) == "text/plain"
756 })
757 })
758 .or_else(|| {
759 pick_media_type(&b.content, |media_type| {
760 media_type_base(media_type) == "application/octet-stream"
761 })
762 })
763 .or_else(|| pick_first_content(&b.content))
764}
765
766fn pick_response_content(r: &IrResponse) -> Option<(String, IrTypeExpr)> {
767 pick_media_type(&r.content, |media_type| {
768 media_type_base(media_type) == "application/json"
769 })
770 .or_else(|| pick_media_type(&r.content, is_json_media_type))
771 .or_else(|| {
772 pick_media_type(&r.content, |media_type| {
773 media_type_base(media_type) == "application/octet-stream"
774 })
775 })
776 .or_else(|| {
777 pick_media_type(&r.content, |media_type| {
778 media_type_base(media_type) == "text/plain"
779 })
780 })
781 .or_else(|| pick_media_type(&r.content, is_xml_media_type))
782 .or_else(|| pick_first_content(&r.content))
783}
784
785fn pick_media_type(
786 content: &indexmap::IndexMap<String, IrTypeExpr>,
787 predicate: impl Fn(&str) -> bool,
788) -> Option<(String, IrTypeExpr)> {
789 content
790 .iter()
791 .find(|(media_type, _)| predicate(media_type))
792 .map(|(media_type, t)| (media_type.clone(), t.clone()))
793}
794
795fn pick_first_content(
796 content: &indexmap::IndexMap<String, IrTypeExpr>,
797) -> Option<(String, IrTypeExpr)> {
798 content
799 .iter()
800 .next()
801 .map(|(media_type, t)| (media_type.clone(), t.clone()))
802}
803
804fn body_encoding(media_type: &str) -> BodyEncoding {
805 let base = media_type_base(media_type);
806 match base.as_str() {
807 "application/json" => BodyEncoding::Json,
808 "application/x-www-form-urlencoded" => BodyEncoding::FormUrlEncoded,
809 "multipart/form-data" => BodyEncoding::Multipart,
810 "application/xml" | "text/xml" => BodyEncoding::Xml,
811 "text/plain" => BodyEncoding::TextPlain,
812 "application/octet-stream" => BodyEncoding::OctetStream,
813 _ if is_json_media_type(media_type) => BodyEncoding::Json,
814 _ if is_xml_media_type(media_type) => BodyEncoding::Xml,
815 _ => BodyEncoding::Other(media_type.to_string()),
816 }
817}
818
819fn response_decoding(media_type: &str) -> ResponseDecoding {
820 let base = media_type_base(media_type);
821 match base.as_str() {
822 "application/json" => ResponseDecoding::Json,
823 "application/xml" | "text/xml" => ResponseDecoding::Xml,
824 "text/plain" => ResponseDecoding::TextPlain,
825 "application/octet-stream" => ResponseDecoding::OctetStream,
826 _ if is_json_media_type(media_type) => ResponseDecoding::Json,
827 _ if is_xml_media_type(media_type) => ResponseDecoding::Xml,
828 _ => ResponseDecoding::Other(media_type.to_string()),
829 }
830}
831
832fn media_type_base(media_type: &str) -> String {
833 media_type
834 .split(';')
835 .next()
836 .unwrap_or(media_type)
837 .trim()
838 .to_ascii_lowercase()
839}
840
841fn is_json_media_type(media_type: &str) -> bool {
842 let base = media_type_base(media_type);
843 base == "application/json" || base.ends_with("+json")
844}
845
846fn is_xml_media_type(media_type: &str) -> bool {
847 let base = media_type_base(media_type);
848 base == "application/xml" || base == "text/xml" || base.ends_with("+xml")
849}
850
851pub fn rust_field_name(wire_name: &str) -> String {
852 escape_rust_keyword(&wire_name.to_snake_case())
853}
854
855fn escape_rust_keyword(name: &str) -> String {
856 const KEYWORDS: &[&str] = &[
857 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
858 "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
859 "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
860 "true", "type", "union", "unsafe", "use", "where", "while", "yield",
861 ];
862 if KEYWORDS.contains(&name) {
863 format!("r#{name}")
864 } else {
865 name.to_string()
866 }
867}
868
869pub fn rust_string_literal(value: &str) -> String {
870 format!("{value:?}")
871}
872
873pub fn text_field_expr(base: &str, part: &MultipartPart) -> String {
874 let field_name = rust_field_name(&part.wire_name);
875 match part.value_encoding {
876 MultipartValueEncoding::Text => format!("{base}.{field_name}.to_string()"),
877 MultipartValueEncoding::Json => {
878 format!("serde_json::to_string(&{base}.{field_name}).map_err(Error::Deserialize)?")
879 }
880 MultipartValueEncoding::Unsupported => {
881 unreachable!("unsupported multipart parts are emitted before value expressions")
882 }
883 }
884}
885
886pub fn binary_field_expr(base: &str, part: &MultipartPart) -> String {
887 format!("{base}.{}.data.clone()", rust_field_name(&part.wire_name))
888}
889
890pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String {
891 match part.value_encoding {
892 MultipartValueEncoding::Text => format!("{value}.to_string()"),
893 MultipartValueEncoding::Json => {
894 format!("serde_json::to_string({value}).map_err(Error::Deserialize)?")
895 }
896 MultipartValueEncoding::Unsupported => {
897 unreachable!("unsupported multipart parts are emitted before value expressions")
898 }
899 }
900}
901
902pub fn optional_binary_field_expr(value: &str) -> String {
903 format!("{value}.data.clone()")
904}
905
906pub fn binary_filename_expr(base: &str, part: &MultipartPart) -> String {
907 format!(
908 "{base}.{}.filename_or_default({}).to_string()",
909 rust_field_name(&part.wire_name),
910 rust_string_literal(&part.default_filename)
911 )
912}
913
914pub fn optional_binary_filename_expr(value: &str, part: &MultipartPart) -> String {
915 format!(
916 "{value}.filename_or_default({}).to_string()",
917 rust_string_literal(&part.default_filename)
918 )
919}
920
921pub fn response_value_expr(tr: &TypedResponse, bytes_var: &str) -> String {
922 let owned_bytes_expr = bytes_var.strip_prefix('&').unwrap_or(bytes_var);
923 match tr.decoding {
924 ResponseDecoding::Json => {
925 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
926 }
927 ResponseDecoding::Xml => {
928 format!(
929 "serde_xml_rs::from_reader(std::io::Cursor::new({bytes_var})).map_err(Error::Xml)"
930 )
931 }
932 ResponseDecoding::TextPlain => {
933 format!("Ok::<String, Error>(String::from_utf8_lossy({bytes_var}).into_owned())")
934 }
935 ResponseDecoding::OctetStream => {
936 format!("Ok::<Vec<u8>, Error>({owned_bytes_expr}.to_vec())")
937 }
938 ResponseDecoding::Other(_) => {
939 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
940 }
941 }
942}
943
944pub fn response_value_expr_from_str(tr: &TypedResponse, body_var: &str) -> String {
945 match tr.decoding {
946 ResponseDecoding::Json => {
947 format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
948 }
949 ResponseDecoding::Xml => {
950 format!("serde_xml_rs::from_str({body_var}).map_err(Error::Xml)")
951 }
952 ResponseDecoding::TextPlain => format!("Ok::<String, Error>({body_var})"),
953 ResponseDecoding::OctetStream => {
954 format!("Ok::<Vec<u8>, Error>({body_var}.into_bytes())")
955 }
956 ResponseDecoding::Other(_) => {
957 format!("serde_json::from_str({body_var}).map_err(Error::Deserialize)")
958 }
959 }
960}
961
962pub fn response_needs_bytes(typed_responses: &[TypedResponse]) -> bool {
963 typed_responses
964 .iter()
965 .any(|tr| matches!(tr.decoding, ResponseDecoding::OctetStream))
966}
967
968pub fn render_to_string(var: &str, type_expr: &IrTypeExpr, _is_optional: bool) -> String {
969 match type_expr {
970 IrTypeExpr::Array(_) => {
971 format!("{var}.iter().map(ToString::to_string).collect::<Vec<_>>().join(\",\")")
972 }
973 _ => format!("{var}.to_string()"),
974 }
975}
976
977pub fn is_copy_type(ty: &str) -> bool {
978 matches!(
979 ty,
980 "bool" | "i32" | "i64" | "f32" | "f64" | "u8" | "u16" | "u32" | "u64"
981 ) || ty.starts_with("Option<")
982 && is_copy_type(
983 ty.strip_prefix("Option<")
984 .unwrap()
985 .strip_suffix('>')
986 .unwrap_or(""),
987 )
988}
989
990pub fn emit_result_init(
996 b: &mut CodeBlockBuilder,
997 response_type: &str,
998 typed_responses: &[TypedResponse],
999) {
1000 let mut fields = vec!["status_code".to_string()];
1001 let mut seen: HashSet<String> = HashSet::new();
1002 for tr in typed_responses {
1003 if seen.insert(tr.field_name.clone()) {
1004 fields.push(format!("{}: None", tr.field_name));
1005 }
1006 }
1007 b.add(
1008 &format!(
1009 "let mut result = {response_type} {{ {} }};\n",
1010 fields.join(", ")
1011 ),
1012 (),
1013 );
1014}
1015
1016pub fn emit_empty_result_init(b: &mut CodeBlockBuilder, response_type: &str) {
1017 b.add(
1018 &format!("let result = {response_type} {{ status_code }};\n"),
1019 (),
1020 );
1021}
1022
1023pub fn emit_response_match(
1025 b: &mut CodeBlockBuilder,
1026 typed_responses: &[TypedResponse],
1027 value_expr: &dyn Fn(&TypedResponse) -> String,
1028) {
1029 b.begin_control_flow("match status_code", ());
1030 let mut seen: HashSet<String> = HashSet::new();
1031 for tr in typed_responses {
1032 if !seen.insert(format!("{}-{}", tr.status, tr.field_name)) {
1033 continue;
1034 }
1035 let status_pattern = status_match_pattern(&tr.status);
1036 let value_expr = value_expr(tr);
1037 b.begin_control_flow(&format!("{status_pattern} =>"), ());
1038 b.add(
1039 &format!("result.{} = Some({value_expr}?);\n", tr.field_name),
1040 (),
1041 );
1042 b.end_control_flow();
1043 }
1044 if !typed_responses.iter().any(|tr| tr.status == "default") {
1045 b.add("_ => {}\n", ());
1046 }
1047 b.end_control_flow();
1048}
1049
1050pub fn emit_error_response_match(
1051 b: &mut CodeBlockBuilder,
1052 error_type: &str,
1053 error_responses: &[ErrorResponse],
1054 value_expr: &dyn Fn(&ErrorResponse) -> String,
1055) {
1056 b.begin_control_flow("if !(200..300).contains(&status_code)", ());
1057 b.begin_control_flow("match status_code", ());
1058
1059 let mut seen: HashSet<String> = HashSet::new();
1060 for er in error_responses
1061 .iter()
1062 .filter(|er| er.status.parse::<u16>().is_ok())
1063 {
1064 let key = format!("{}-{}", er.status, er.variant_name);
1065 if !seen.insert(key) {
1066 continue;
1067 }
1068 let pattern = status_match_pattern(&er.status);
1069 let body_expr = value_expr(er);
1070 b.begin_control_flow(&format!("{pattern} =>"), ());
1071 b.add(&format!("let body = {body_expr};\n"), ());
1072 b.add(&format!(
1073 "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1074 er.variant_name
1075 ), ());
1076 b.end_control_flow();
1077 }
1078
1079 for er in error_responses
1080 .iter()
1081 .filter(|er| er.status.ends_with("XX") && er.status.parse::<u16>().is_err())
1082 {
1083 let key = format!("{}-{}", er.status, er.variant_name);
1084 if !seen.insert(key) {
1085 continue;
1086 }
1087 let pattern = status_match_pattern(&er.status);
1088 let body_expr = value_expr(er);
1089 b.begin_control_flow(&format!("{pattern} =>"), ());
1090 b.add(&format!("let body = {body_expr};\n"), ());
1091 b.add(&format!(
1092 "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1093 er.variant_name
1094 ), ());
1095 b.end_control_flow();
1096 }
1097
1098 if let Some(er) = error_responses
1099 .iter()
1100 .find(|er| er.status.eq_ignore_ascii_case("default"))
1101 {
1102 let body_expr = value_expr(er);
1103 b.begin_control_flow("_ =>", ());
1104 b.add(&format!("let body = {body_expr};\n"), ());
1105 b.add(&format!(
1106 "return Err({error_type}::{}(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n",
1107 er.variant_name
1108 ), ());
1109 b.end_control_flow();
1110 } else {
1111 b.begin_control_flow("_ =>", ());
1112 b.add(
1113 "let body = Ok::<Vec<u8>, Error>(body_bytes.to_vec());\n",
1114 (),
1115 );
1116 b.add(&format!(
1117 "return Err({error_type}::Unexpected(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body)));\n"
1118 ), ());
1119 b.end_control_flow();
1120 }
1121
1122 b.end_control_flow();
1123 b.end_control_flow();
1124}
1125
1126pub fn error_response_value_expr(er: &ErrorResponse, bytes_var: &str) -> String {
1127 let owned_bytes_expr = bytes_var.strip_prefix('&').unwrap_or(bytes_var);
1128 match er.decoding {
1129 Some(ResponseDecoding::Json) => {
1130 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1131 }
1132 Some(ResponseDecoding::Xml) => {
1133 format!(
1134 "serde_xml_rs::from_reader(std::io::Cursor::new({bytes_var})).map_err(Error::Xml)"
1135 )
1136 }
1137 Some(ResponseDecoding::TextPlain) => {
1138 format!("Ok::<String, Error>(String::from_utf8_lossy({bytes_var}).into_owned())")
1139 }
1140 Some(ResponseDecoding::OctetStream) => {
1141 format!("Ok::<Vec<u8>, Error>({owned_bytes_expr}.to_vec())")
1142 }
1143 Some(ResponseDecoding::Other(_)) => {
1144 format!("serde_json::from_slice({bytes_var}).map_err(Error::Deserialize)")
1145 }
1146 None => "Ok::<(), Error>(())".to_string(),
1147 }
1148}