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