1use crate::{GeneratorError, Result, analysis::SchemaAnalysis, streaming::StreamingConfig};
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7fn parse_rust_type(rust_type: &str) -> Result<TokenStream> {
16 let parsed: syn::Type = syn::parse_str(rust_type).map_err(|e| {
17 GeneratorError::CodeGenError(format!(
18 "TypeMapper produced un-parseable type `{rust_type}`: {e}"
19 ))
20 })?;
21 Ok(quote! { #parsed })
22}
23
24fn format_constraints_doc(c: &crate::analysis::PropertyConstraints) -> String {
33 let mut parts: Vec<String> = Vec::new();
34
35 if let Some(v) = c.minimum {
36 parts.push(format!("minimum={}", strip_trailing_zero(v)));
37 }
38 if let Some(v) = c.maximum {
39 parts.push(format!("maximum={}", strip_trailing_zero(v)));
40 }
41 if let Some(v) = c.exclusive_minimum {
42 parts.push(format!("exclusiveMinimum={}", strip_trailing_zero(v)));
43 }
44 if let Some(v) = c.exclusive_maximum {
45 parts.push(format!("exclusiveMaximum={}", strip_trailing_zero(v)));
46 }
47 if let Some(v) = c.multiple_of {
48 parts.push(format!("multipleOf={}", strip_trailing_zero(v)));
49 }
50 if let Some(v) = c.min_length {
51 parts.push(format!("minLength={v}"));
52 }
53 if let Some(v) = c.max_length {
54 parts.push(format!("maxLength={v}"));
55 }
56 if let Some(v) = c.min_items {
57 parts.push(format!("minItems={v}"));
58 }
59 if let Some(v) = c.max_items {
60 parts.push(format!("maxItems={v}"));
61 }
62 if c.unique_items == Some(true) {
63 parts.push("uniqueItems=true".to_string());
64 }
65 if let Some(p) = &c.pattern {
66 let safe = p.replace("///", "/\u{200B}//").replace("*/", "*\u{200B}/");
71 parts.push(format!("pattern=`{safe}`"));
72 }
73
74 format!("Constraint: {}", parts.join(", "))
75}
76
77fn strip_trailing_zero(v: f64) -> String {
80 if v.fract() == 0.0 && v.is_finite() {
81 format!("{}", v as i64)
82 } else {
83 format!("{v}")
84 }
85}
86
87#[derive(Clone)]
89pub(crate) struct DiscriminatedVariantInfo {
90 pub(crate) discriminator_field: String,
92 pub(crate) discriminator_value: String,
94 pub(crate) is_parent_untagged: bool,
96}
97
98pub(crate) struct EmittedObjectProperty<'a> {
102 pub(crate) wire_name: &'a str,
103 pub(crate) property: &'a crate::analysis::PropertyInfo,
104 pub(crate) ident: syn::Ident,
105 pub(crate) is_required: bool,
106 pub(crate) field_type: TokenStream,
107}
108
109struct TypeGenerationIndex {
113 request_body_roots: std::collections::HashSet<String>,
114 reserved_type_names: std::collections::HashSet<String>,
115}
116
117struct TypeGenerationContext<'a> {
118 discriminated_variants: &'a BTreeMap<String, DiscriminatedVariantInfo>,
119 index: &'a TypeGenerationIndex,
120}
121
122#[derive(Debug, Clone)]
123pub struct GeneratorConfig {
124 pub spec_path: PathBuf,
126 pub output_dir: PathBuf,
128 pub module_name: String,
135 pub enable_sse_client: bool,
137 pub enable_async_client: bool,
139 pub enable_specta: bool,
141 pub type_mappings: BTreeMap<String, String>,
143 pub streaming_config: Option<StreamingConfig>,
145 pub nullable_field_overrides: BTreeMap<String, bool>,
148 pub extensible_enum_overrides: BTreeMap<String, bool>,
155 pub schema_extensions: Vec<PathBuf>,
158 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
160 pub retry_config: Option<crate::http_config::RetryConfig>,
162 pub tracing_enabled: bool,
164 pub auth_config: Option<crate::http_config::AuthConfig>,
166 pub enable_registry: bool,
168 pub registry_only: bool,
170 pub types: crate::type_mapping::TypeMappingConfig,
174 pub builders: crate::config::BuildersSection,
176 pub server: Option<crate::config::ServerSection>,
179 pub client: Option<crate::config::ClientSection>,
182}
183
184impl Default for GeneratorConfig {
185 fn default() -> Self {
186 Self {
187 spec_path: "openapi.json".into(),
188 output_dir: "src/gen".into(),
189 module_name: "api_types".to_string(),
190 enable_sse_client: true,
191 enable_async_client: true,
192 enable_specta: false,
193 type_mappings: default_type_mappings(),
194 streaming_config: None,
195 nullable_field_overrides: BTreeMap::new(),
196 extensible_enum_overrides: BTreeMap::new(),
197 schema_extensions: Vec::new(),
198 http_client_config: None,
199 retry_config: None,
200 tracing_enabled: true,
201 auth_config: None,
202 enable_registry: false,
203 registry_only: false,
204 types: crate::type_mapping::TypeMappingConfig::default(),
205 builders: crate::config::BuildersSection::default(),
206 server: None,
207 client: None,
208 }
209 }
210}
211
212pub fn default_type_mappings() -> BTreeMap<String, String> {
213 let mut mappings = BTreeMap::new();
214 mappings.insert("integer".to_string(), "i64".to_string());
215 mappings.insert("number".to_string(), "f64".to_string());
216 mappings.insert("string".to_string(), "String".to_string());
217 mappings.insert("boolean".to_string(), "bool".to_string());
218 mappings
219}
220
221#[derive(Debug, Clone)]
223pub struct GeneratedFile {
224 pub path: PathBuf,
226 pub content: String,
228}
229
230#[derive(Debug, Clone)]
232pub struct GenerationResult {
233 pub files: Vec<GeneratedFile>,
235 pub mod_file: GeneratedFile,
237 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
241 pub pruned_schemas: usize,
243}
244
245#[derive(Debug)]
246struct OperationScopes {
247 client_ids: Option<std::collections::BTreeSet<String>>,
249 server_ids: std::collections::BTreeSet<String>,
250 streaming_ids: std::collections::BTreeSet<String>,
251 prune_models: bool,
252 extra_schema_roots: Vec<String>,
253}
254
255pub struct CodeGenerator {
256 config: GeneratorConfig,
257 source_provenance: Option<String>,
258}
259
260impl CodeGenerator {
261 pub fn new(config: GeneratorConfig) -> Self {
262 Self {
263 config,
264 source_provenance: None,
265 }
266 }
267
268 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
270 self.source_provenance = Some(source.into());
271 self
272 }
273
274 pub fn config(&self) -> &GeneratorConfig {
276 &self.config
277 }
278
279 pub(crate) fn provenance_attribute(&self) -> TokenStream {
280 self.source_provenance
281 .as_ref()
282 .map(|source| {
283 let provenance = format!(
284 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
285 env!("CARGO_PKG_VERSION")
286 );
287 quote! { #![doc = #provenance] }
288 })
289 .unwrap_or_default()
290 }
291
292 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
294 let scopes = self.resolve_operation_scopes(analysis)?;
297 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
298 let mut files = Vec::new();
299
300 if !self.config.registry_only {
301 let types_content = self.generate_types(analysis)?;
303 files.push(GeneratedFile {
304 path: "types.rs".into(),
305 content: types_content,
306 });
307
308 if self.config.enable_sse_client
310 && let Some(ref streaming_config) = self.config.streaming_config
311 {
312 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
313 return Err(GeneratorError::ValidationError(
314 "streaming generate_client=true requires event_parser_helpers=true"
315 .to_string(),
316 ));
317 }
318 let streaming_content =
319 self.generate_streaming_client(streaming_config, analysis)?;
320 files.push(GeneratedFile {
321 path: "streaming.rs".into(),
322 content: streaming_content,
323 });
324 }
325
326 if self.config.enable_async_client {
328 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
329 let http_content =
330 self.generate_http_client_for_operations(analysis, &operations)?;
331 files.push(GeneratedFile {
332 path: "client.rs".into(),
333 content: http_content,
334 });
335 }
336 }
337
338 if self.config.enable_registry || self.config.registry_only {
340 let registry_content = self.generate_registry(analysis)?;
341 files.push(GeneratedFile {
342 path: "registry.rs".into(),
343 content: registry_content,
344 });
345 }
346
347 if !self.config.registry_only
351 && let Some(server) = self
352 .config
353 .server
354 .as_ref()
355 .filter(|server| !server.operations.is_empty())
356 {
357 let server_files =
358 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
359 .with_source_provenance(self.source_provenance.as_deref())
360 .generate()
361 .map_err(|error| {
362 GeneratorError::CodeGenError(format!(
363 "server code generation failed: {error}"
364 ))
365 })?;
366 files.extend(server_files);
367 }
368
369 let mod_content = self.generate_mod_file(&files)?;
371 let mod_file = GeneratedFile {
372 path: "mod.rs".into(),
373 content: mod_content,
374 };
375
376 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
377 files.iter().map(|file| file.content.as_str()),
378 self.config.enable_specta,
379 );
380
381 Ok(GenerationResult {
382 files,
383 mod_file,
384 required_deps,
385 pruned_schemas,
386 })
387 }
388
389 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
391 self.generate_types(analysis)
392 }
393
394 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
396 let provenance_attribute = self.provenance_attribute();
397 let mut type_definitions = TokenStream::new();
398
399 let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
402 BTreeMap::new();
403
404 let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
406 sorted_schemas.sort_by_key(|(name, _)| name.as_str());
407
408 for (_parent_name, schema) in sorted_schemas {
409 if let crate::analysis::SchemaType::DiscriminatedUnion {
410 variants,
411 discriminator_field,
412 } = &schema.schema_type
413 {
414 let is_parent_untagged =
416 self.should_use_untagged_discriminated_union(schema, analysis);
417
418 for variant in variants {
419 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
422 if let crate::analysis::SchemaType::Object { properties, .. } =
423 &variant_schema.schema_type
424 {
425 if properties.contains_key(discriminator_field) {
426 discriminated_variant_info.insert(
427 variant.type_name.clone(),
428 DiscriminatedVariantInfo {
429 discriminator_field: discriminator_field.clone(),
430 discriminator_value: variant.discriminator_value.clone(),
431 is_parent_untagged,
432 },
433 );
434 }
435 }
436 }
437 }
438 }
439 }
440
441 let type_index = self.type_generation_index(analysis);
442 let type_context = TypeGenerationContext {
443 discriminated_variants: &discriminated_variant_info,
444 index: &type_index,
445 };
446
447 let generation_order = analysis.dependencies.topological_sort()?;
449
450 let mut emitted_rust_names: std::collections::HashSet<String> =
458 std::collections::HashSet::new();
459 let mut processed = std::collections::HashSet::new();
460
461 for schema_name in generation_order {
463 if let Some(schema) = analysis.schemas.get(&schema_name) {
464 let rust_name = self.to_rust_type_name(&schema.name);
465 if !emitted_rust_names.insert(rust_name) {
466 processed.insert(schema_name);
467 continue;
468 }
469 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
470 if !type_def.is_empty() {
471 type_definitions.extend(type_def);
472 }
473 processed.insert(schema_name);
474 }
475 }
476
477 let mut remaining_schemas: Vec<_> = analysis
479 .schemas
480 .iter()
481 .filter(|(name, _)| !processed.contains(*name))
482 .collect();
483 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
484
485 for (_schema_name, schema) in remaining_schemas {
486 let rust_name = self.to_rust_type_name(&schema.name);
487 if !emitted_rust_names.insert(rust_name) {
488 continue;
489 }
490 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
491 if !type_def.is_empty() {
492 type_definitions.extend(type_def);
493 }
494 }
495
496 let base64_helper = if analysis
501 .used_type_features
502 .contains(crate::type_mapping::TypeFeature::Base64)
503 {
504 let engine = match self.config.types.byte {
505 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
506 quote::format_ident!("URL_SAFE_NO_PAD")
507 }
508 _ => quote::format_ident!("STANDARD"),
509 };
510 quote! {
511 mod base64_serde {
516 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
517 use serde::{Deserialize, Deserializer, Serializer};
518
519 pub fn serialize<S: Serializer>(
520 bytes: &Vec<u8>,
521 ser: S,
522 ) -> Result<S::Ok, S::Error> {
523 ser.serialize_str(&ENGINE.encode(bytes))
524 }
525
526 pub fn deserialize<'de, D: Deserializer<'de>>(
527 de: D,
528 ) -> Result<Vec<u8>, D::Error> {
529 let s = String::deserialize(de)?;
530 ENGINE
531 .decode(s.as_bytes())
532 .map_err(serde::de::Error::custom)
533 }
534
535 pub mod option {
541 use super::*;
542 use serde::{Deserialize, Deserializer, Serializer};
543
544 pub fn serialize<S: Serializer>(
545 opt: &Option<Vec<u8>>,
546 ser: S,
547 ) -> Result<S::Ok, S::Error> {
548 match opt {
549 Some(bytes) => super::serialize(bytes, ser),
550 None => ser.serialize_none(),
551 }
552 }
553
554 pub fn deserialize<'de, D: Deserializer<'de>>(
555 de: D,
556 ) -> Result<Option<Vec<u8>>, D::Error> {
557 let opt = Option::<String>::deserialize(de)?;
558 opt.map(|s| {
559 ENGINE
560 .decode(s.as_bytes())
561 .map_err(serde::de::Error::custom)
562 })
563 .transpose()
564 }
565 }
566 }
567 }
568 } else {
569 TokenStream::new()
570 };
571
572 let time_date_helper = if analysis
579 .used_type_features
580 .contains(crate::type_mapping::TypeFeature::TimeDate)
581 {
582 quote! {
583 time::serde::format_description!(
584 time_date_format,
585 Date,
586 "[year]-[month]-[day]"
587 );
588 }
589 } else {
590 TokenStream::new()
591 };
592
593 let time_time_helper = if analysis
598 .used_type_features
599 .contains(crate::type_mapping::TypeFeature::TimeTime)
600 {
601 quote! {
602 time::serde::format_description!(
603 version = 2,
604 time_time_format,
605 Time,
606 "[hour]:[minute]:[second][optional [.[subsecond]]]"
607 );
608 }
609 } else {
610 TokenStream::new()
611 };
612
613 let generated = quote! {
615 #provenance_attribute
621
622 #![allow(clippy::large_enum_variant)]
623 #![allow(clippy::format_in_format_args)]
624 #![allow(clippy::let_unit_value)]
625 #![allow(unreachable_patterns)]
626
627 use serde::{Deserialize, Serialize};
628
629 #base64_helper
630
631 #time_date_helper
632
633 #time_time_helper
634
635 #type_definitions
636 };
637
638 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
640 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
641 })?;
642
643 let formatted = prettyplease::unparse(&syntax_tree);
644
645 Ok(formatted)
646 }
647
648 fn generate_streaming_client(
650 &self,
651 streaming_config: &StreamingConfig,
652 analysis: &SchemaAnalysis,
653 ) -> Result<String> {
654 let mut client_code = TokenStream::new();
655 let provenance_attribute = self.provenance_attribute();
656
657 let imports = quote! {
659 #provenance_attribute
664 #![allow(clippy::format_in_format_args)]
665 #![allow(clippy::let_unit_value)]
666 #![allow(unused_mut)]
667
668 use super::types::*;
669 use async_trait::async_trait;
670 use futures_util::{Stream, StreamExt};
671 use std::pin::Pin;
672 use std::time::Duration;
673 use reqwest::header::{HeaderMap, HeaderValue};
674 use tracing::{debug, error, info, warn, instrument};
675 };
676 client_code.extend(imports);
677
678 if streaming_config.generate_client {
680 let error_types = self.generate_streaming_error_types()?;
681 client_code.extend(error_types);
682 }
683
684 for endpoint in &streaming_config.endpoints {
686 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
687 client_code.extend(trait_code);
688 }
689
690 if streaming_config.generate_client {
692 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
693 client_code.extend(client_impl);
694 }
695
696 if streaming_config.event_parser_helpers {
698 let parser_code = self.generate_sse_parser_utilities(streaming_config)?;
699 client_code.extend(parser_code);
700 }
701
702 if let Some(reconnect_config) = &streaming_config.reconnection_config {
704 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
705 client_code.extend(reconnect_code);
706 }
707
708 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
709 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
710 })?;
711
712 Ok(prettyplease::unparse(&syntax_tree))
713 }
714
715 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
721 let client_ids = self.resolve_client_operation_ids(analysis)?;
722 let operations = self.client_operations(analysis, client_ids.as_ref());
723 self.generate_http_client_for_operations(analysis, &operations)
724 }
725
726 fn generate_http_client_for_operations(
727 &self,
728 analysis: &SchemaAnalysis,
729 operations: &[&crate::analysis::OperationInfo],
730 ) -> Result<String> {
731 let provenance_attribute = self.provenance_attribute();
732 let error_types = self.generate_http_error_types();
733 let client_struct = self.generate_http_client_struct();
734 let operation_methods = self.generate_operation_methods_for(analysis, operations);
735
736 let generated = quote! {
737 #provenance_attribute
742 #![allow(clippy::format_in_format_args)]
743 #![allow(clippy::let_unit_value)]
744
745 use super::types::*;
746
747 #error_types
748
749 #client_struct
750
751 #operation_methods
752 };
753
754 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
755 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
756 })?;
757
758 Ok(prettyplease::unparse(&syntax_tree))
759 }
760
761 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
762 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
763 self.resolve_client_operation_ids(analysis)?
764 } else {
765 None
766 };
767
768 let server_ids = match &self.config.server {
769 Some(server) if !server.operations.is_empty() => {
770 crate::server::resolve_operation_selectors(&server.operations, analysis)
771 .map_err(|error| {
772 GeneratorError::ValidationError(format!(
773 "Invalid [server].operations: {error}"
774 ))
775 })?
776 .operations
777 .into_iter()
778 .map(|operation| operation.operation_id)
779 .collect()
780 }
781 _ => Default::default(),
782 };
783
784 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
785 Default::default()
786 } else if let Some(streaming) = &self.config.streaming_config {
787 let mut ids = std::collections::BTreeSet::new();
788 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
789 let resolution =
790 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
791 |error| {
792 GeneratorError::ValidationError(format!(
793 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
794 ))
795 },
796 )?;
797 ids.extend(
798 resolution
799 .operations
800 .into_iter()
801 .map(|operation| operation.operation_id),
802 );
803 }
804 ids
805 } else {
806 Default::default()
807 };
808
809 let client_prunes = self.config.enable_async_client
810 && !self.config.registry_only
811 && self
812 .config
813 .client
814 .as_ref()
815 .is_some_and(|client| client.prune_models);
816 let server_prunes = self
817 .config
818 .server
819 .as_ref()
820 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
821 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
822 Vec::new()
823 } else {
824 self.config
825 .streaming_config
826 .as_ref()
827 .map(|streaming| {
828 streaming
829 .endpoints
830 .iter()
831 .map(|endpoint| endpoint.event_union_type.clone())
832 .collect()
833 })
834 .unwrap_or_default()
835 };
836
837 Ok(OperationScopes {
838 client_ids,
839 server_ids,
840 streaming_ids,
841 prune_models: client_prunes || server_prunes,
842 extra_schema_roots,
843 })
844 }
845
846 fn resolve_client_operation_ids(
847 &self,
848 analysis: &SchemaAnalysis,
849 ) -> Result<Option<std::collections::BTreeSet<String>>> {
850 match &self.config.client {
851 Some(client) if !client.operations.is_empty() => {
852 let resolution =
853 crate::server::resolve_operation_selectors(&client.operations, analysis)
854 .map_err(|error| {
855 GeneratorError::ValidationError(format!(
856 "Invalid [client].operations: {error}"
857 ))
858 })?;
859 Ok(Some(
860 resolution
861 .operations
862 .into_iter()
863 .map(|operation| operation.operation_id)
864 .collect(),
865 ))
866 }
867 _ => Ok(None),
868 }
869 }
870
871 fn client_operations<'a>(
872 &self,
873 analysis: &'a SchemaAnalysis,
874 selected: Option<&std::collections::BTreeSet<String>>,
875 ) -> Vec<&'a crate::analysis::OperationInfo> {
876 analysis
877 .operations
878 .iter()
879 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
880 .map(|(_, operation)| operation)
881 .collect()
882 }
883
884 fn prune_models_to_scopes(
885 &self,
886 analysis: &mut SchemaAnalysis,
887 scopes: &OperationScopes,
888 ) -> usize {
889 if !scopes.prune_models {
890 return 0;
891 }
892
893 let mut consumer_ids = scopes.server_ids.clone();
894 if self.config.enable_async_client && !self.config.registry_only {
895 match &scopes.client_ids {
896 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
897 None => consumer_ids.extend(analysis.operations.keys().cloned()),
898 }
899 }
900 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
901
902 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
903 .iter()
904 .filter_map(|operation_id| analysis.operations.get(operation_id))
905 .collect();
906 let keep = crate::server::codegen::reachable_schemas_with_roots(
907 analysis,
908 &operations,
909 &scopes.extra_schema_roots,
910 );
911 let before = analysis.schemas.len();
912 analysis.schemas.retain(|name, _| keep.contains(name));
913 before - analysis.schemas.len()
914 }
915
916 fn generate_http_error_types(&self) -> TokenStream {
918 quote! {
919 use thiserror::Error;
920
921 #[derive(Error, Debug)]
929 pub enum HttpError {
930 #[error("Network error: {0}")]
932 Network(#[from] reqwest::Error),
933
934 #[error("Middleware error: {0}")]
936 Middleware(#[from] reqwest_middleware::Error),
937
938 #[error("Failed to serialize request: {0}")]
940 Serialization(String),
941
942 #[error("Authentication error: {0}")]
944 Auth(String),
945
946 #[error("Request timeout")]
948 Timeout,
949
950 #[error("Configuration error: {0}")]
952 Config(String),
953
954 #[error("{0}")]
956 Other(String),
957 }
958
959 impl HttpError {
960 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
962 Self::Serialization(error.to_string())
963 }
964
965 pub fn is_retryable(&self) -> bool {
967 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
968 }
969 }
970
971 #[derive(Debug, Clone)]
983 pub struct ApiError<E> {
984 pub status: u16,
985 pub headers: reqwest::header::HeaderMap,
986 pub body: String,
987 pub typed: Option<E>,
988 pub parse_error: Option<String>,
989 }
990
991 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
992 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
993
994 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
995 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
996 return std::borrow::Cow::Borrowed(body);
997 };
998
999 let mut displayed =
1000 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1001 displayed.push_str(&body[..end]);
1002 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1003 std::borrow::Cow::Owned(displayed)
1004 }
1005
1006 impl<E> ApiError<E> {
1007 pub fn is_client_error(&self) -> bool {
1008 (400..500).contains(&self.status)
1009 }
1010
1011 pub fn is_server_error(&self) -> bool {
1012 (500..600).contains(&self.status)
1013 }
1014
1015 pub fn is_retryable(&self) -> bool {
1018 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1019 }
1020 }
1021
1022 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1023 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024 write!(
1025 f,
1026 "API error {}: {}",
1027 self.status,
1028 display_api_error_body(&self.body)
1029 )?;
1030
1031 if let Some(typed) = &self.typed {
1032 write!(f, "; typed: {typed:?}")?;
1033 }
1034
1035 if let Some(parse_error) = &self.parse_error {
1036 write!(f, "; parse error: {parse_error}")?;
1037 }
1038
1039 Ok(())
1040 }
1041 }
1042
1043 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1044
1045 #[derive(Debug, Error)]
1053 pub enum ApiOpError<E: std::fmt::Debug> {
1054 #[error(transparent)]
1055 Transport(#[from] HttpError),
1056
1057 #[error(transparent)]
1058 Api(ApiError<E>),
1059 }
1060
1061 impl<E: std::fmt::Debug> ApiOpError<E> {
1062 pub fn api(&self) -> Option<&ApiError<E>> {
1064 match self {
1065 Self::Api(e) => Some(e),
1066 Self::Transport(_) => None,
1067 }
1068 }
1069
1070 pub fn is_api_error(&self) -> bool {
1073 matches!(self, Self::Api(_))
1074 }
1075 }
1076
1077 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1080 fn from(e: reqwest::Error) -> Self {
1081 Self::Transport(HttpError::Network(e))
1082 }
1083 }
1084
1085 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1086 fn from(e: reqwest_middleware::Error) -> Self {
1087 Self::Transport(HttpError::Middleware(e))
1088 }
1089 }
1090
1091 pub type HttpResult<T> = Result<T, HttpError>;
1095 }
1096 }
1097
1098 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1100 let mut module_names = std::collections::BTreeSet::new();
1101
1102 for file in files {
1103 let module_name = if file.path.components().count() > 1 {
1104 file.path.iter().next().and_then(|part| part.to_str())
1105 } else {
1106 file.path.file_stem().and_then(|stem| stem.to_str())
1107 };
1108 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1109 module_names.insert(module_name.to_string());
1110 }
1111 }
1112 let module_declarations = module_names
1113 .iter()
1114 .map(|name| format!("pub mod {name};"))
1115 .collect::<Vec<_>>();
1116 let pub_uses = module_names
1117 .iter()
1118 .map(|name| format!("pub use {name}::*;"))
1119 .collect::<Vec<_>>();
1120
1121 let mount_hint = format!(
1128 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1129 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1130 name = self.config.module_name,
1131 );
1132 let source_hint = self
1133 .source_provenance
1134 .as_ref()
1135 .map(|source| {
1136 format!(
1137 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1138 env!("CARGO_PKG_VERSION")
1139 )
1140 })
1141 .unwrap_or_default();
1142
1143 let content = format!(
1144 r#"//! Generated API modules
1145//!
1146//! This module exports all generated API types and clients.
1147//! Do not edit manually - regenerate using the appropriate script.
1148//!
1149{source_hint}
1150{mount_hint}
1151#![allow(unused_imports)]
1152
1153{decls}
1154
1155{uses}
1156"#,
1157 mount_hint = mount_hint,
1158 source_hint = source_hint,
1159 decls = module_declarations.join("\n"),
1160 uses = pub_uses.join("\n"),
1161 );
1162
1163 Ok(content)
1164 }
1165
1166 pub fn output_artifacts(
1168 &self,
1169 result: &GenerationResult,
1170 ) -> std::collections::BTreeMap<PathBuf, String> {
1171 let mut artifacts = std::collections::BTreeMap::new();
1172 for file in &result.files {
1173 artifacts.insert(file.path.clone(), file.content.clone());
1174 }
1175 artifacts.insert(
1176 result.mod_file.path.clone(),
1177 result.mod_file.content.clone(),
1178 );
1179 if let Some(mut fragment) =
1180 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1181 {
1182 if let Some(source) = &self.source_provenance {
1183 let header = format!(
1184 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1185 env!("CARGO_PKG_VERSION")
1186 );
1187 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1188 }
1189 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1190 }
1191 artifacts
1192 }
1193
1194 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1197 use std::fs;
1198
1199 fs::create_dir_all(&self.config.output_dir)?;
1201
1202 let artifacts = self.output_artifacts(result);
1203 for (relative, content) in &artifacts {
1204 let file_path = self.config.output_dir.join(relative);
1205 if let Some(parent) = file_path.parent() {
1206 fs::create_dir_all(parent)?;
1207 }
1208 fs::write(&file_path, content)?;
1209 }
1210
1211 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1212 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1213 {
1214 fs::remove_file(&deps_path)?;
1215 }
1216
1217 Ok(())
1218 }
1219
1220 fn generate_type_definition(
1221 &self,
1222 schema: &crate::analysis::AnalyzedSchema,
1223 analysis: &crate::analysis::SchemaAnalysis,
1224 type_context: &TypeGenerationContext<'_>,
1225 ) -> Result<TokenStream> {
1226 use crate::analysis::SchemaType;
1227
1228 match &schema.schema_type {
1229 SchemaType::Primitive { rust_type, .. } => {
1230 self.generate_type_alias(schema, rust_type)
1232 }
1233 SchemaType::StringEnum { values } => {
1234 let ext = analysis.enum_extensions.get(&schema.name);
1235 let rust_name = self.to_rust_type_name(&schema.name);
1242 let force_extensible = self
1243 .config
1244 .extensible_enum_overrides
1245 .get(&schema.name)
1246 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1247 .copied()
1248 .unwrap_or(false);
1249 if force_extensible {
1250 self.generate_extensible_enum(schema, values, ext)
1251 } else {
1252 self.generate_string_enum(schema, values, ext)
1253 }
1254 }
1255 SchemaType::ExtensibleEnum { known_values } => {
1256 let ext = analysis.enum_extensions.get(&schema.name);
1257 self.generate_extensible_enum(schema, known_values, ext)
1258 }
1259 SchemaType::Object {
1260 properties,
1261 required,
1262 additional_properties,
1263 } => self.generate_struct(
1264 schema,
1265 properties,
1266 required,
1267 additional_properties,
1268 analysis,
1269 type_context,
1270 ),
1271 SchemaType::DiscriminatedUnion {
1272 discriminator_field,
1273 variants,
1274 } => {
1275 if self.should_use_untagged_discriminated_union(schema, analysis) {
1277 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1279 .iter()
1280 .map(|v| crate::analysis::SchemaRef {
1281 target: v.type_name.clone(),
1282 nullable: false,
1283 })
1284 .collect();
1285 self.generate_union_enum(schema, &schema_refs, analysis)
1286 } else {
1287 self.generate_discriminated_enum(
1288 schema,
1289 discriminator_field,
1290 variants,
1291 analysis,
1292 )
1293 }
1294 }
1295 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1296 SchemaType::Reference { target } => {
1297 if schema.name != *target {
1300 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1302 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1303
1304 let doc_comment = if let Some(desc) = &schema.description {
1305 quote! { #[doc = #desc] }
1306 } else {
1307 TokenStream::new()
1308 };
1309
1310 Ok(quote! {
1311 #doc_comment
1312 pub type #alias_name = #target_type;
1313 })
1314 } else {
1315 Ok(TokenStream::new())
1317 }
1318 }
1319 SchemaType::Array { item_type } => {
1320 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1328
1329 if let SchemaType::Reference { target } = item_type.as_ref() {
1331 if let Some(info) = type_context.discriminated_variants.get(target) {
1332 if !info.is_parent_untagged {
1333 let wrapper_name =
1335 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1336 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1337 let disc_field = &info.discriminator_field;
1338 let disc_value = &info.discriminator_value;
1339
1340 let doc_comment = if let Some(desc) = &schema.description {
1341 quote! { #[doc = #desc] }
1342 } else {
1343 TokenStream::new()
1344 };
1345
1346 return Ok(quote! {
1347 #[derive(Debug, Clone, Deserialize, Serialize)]
1351 #[serde(tag = #disc_field)]
1352 pub enum #wrapper_name {
1353 #[serde(rename = #disc_value)]
1354 #variant_type(#variant_type),
1355 }
1356 #doc_comment
1357 pub type #array_name = Vec<#wrapper_name>;
1358 });
1359 }
1360 }
1361 }
1362
1363 let inner_type = self.generate_array_item_type(item_type, analysis);
1364
1365 let doc_comment = if let Some(desc) = &schema.description {
1366 quote! { #[doc = #desc] }
1367 } else {
1368 TokenStream::new()
1369 };
1370
1371 Ok(quote! {
1372 #doc_comment
1373 pub type #array_name = Vec<#inner_type>;
1374 })
1375 }
1376 SchemaType::Composition { schemas } => {
1377 self.generate_composition_struct(schema, schemas)
1378 }
1379 }
1380 }
1381
1382 fn generate_type_alias(
1383 &self,
1384 schema: &crate::analysis::AnalyzedSchema,
1385 rust_type: &str,
1386 ) -> Result<TokenStream> {
1387 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1388 let base_type = parse_rust_type(rust_type)?;
1392
1393 let doc_comment = if let Some(desc) = &schema.description {
1394 let sanitized_desc = self.sanitize_doc_comment(desc);
1395 quote! { #[doc = #sanitized_desc] }
1396 } else {
1397 TokenStream::new()
1398 };
1399
1400 Ok(quote! {
1401 #doc_comment
1402 pub type #type_name = #base_type;
1403 })
1404 }
1405
1406 fn generate_extensible_enum(
1407 &self,
1408 schema: &crate::analysis::AnalyzedSchema,
1409 known_values: &[String],
1410 ext: Option<&crate::analysis::EnumExtensions>,
1411 ) -> Result<TokenStream> {
1412 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1413
1414 let doc_comment = if let Some(desc) = &schema.description {
1415 quote! { #[doc = #desc] }
1416 } else {
1417 TokenStream::new()
1418 };
1419
1420 let varnames_override: Option<&Vec<String>> = ext
1424 .filter(|_| self.config.types.x_enum_varnames_enabled())
1425 .map(|e| &e.varnames)
1426 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1427 let descriptions_override: Option<&Vec<String>> = ext
1428 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1429 .map(|e| &e.descriptions)
1430 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1431
1432 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1433 let name = match varnames_override {
1434 Some(v) => v[index].clone(),
1435 None => self.to_rust_enum_variant(value),
1436 };
1437 format_ident!("{}", name)
1438 };
1439
1440 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1445 let variant_ident = variant_ident_for(i, value);
1446 let doc = descriptions_override
1447 .map(|d| {
1448 let s = self.sanitize_doc_comment(&d[i]);
1449 quote! { #[doc = #s] }
1450 })
1451 .unwrap_or_default();
1452 quote! {
1453 #doc
1454 #variant_ident,
1455 }
1456 });
1457
1458 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1459 let variant_ident = variant_ident_for(i, value);
1460 quote! {
1461 #value => Ok(#enum_name::#variant_ident),
1462 }
1463 });
1464
1465 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1466 let variant_ident = variant_ident_for(i, value);
1467 quote! {
1468 #enum_name::#variant_ident => #value,
1469 }
1470 });
1471
1472 let derives = if self.config.enable_specta {
1473 quote! {
1474 #[derive(Debug, Clone, PartialEq, Eq)]
1475 #[cfg_attr(feature = "specta", derive(specta::Type))]
1476 }
1477 } else {
1478 quote! {
1479 #[derive(Debug, Clone, PartialEq, Eq)]
1480 }
1481 };
1482
1483 Ok(quote! {
1484 #doc_comment
1485 #derives
1486 pub enum #enum_name {
1487 #(#known_variants)*
1488 Custom(String),
1490 }
1491
1492 impl<'de> serde::Deserialize<'de> for #enum_name {
1493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1494 where
1495 D: serde::Deserializer<'de>,
1496 {
1497 let value = String::deserialize(deserializer)?;
1498 match value.as_str() {
1499 #(#match_arms_de)*
1500 _ => Ok(#enum_name::Custom(value)),
1501 }
1502 }
1503 }
1504
1505 impl serde::Serialize for #enum_name {
1506 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1507 where
1508 S: serde::Serializer,
1509 {
1510 let value = match self {
1511 #(#match_arms_ser)*
1512 #enum_name::Custom(s) => s.as_str(),
1513 };
1514 serializer.serialize_str(value)
1515 }
1516 }
1517 })
1518 }
1519
1520 fn generate_string_enum(
1521 &self,
1522 schema: &crate::analysis::AnalyzedSchema,
1523 values: &[String],
1524 ext: Option<&crate::analysis::EnumExtensions>,
1525 ) -> Result<TokenStream> {
1526 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1527
1528 let default_value = schema
1535 .default
1536 .as_ref()
1537 .and_then(|v| v.as_str())
1538 .map(|s| s.to_string());
1539 let has_default_match = match &default_value {
1540 Some(d) => values.iter().any(|v| v == d),
1541 None => !values.is_empty(),
1542 };
1543
1544 let varnames_override: Option<&Vec<String>> = ext
1548 .filter(|_| self.config.types.x_enum_varnames_enabled())
1549 .map(|e| &e.varnames)
1550 .filter(|v| !v.is_empty() && v.len() == values.len());
1551 let descriptions_override: Option<&Vec<String>> = ext
1552 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1553 .map(|e| &e.descriptions)
1554 .filter(|v| !v.is_empty() && v.len() == values.len());
1555
1556 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1563 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1564 .iter()
1565 .enumerate()
1566 .map(|(i, value)| {
1567 let base = match varnames_override {
1568 Some(v) => v[i].clone(),
1569 None => self.to_rust_enum_variant(value),
1570 };
1571 let mut variant_name = base.clone();
1572 let mut suffix = 2;
1573 while !used.insert(variant_name.clone()) {
1574 variant_name = format!("{base}_{suffix}");
1575 suffix += 1;
1576 }
1577 let variant_ident = format_ident!("{}", variant_name);
1578 let is_default = if let Some(ref default) = default_value {
1579 value == default
1580 } else {
1581 i == 0
1582 };
1583 let description = descriptions_override.map(|d| d[i].clone());
1584 (variant_ident, value, is_default, description)
1585 })
1586 .collect();
1587
1588 let variants =
1589 variant_pairs
1590 .iter()
1591 .map(|(variant_ident, value, is_default, description)| {
1592 let doc = description
1593 .as_ref()
1594 .map(|d| {
1595 let s = self.sanitize_doc_comment(d);
1596 quote! { #[doc = #s] }
1597 })
1598 .unwrap_or_default();
1599 if *is_default {
1600 quote! {
1601 #doc
1602 #[default]
1603 #[serde(rename = #value)]
1604 #variant_ident,
1605 }
1606 } else {
1607 quote! {
1608 #doc
1609 #[serde(rename = #value)]
1610 #variant_ident,
1611 }
1612 }
1613 });
1614
1615 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1619 quote! { Self::#variant_ident => #value, }
1620 });
1621
1622 let doc_comment = if let Some(desc) = &schema.description {
1623 quote! { #[doc = #desc] }
1624 } else {
1625 TokenStream::new()
1626 };
1627
1628 let derives = match (self.config.enable_specta, has_default_match) {
1631 (true, true) => quote! {
1632 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1633 #[cfg_attr(feature = "specta", derive(specta::Type))]
1634 },
1635 (true, false) => quote! {
1636 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1637 #[cfg_attr(feature = "specta", derive(specta::Type))]
1638 },
1639 (false, true) => quote! {
1640 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1641 },
1642 (false, false) => quote! {
1643 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1644 },
1645 };
1646
1647 Ok(quote! {
1648 #doc_comment
1649 #derives
1650 pub enum #enum_name {
1651 #(#variants)*
1652 }
1653
1654 impl #enum_name {
1655 pub fn as_str(&self) -> &'static str {
1656 match self {
1657 #(#as_str_arms)*
1658 }
1659 }
1660 }
1661
1662 impl ::std::fmt::Display for #enum_name {
1663 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1664 f.write_str(self.as_str())
1665 }
1666 }
1667
1668 impl AsRef<str> for #enum_name {
1669 fn as_ref(&self) -> &str {
1670 self.as_str()
1671 }
1672 }
1673 })
1674 }
1675
1676 fn generate_struct(
1677 &self,
1678 schema: &crate::analysis::AnalyzedSchema,
1679 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1680 required: &std::collections::HashSet<String>,
1681 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1682 analysis: &crate::analysis::SchemaAnalysis,
1683 type_context: &TypeGenerationContext<'_>,
1684 ) -> Result<TokenStream> {
1685 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1686 let emitted_properties = self.emitted_object_properties(
1687 &schema.name,
1688 properties,
1689 required,
1690 additional_properties,
1691 analysis,
1692 type_context.discriminated_variants.get(&schema.name),
1693 );
1694
1695 let mut fields: Vec<TokenStream> = emitted_properties
1696 .iter()
1697 .map(|emitted| {
1698 let field_name = emitted.wire_name;
1699 let property = emitted.property;
1700 let field_ident = &emitted.ident;
1701 let field_type = &emitted.field_type;
1702 let serde_attrs = self.generate_serde_field_attrs(
1703 &schema.name,
1704 field_name,
1705 field_ident,
1706 property,
1707 emitted.is_required,
1708 analysis,
1709 );
1710 let specta_attrs = self.generate_specta_field_attrs(field_name);
1711
1712 let doc_comment = if let Some(desc) = &property.description {
1713 let sanitized_desc = self.sanitize_doc_comment(desc);
1714 quote! { #[doc = #sanitized_desc] }
1715 } else {
1716 TokenStream::new()
1717 };
1718 let constraint_doc = self.generate_constraint_doc(&property.constraints);
1719
1720 quote! {
1721 #doc_comment
1722 #constraint_doc
1723 #serde_attrs
1724 #specta_attrs
1725 pub #field_ident: #field_type,
1726 }
1727 })
1728 .collect();
1729
1730 match additional_properties {
1736 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1737 crate::analysis::ObjectAdditionalProperties::Untyped => {
1738 fields.push(quote! {
1739 #[serde(flatten)]
1741 pub additional_properties:
1742 std::collections::BTreeMap<String, serde_json::Value>,
1743 });
1744 }
1745 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1746 let value_tokens = self.generate_array_item_type(value_type, analysis);
1747 fields.push(quote! {
1748 #[serde(flatten)]
1751 pub additional_properties:
1752 std::collections::BTreeMap<String, #value_tokens>,
1753 });
1754 }
1755 }
1756
1757 let doc_comment = if let Some(desc) = &schema.description {
1758 quote! { #[doc = #desc] }
1759 } else {
1760 TokenStream::new()
1761 };
1762
1763 let can_derive_default = emitted_properties
1769 .iter()
1770 .all(|property| !property.is_required);
1771
1772 let derives = match (self.config.enable_specta, can_derive_default) {
1776 (true, true) => quote! {
1777 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1778 #[cfg_attr(feature = "specta", derive(specta::Type))]
1779 },
1780 (true, false) => quote! {
1781 #[derive(Debug, Clone, Deserialize, Serialize)]
1782 #[cfg_attr(feature = "specta", derive(specta::Type))]
1783 },
1784 (false, true) => quote! {
1785 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1786 },
1787 (false, false) => quote! {
1788 #[derive(Debug, Clone, Deserialize, Serialize)]
1789 },
1790 };
1791
1792 let builder = if type_context.index.request_body_roots.contains(&schema.name)
1793 && emitted_properties
1794 .iter()
1795 .any(|property| property.is_required)
1796 && (emitted_properties
1797 .iter()
1798 .any(|property| !property.is_required)
1799 || !matches!(
1800 additional_properties,
1801 crate::analysis::ObjectAdditionalProperties::Forbidden
1802 )) {
1803 self.generate_request_model_builder(
1804 schema,
1805 &emitted_properties,
1806 additional_properties,
1807 analysis,
1808 type_context.index,
1809 )
1810 } else {
1811 TokenStream::new()
1812 };
1813
1814 Ok(quote! {
1815 #doc_comment
1816 #derives
1817 pub struct #struct_name {
1818 #(#fields)*
1819 }
1820
1821 #builder
1822 })
1823 }
1824
1825 pub(crate) fn emitted_object_properties<'a>(
1830 &self,
1831 schema_name: &str,
1832 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
1833 required: &std::collections::HashSet<String>,
1834 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1835 analysis: &crate::analysis::SchemaAnalysis,
1836 discriminator_info: Option<&DiscriminatedVariantInfo>,
1837 ) -> Vec<EmittedObjectProperty<'a>> {
1838 let mut sorted_properties: Vec<_> = properties.iter().collect();
1839 sorted_properties.sort_by_key(|(name, _)| name.as_str());
1840
1841 let mut used_field_idents = std::collections::HashSet::new();
1842 if !matches!(
1843 additional_properties,
1844 crate::analysis::ObjectAdditionalProperties::Forbidden
1845 ) {
1846 used_field_idents.insert("additional_properties".to_string());
1847 }
1848
1849 let mut emitted = Vec::new();
1850 for (field_name, property) in sorted_properties {
1851 if discriminator_info.is_some_and(|info| {
1852 !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
1853 }) {
1854 continue;
1855 }
1856
1857 let raw = self.to_rust_field_name(field_name);
1858 let mut chosen = raw.clone();
1859 let mut suffix = 2;
1860 while !used_field_idents.insert(chosen.clone()) {
1861 chosen = format!("{raw}_{suffix}");
1862 suffix += 1;
1863 }
1864 let is_required = required.contains(field_name);
1865 emitted.push(EmittedObjectProperty {
1866 wire_name: field_name,
1867 property,
1868 ident: Self::to_field_ident(&chosen),
1869 is_required,
1870 field_type: self.generate_field_type(
1871 schema_name,
1872 field_name,
1873 property,
1874 is_required,
1875 analysis,
1876 ),
1877 });
1878 }
1879 emitted
1880 }
1881
1882 fn type_generation_index(
1883 &self,
1884 analysis: &crate::analysis::SchemaAnalysis,
1885 ) -> TypeGenerationIndex {
1886 let reserved_type_names = analysis
1887 .schemas
1888 .keys()
1889 .map(|name| self.to_rust_type_name(name))
1890 .collect();
1891 let mut request_body_roots = std::collections::HashSet::new();
1892 for operation in analysis.operations.values() {
1893 let Some(mut current) = operation
1894 .request_body
1895 .as_ref()
1896 .and_then(crate::analysis::RequestBodyContent::schema_name)
1897 else {
1898 continue;
1899 };
1900 while request_body_roots.insert(current.to_string()) {
1901 let Some(crate::analysis::AnalyzedSchema {
1902 schema_type: crate::analysis::SchemaType::Reference { target },
1903 ..
1904 }) = analysis.schemas.get(current)
1905 else {
1906 break;
1907 };
1908 current = target;
1909 }
1910 }
1911 TypeGenerationIndex {
1912 request_body_roots,
1913 reserved_type_names,
1914 }
1915 }
1916
1917 fn generate_request_model_builder(
1918 &self,
1919 schema: &crate::analysis::AnalyzedSchema,
1920 properties: &[EmittedObjectProperty<'_>],
1921 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1922 analysis: &crate::analysis::SchemaAnalysis,
1923 type_index: &TypeGenerationIndex,
1924 ) -> TokenStream {
1925 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1926 let builder_base = format!("{}Builder", struct_name);
1927 let mut builder_name = builder_base.clone();
1928 let mut suffix = 2;
1929 while type_index.reserved_type_names.contains(&builder_name) {
1930 builder_name = format!("{builder_base}{suffix}");
1931 suffix += 1;
1932 }
1933 let builder_name = format_ident!("{builder_name}");
1934
1935 let required_parameters: Vec<TokenStream> = properties
1936 .iter()
1937 .filter(|property| property.is_required)
1938 .map(|property| {
1939 let ident = &property.ident;
1940 let field_type = &property.field_type;
1941 quote! { #ident: #field_type }
1942 })
1943 .collect();
1944 let required_idents: Vec<&syn::Ident> = properties
1945 .iter()
1946 .filter(|property| property.is_required)
1947 .map(|property| &property.ident)
1948 .collect();
1949 let optional_initializers: Vec<TokenStream> = properties
1950 .iter()
1951 .filter(|property| !property.is_required)
1952 .map(|property| {
1953 let ident = &property.ident;
1954 quote! { #ident: None }
1955 })
1956 .collect();
1957
1958 let additional_initializer = match additional_properties {
1959 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
1960 crate::analysis::ObjectAdditionalProperties::Untyped
1961 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
1962 additional_properties: ::std::collections::BTreeMap::new(),
1963 },
1964 };
1965
1966 let mut used_builder_methods =
1967 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
1968 if !matches!(
1969 additional_properties,
1970 crate::analysis::ObjectAdditionalProperties::Forbidden
1971 ) {
1972 used_builder_methods.insert("additional_properties".to_string());
1973 }
1974 let optional_setters: Vec<TokenStream> = properties
1975 .iter()
1976 .filter(|property| !property.is_required)
1977 .map(|property| {
1978 let field_ident = &property.ident;
1979 let field_type = self.generate_property_base_type(
1980 &schema.name,
1981 property.wire_name,
1982 property.property,
1983 analysis,
1984 );
1985 let field_name = field_ident.to_string();
1989 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
1990 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
1991 format!("with_{plain_field_name}")
1992 } else {
1993 field_name.clone()
1994 };
1995 let setter_base = setter_name.clone();
1996 let mut suffix = 2;
1997 while !used_builder_methods.insert(setter_name.clone()) {
1998 setter_name = format!("{setter_base}_{suffix}");
1999 suffix += 1;
2000 }
2001 let setter_ident = Self::to_field_ident(&setter_name);
2002 let wire_name = property.wire_name;
2003 quote! {
2004 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2005 #[must_use]
2006 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2007 self.value.#field_ident = Some(#field_ident);
2008 self
2009 }
2010 }
2011 })
2012 .collect();
2013
2014 let additional_setter = match additional_properties {
2015 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2016 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2017 #[must_use]
2019 pub fn additional_properties(
2020 mut self,
2021 additional_properties: ::std::collections::BTreeMap<
2022 String,
2023 serde_json::Value,
2024 >,
2025 ) -> Self {
2026 self.value.additional_properties = additional_properties;
2027 self
2028 }
2029 },
2030 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2031 let value_type = self.generate_array_item_type(value_type, analysis);
2032 quote! {
2033 #[must_use]
2035 pub fn additional_properties(
2036 mut self,
2037 additional_properties: ::std::collections::BTreeMap<
2038 String,
2039 #value_type,
2040 >,
2041 ) -> Self {
2042 self.value.additional_properties = additional_properties;
2043 self
2044 }
2045 }
2046 }
2047 };
2048
2049 quote! {
2050 impl #struct_name {
2051 pub fn new(#(#required_parameters),*) -> Self {
2053 Self {
2054 #(#required_idents,)*
2055 #(#optional_initializers,)*
2056 #additional_initializer
2057 }
2058 }
2059
2060 pub fn builder(#(#required_parameters),*) -> #builder_name {
2062 #builder_name::new(#(#required_idents),*)
2063 }
2064 }
2065
2066 #[derive(Debug, Clone)]
2068 #[must_use]
2069 pub struct #builder_name {
2070 value: #struct_name,
2071 }
2072
2073 impl #builder_name {
2074 pub fn new(#(#required_parameters),*) -> Self {
2076 Self {
2077 value: #struct_name::new(#(#required_idents),*),
2078 }
2079 }
2080
2081 #(#optional_setters)*
2082 #additional_setter
2083
2084 pub fn build(self) -> #struct_name {
2086 self.value
2087 }
2088 }
2089 }
2090 }
2091
2092 fn generate_discriminated_enum(
2093 &self,
2094 schema: &crate::analysis::AnalyzedSchema,
2095 discriminator_field: &str,
2096 variants: &[crate::analysis::UnionVariant],
2097 analysis: &crate::analysis::SchemaAnalysis,
2098 ) -> Result<TokenStream> {
2099 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2100
2101 let has_nested_discriminated_union = variants.iter().any(|variant| {
2103 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2104 matches!(
2105 variant_schema.schema_type,
2106 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2107 )
2108 } else {
2109 false
2110 }
2111 });
2112
2113 if has_nested_discriminated_union {
2115 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2117 .iter()
2118 .map(|v| crate::analysis::SchemaRef {
2119 target: v.type_name.clone(),
2120 nullable: false,
2121 })
2122 .collect();
2123 return self.generate_union_enum(schema, &schema_refs, analysis);
2124 }
2125
2126 let enclosing = self.to_rust_type_name(&schema.name);
2127 let enum_variants = variants.iter().map(|variant| {
2128 let variant_name = format_ident!("{}", variant.rust_name);
2129 let variant_value = &variant.discriminator_value;
2130
2131 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2132 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2136 || analysis
2137 .dependencies
2138 .recursive_schemas
2139 .contains(&variant.type_name)
2140 {
2141 quote! { Box<#variant_type> }
2142 } else {
2143 quote! { #variant_type }
2144 };
2145 quote! {
2146 #[serde(rename = #variant_value)]
2147 #variant_name(#payload),
2148 }
2149 });
2150
2151 let doc_comment = if let Some(desc) = &schema.description {
2152 quote! { #[doc = #desc] }
2153 } else {
2154 TokenStream::new()
2155 };
2156
2157 let derives = if self.config.enable_specta {
2159 quote! {
2160 #[derive(Debug, Clone, Deserialize, Serialize)]
2161 #[cfg_attr(feature = "specta", derive(specta::Type))]
2162 #[serde(tag = #discriminator_field)]
2163 }
2164 } else {
2165 quote! {
2166 #[derive(Debug, Clone, Deserialize, Serialize)]
2167 #[serde(tag = #discriminator_field)]
2168 }
2169 };
2170
2171 Ok(quote! {
2172 #doc_comment
2173 #derives
2174 pub enum #enum_name {
2175 #(#enum_variants)*
2176 }
2177 })
2178 }
2179
2180 fn should_use_untagged_discriminated_union(
2182 &self,
2183 schema: &crate::analysis::AnalyzedSchema,
2184 analysis: &crate::analysis::SchemaAnalysis,
2185 ) -> bool {
2186 for other_schema in analysis.schemas.values() {
2191 if let crate::analysis::SchemaType::DiscriminatedUnion {
2192 variants,
2193 discriminator_field: _,
2194 } = &other_schema.schema_type
2195 {
2196 for variant in variants {
2197 if variant.type_name == schema.name {
2198 if let crate::analysis::SchemaType::DiscriminatedUnion {
2203 discriminator_field: current_discriminator,
2204 variants: current_variants,
2205 ..
2206 } = &schema.schema_type
2207 {
2208 for current_variant in current_variants {
2210 if let Some(variant_schema) =
2211 analysis.schemas.get(¤t_variant.type_name)
2212 {
2213 if let crate::analysis::SchemaType::Object {
2214 properties, ..
2215 } = &variant_schema.schema_type
2216 {
2217 if properties.contains_key(current_discriminator) {
2218 return false;
2221 }
2222 }
2223 }
2224 }
2225 }
2226
2227 return true;
2229 }
2230 }
2231 }
2232 }
2233 false
2234 }
2235
2236 fn generate_union_enum(
2237 &self,
2238 schema: &crate::analysis::AnalyzedSchema,
2239 variants: &[crate::analysis::SchemaRef],
2240 analysis: &crate::analysis::SchemaAnalysis,
2241 ) -> Result<TokenStream> {
2242 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2243
2244 let mut used_variant_names = std::collections::HashSet::new();
2246 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2247 let base_variant_name = self.type_name_to_variant_name(&variant.target);
2249 let variant_name = self.ensure_unique_variant_name_generator(
2250 base_variant_name,
2251 &mut used_variant_names,
2252 i,
2253 );
2254 let variant_name_ident = format_ident!("{}", variant_name);
2255
2256 let variant_type_tokens = if matches!(
2258 variant.target.as_str(),
2259 "bool"
2260 | "i8"
2261 | "i16"
2262 | "i32"
2263 | "i64"
2264 | "i128"
2265 | "u8"
2266 | "u16"
2267 | "u32"
2268 | "u64"
2269 | "u128"
2270 | "f32"
2271 | "f64"
2272 | "String"
2273 ) {
2274 let type_ident = format_ident!("{}", variant.target);
2275 quote! { #type_ident }
2276 } else if variant.target == "serde_json::Value" {
2277 quote! { serde_json::Value }
2280 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2281 let inner = &variant.target[4..variant.target.len() - 1];
2283
2284 if inner.starts_with("Vec<") && inner.ends_with(">") {
2286 let inner_inner = &inner[4..inner.len() - 1];
2287 if inner_inner == "serde_json::Value" {
2288 quote! { Vec<Vec<serde_json::Value>> }
2289 } else {
2290 let inner_inner_type = if matches!(
2291 inner_inner,
2292 "bool"
2293 | "i8"
2294 | "i16"
2295 | "i32"
2296 | "i64"
2297 | "i128"
2298 | "u8"
2299 | "u16"
2300 | "u32"
2301 | "u64"
2302 | "u128"
2303 | "f32"
2304 | "f64"
2305 | "String"
2306 ) {
2307 format_ident!("{}", inner_inner)
2308 } else {
2309 format_ident!("{}", self.to_rust_type_name(inner_inner))
2310 };
2311 quote! { Vec<Vec<#inner_inner_type>> }
2312 }
2313 } else if inner == "serde_json::Value" {
2314 quote! { Vec<serde_json::Value> }
2315 } else {
2316 let inner_type = if matches!(
2317 inner,
2318 "bool"
2319 | "i8"
2320 | "i16"
2321 | "i32"
2322 | "i64"
2323 | "i128"
2324 | "u8"
2325 | "u16"
2326 | "u32"
2327 | "u64"
2328 | "u128"
2329 | "f32"
2330 | "f64"
2331 | "String"
2332 ) {
2333 format_ident!("{}", inner)
2334 } else {
2335 format_ident!("{}", self.to_rust_type_name(inner))
2336 };
2337 quote! { Vec<#inner_type> }
2338 }
2339 } else if variant.target.contains("::") || variant.target.contains('<') {
2340 parse_rust_type(&variant.target).unwrap_or_else(|_| {
2345 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2346 quote! { #fallback }
2347 })
2348 } else {
2349 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2350 quote! { #type_ident }
2351 };
2352
2353 let target_rust_name = self.to_rust_type_name(&variant.target);
2357 let enclosing_name = self.to_rust_type_name(&schema.name);
2358 let is_self_ref = target_rust_name == enclosing_name;
2359 let is_recursive_target = analysis
2363 .dependencies
2364 .recursive_schemas
2365 .contains(&variant.target);
2366 let variant_type_tokens = if is_self_ref || is_recursive_target {
2367 quote! { Box<#variant_type_tokens> }
2368 } else {
2369 variant_type_tokens
2370 };
2371
2372 quote! {
2373 #variant_name_ident(#variant_type_tokens),
2374 }
2375 });
2376
2377 let doc_comment = if let Some(desc) = &schema.description {
2378 quote! { #[doc = #desc] }
2379 } else {
2380 TokenStream::new()
2381 };
2382
2383 let derives = if self.config.enable_specta {
2385 quote! {
2386 #[derive(Debug, Clone, Deserialize, Serialize)]
2387 #[cfg_attr(feature = "specta", derive(specta::Type))]
2388 #[serde(untagged)]
2389 }
2390 } else {
2391 quote! {
2392 #[derive(Debug, Clone, Deserialize, Serialize)]
2393 #[serde(untagged)]
2394 }
2395 };
2396
2397 Ok(quote! {
2398 #doc_comment
2399 #derives
2400 pub enum #enum_name {
2401 #(#enum_variants)*
2402 }
2403 })
2404 }
2405
2406 fn target_aliases_back_to(
2411 &self,
2412 target: &str,
2413 enclosing_rust_name: &str,
2414 analysis: &crate::analysis::SchemaAnalysis,
2415 ) -> bool {
2416 let mut current = target.to_string();
2417 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2418 for _ in 0..16 {
2419 if !visited.insert(current.clone()) {
2420 return true;
2421 }
2422 let Some(schema) = analysis.schemas.get(¤t) else {
2423 return false;
2424 };
2425 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2426 if self.to_rust_type_name(next) == enclosing_rust_name {
2427 return true;
2428 }
2429 current = next.clone();
2430 continue;
2431 }
2432 return false;
2433 }
2434 false
2435 }
2436
2437 fn generate_field_type(
2438 &self,
2439 schema_name: &str,
2440 field_name: &str,
2441 prop: &crate::analysis::PropertyInfo,
2442 is_required: bool,
2443 analysis: &crate::analysis::SchemaAnalysis,
2444 ) -> TokenStream {
2445 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2446
2447 if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2448 quote! { Option<#base_type> }
2449 } else {
2450 base_type
2451 }
2452 }
2453
2454 fn property_is_option_wrapped(
2455 &self,
2456 schema_name: &str,
2457 field_name: &str,
2458 prop: &crate::analysis::PropertyInfo,
2459 is_required: bool,
2460 analysis: &crate::analysis::SchemaAnalysis,
2461 ) -> bool {
2462 let override_key = format!("{schema_name}.{field_name}");
2463 let is_nullable_override = self
2464 .config
2465 .nullable_field_overrides
2466 .get(&override_key)
2467 .copied()
2468 .unwrap_or(false);
2469
2470 !is_required
2471 || prop.nullable
2472 || is_nullable_override
2473 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2474 }
2475
2476 pub(crate) fn generate_property_base_type(
2477 &self,
2478 schema_name: &str,
2479 _field_name: &str,
2480 prop: &crate::analysis::PropertyInfo,
2481 analysis: &crate::analysis::SchemaAnalysis,
2482 ) -> TokenStream {
2483 use crate::analysis::SchemaType;
2484
2485 match &prop.schema_type {
2486 SchemaType::Primitive { rust_type, .. } => {
2487 parse_rust_type(rust_type).unwrap_or_else(|_| {
2490 eprintln!(
2495 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
2496 falling back to String"
2497 );
2498 quote! { String }
2499 })
2500 }
2501 SchemaType::Reference { target } => {
2502 let target_rust_name = self.to_rust_type_name(target);
2503 let target_type = format_ident!("{}", target_rust_name);
2504 let enclosing_rust_name = self.to_rust_type_name(schema_name);
2517 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2518 let is_alias_chain_self =
2519 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2520 if analysis.dependencies.recursive_schemas.contains(target)
2521 || is_self_via_rust_name
2522 || is_alias_chain_self
2523 {
2524 quote! { Box<#target_type> }
2525 } else {
2526 quote! { #target_type }
2527 }
2528 }
2529 SchemaType::Array { item_type } => {
2530 let inner_type = self.generate_array_item_type(item_type, analysis);
2531 quote! { Vec<#inner_type> }
2532 }
2533 _ => {
2534 quote! { serde_json::Value }
2536 }
2537 }
2538 }
2539
2540 fn generate_serde_field_attrs(
2541 &self,
2542 schema_name: &str,
2543 field_name: &str,
2544 field_ident: &syn::Ident,
2545 prop: &crate::analysis::PropertyInfo,
2546 is_required: bool,
2547 analysis: &crate::analysis::SchemaAnalysis,
2548 ) -> TokenStream {
2549 let mut attrs = Vec::new();
2550
2551 let rust_field_name = field_ident.to_string();
2554 let comparison_name = rust_field_name
2555 .strip_prefix("r#")
2556 .unwrap_or(&rust_field_name);
2557 if comparison_name != field_name {
2558 attrs.push(quote! { rename = #field_name });
2559 }
2560
2561 if !is_required || prop.nullable {
2563 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2564 }
2565
2566 if prop.default.is_some()
2570 && (is_required && !prop.nullable)
2571 && !self.type_lacks_default(&prop.schema_type, analysis)
2572 {
2573 attrs.push(quote! { default });
2574 }
2575
2576 if let crate::analysis::SchemaType::Primitive {
2584 serde_with: Some(codec),
2585 ..
2586 } = &prop.schema_type
2587 {
2588 let is_option_wrapped = self.property_is_option_wrapped(
2589 schema_name,
2590 field_name,
2591 prop,
2592 is_required,
2593 analysis,
2594 );
2595 let codec_path = if is_option_wrapped {
2596 format!("{codec}::option")
2597 } else {
2598 codec.clone()
2599 };
2600 attrs.push(quote! { with = #codec_path });
2601 if is_option_wrapped {
2606 attrs.push(quote! { default });
2607 }
2608 }
2609
2610 if attrs.is_empty() {
2611 TokenStream::new()
2612 } else {
2613 quote! { #[serde(#(#attrs),*)] }
2614 }
2615 }
2616
2617 fn type_lacks_default(
2621 &self,
2622 schema_type: &crate::analysis::SchemaType,
2623 analysis: &crate::analysis::SchemaAnalysis,
2624 ) -> bool {
2625 use crate::analysis::SchemaType;
2626 match schema_type {
2627 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2628 SchemaType::Primitive { rust_type, .. } => matches!(
2632 rust_type.as_str(),
2633 "chrono::DateTime<chrono::Utc>"
2634 | "chrono::NaiveDate"
2635 | "chrono::NaiveTime"
2636 | "chrono::Duration"
2637 | "url::Url"
2638 | "time::OffsetDateTime"
2639 | "time::Date"
2640 | "time::Time"
2641 | "iso8601::Duration"
2642 | "email_address::EmailAddress"
2643 ),
2644 SchemaType::Reference { target } => {
2645 if let Some(schema) = analysis.schemas.get(target) {
2646 self.type_lacks_default(&schema.schema_type, analysis)
2647 } else {
2648 false
2649 }
2650 }
2651 _ => false,
2652 }
2653 }
2654
2655 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2656 if !self.config.enable_specta {
2657 return TokenStream::new();
2658 }
2659
2660 let camel_case_name = self.to_camel_case(field_name);
2662
2663 if camel_case_name != field_name {
2665 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2666 } else {
2667 TokenStream::new()
2668 }
2669 }
2670
2671 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2672 let neg_prefix =
2676 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2677 "Neg"
2678 } else {
2679 ""
2680 };
2681
2682 let mut result = String::new();
2684 let mut next_upper = true;
2685 let mut prev_was_upper = false;
2686
2687 for (i, c) in s.chars().enumerate() {
2688 match c {
2689 'a'..='z' => {
2690 if next_upper {
2691 result.push(c.to_ascii_uppercase());
2692 next_upper = false;
2693 } else {
2694 result.push(c);
2695 }
2696 prev_was_upper = false;
2697 }
2698 'A'..='Z' => {
2699 if next_upper || (!prev_was_upper && i > 0) {
2700 result.push(c);
2702 next_upper = false;
2703 } else {
2704 result.push(c.to_ascii_lowercase());
2706 }
2707 prev_was_upper = true;
2708 }
2709 '0'..='9' => {
2710 result.push(c);
2711 next_upper = false;
2712 prev_was_upper = false;
2713 }
2714 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2715 next_upper = true;
2717 prev_was_upper = false;
2718 }
2719 _ => {
2720 next_upper = true;
2722 prev_was_upper = false;
2723 }
2724 }
2725 }
2726
2727 if result.is_empty() {
2729 result = "Value".to_string();
2730 }
2731
2732 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2734 result = format!("Variant{neg_prefix}{result}");
2735 } else if !neg_prefix.is_empty() {
2736 result = format!("{neg_prefix}{result}");
2739 }
2740
2741 match result.as_str() {
2743 "Null" => "NullValue".to_string(),
2744 "True" => "TrueValue".to_string(),
2745 "False" => "FalseValue".to_string(),
2746 "Type" => "Type_".to_string(),
2747 "Match" => "Match_".to_string(),
2748 "Fn" => "Fn_".to_string(),
2749 "Impl" => "Impl_".to_string(),
2750 "Trait" => "Trait_".to_string(),
2751 "Struct" => "Struct_".to_string(),
2752 "Enum" => "Enum_".to_string(),
2753 "Mod" => "Mod_".to_string(),
2754 "Use" => "Use_".to_string(),
2755 "Pub" => "Pub_".to_string(),
2756 "Const" => "Const_".to_string(),
2757 "Static" => "Static_".to_string(),
2758 "Let" => "Let_".to_string(),
2759 "Mut" => "Mut_".to_string(),
2760 "Ref" => "Ref_".to_string(),
2761 "Move" => "Move_".to_string(),
2762 "Return" => "Return_".to_string(),
2763 "If" => "If_".to_string(),
2764 "Else" => "Else_".to_string(),
2765 "While" => "While_".to_string(),
2766 "For" => "For_".to_string(),
2767 "Loop" => "Loop_".to_string(),
2768 "Break" => "Break_".to_string(),
2769 "Continue" => "Continue_".to_string(),
2770 "Self" => "Self_".to_string(),
2771 "Super" => "Super_".to_string(),
2772 "Crate" => "Crate_".to_string(),
2773 "Async" => "Async_".to_string(),
2774 "Await" => "Await_".to_string(),
2775 _ => result,
2776 }
2777 }
2778
2779 #[allow(dead_code)]
2780 fn to_rust_identifier(&self, s: &str) -> String {
2781 let mut result = s
2783 .chars()
2784 .map(|c| match c {
2785 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2786 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2787 _ => '_',
2788 })
2789 .collect::<String>();
2790
2791 result = result.trim_matches('_').to_string();
2793
2794 if result.is_empty() {
2796 result = "value".to_string();
2797 }
2798
2799 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2801 result = format!("variant_{result}");
2802 }
2803
2804 match result.as_str() {
2806 "null" => "null_value".to_string(),
2807 "true" => "true_value".to_string(),
2808 "false" => "false_value".to_string(),
2809 "type" => "type_".to_string(),
2810 "match" => "match_".to_string(),
2811 "fn" => "fn_".to_string(),
2812 "impl" => "impl_".to_string(),
2813 "trait" => "trait_".to_string(),
2814 "struct" => "struct_".to_string(),
2815 "enum" => "enum_".to_string(),
2816 "mod" => "mod_".to_string(),
2817 "use" => "use_".to_string(),
2818 "pub" => "pub_".to_string(),
2819 "const" => "const_".to_string(),
2820 "static" => "static_".to_string(),
2821 "let" => "let_".to_string(),
2822 "mut" => "mut_".to_string(),
2823 "ref" => "ref_".to_string(),
2824 "move" => "move_".to_string(),
2825 "return" => "return_".to_string(),
2826 "if" => "if_".to_string(),
2827 "else" => "else_".to_string(),
2828 "while" => "while_".to_string(),
2829 "for" => "for_".to_string(),
2830 "loop" => "loop_".to_string(),
2831 "break" => "break_".to_string(),
2832 "continue" => "continue_".to_string(),
2833 "self" => "self_".to_string(),
2834 "super" => "super_".to_string(),
2835 "crate" => "crate_".to_string(),
2836 "async" => "async_".to_string(),
2837 "await" => "await_".to_string(),
2838 "override" => "override_".to_string(),
2840 "box" => "box_".to_string(),
2841 "dyn" => "dyn_".to_string(),
2842 "where" => "where_".to_string(),
2843 "in" => "in_".to_string(),
2844 "abstract" => "abstract_".to_string(),
2846 "become" => "become_".to_string(),
2847 "do" => "do_".to_string(),
2848 "final" => "final_".to_string(),
2849 "macro" => "macro_".to_string(),
2850 "priv" => "priv_".to_string(),
2851 "try" => "try_".to_string(),
2852 "typeof" => "typeof_".to_string(),
2853 "unsized" => "unsized_".to_string(),
2854 "virtual" => "virtual_".to_string(),
2855 "yield" => "yield_".to_string(),
2856 _ => result,
2857 }
2858 }
2859
2860 fn generate_constraint_doc(
2868 &self,
2869 constraints: &crate::analysis::PropertyConstraints,
2870 ) -> TokenStream {
2871 use crate::type_mapping::ConstraintMode;
2872
2873 if constraints.is_empty() {
2874 return TokenStream::new();
2875 }
2876 match self.config.types.constraint_mode() {
2877 ConstraintMode::Off => TokenStream::new(),
2878 ConstraintMode::Doc => {
2879 let formatted = format_constraints_doc(constraints);
2880 quote! { #[doc = #formatted] }
2881 }
2882 }
2883 }
2884
2885 fn sanitize_doc_comment(&self, desc: &str) -> String {
2886 let mut result = desc.to_string();
2888
2889 if result.contains('\n')
2897 && (result.contains('{')
2898 || result.contains("```")
2899 || result.contains("Human:")
2900 || result.contains("Assistant:")
2901 || result
2902 .lines()
2903 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2904 {
2905 if result.contains("```") {
2907 result = result.replace("```", "```ignore");
2908 } else {
2909 if result.lines().any(|line| {
2911 let trimmed = line.trim();
2912 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2913 }) {
2914 result = format!("```ignore\n{result}\n```");
2915 }
2916 }
2917 }
2918
2919 result
2920 }
2921
2922 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2923 let mut result = String::new();
2925 let mut next_upper = true;
2926 let mut prev_was_lower = false;
2927
2928 for c in s.chars() {
2929 match c {
2930 'a'..='z' => {
2931 if next_upper {
2932 result.push(c.to_ascii_uppercase());
2933 next_upper = false;
2934 } else {
2935 result.push(c);
2936 }
2937 prev_was_lower = true;
2938 }
2939 'A'..='Z' => {
2940 result.push(c);
2941 next_upper = false;
2942 prev_was_lower = false;
2943 }
2944 '0'..='9' => {
2945 if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2948 }
2950 result.push(c);
2951 next_upper = false;
2952 prev_was_lower = false;
2953 }
2954 '_' | '-' | '.' | ' ' => {
2955 next_upper = true;
2957 prev_was_lower = false;
2958 }
2959 _ => {
2960 next_upper = true;
2962 prev_was_lower = false;
2963 }
2964 }
2965 }
2966
2967 if result.is_empty() {
2969 result = "Type".to_string();
2970 }
2971
2972 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2974 result = format!("Type{result}");
2975 }
2976
2977 if matches!(
2984 result.as_str(),
2985 "Result"
2986 | "Option"
2987 | "Box"
2988 | "Vec"
2989 | "String"
2990 | "Some"
2991 | "None"
2992 | "Ok"
2993 | "Err"
2994 | "Default"
2995 | "Clone"
2996 | "Debug"
2997 | "Send"
2998 | "Sync"
2999 | "Sized"
3000 | "Iterator"
3001 | "From"
3002 | "Into"
3003 | "TryFrom"
3004 | "TryInto"
3005 | "AsRef"
3006 | "AsMut"
3007 ) {
3008 result.push_str("Type");
3009 }
3010
3011 result
3012 }
3013
3014 fn to_rust_field_name(&self, s: &str) -> String {
3015 let leading_marker = match s.chars().next() {
3019 Some('-') if s.len() > 1 => "neg_",
3020 Some('+') if s.len() > 1 => "pos_",
3021 _ => "",
3022 };
3023
3024 let mut result = String::new();
3026 let mut prev_was_upper = false;
3027 let mut prev_was_underscore = false;
3028
3029 for (i, c) in s.chars().enumerate() {
3030 match c {
3031 'A'..='Z' => {
3032 if i > 0 && !prev_was_upper && !prev_was_underscore {
3034 result.push('_');
3035 }
3036 result.push(c.to_ascii_lowercase());
3037 prev_was_upper = true;
3038 prev_was_underscore = false;
3039 }
3040 'a'..='z' | '0'..='9' => {
3041 result.push(c);
3042 prev_was_upper = false;
3043 prev_was_underscore = false;
3044 }
3045 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3046 if !prev_was_underscore && !result.is_empty() {
3047 result.push('_');
3048 prev_was_underscore = true;
3049 }
3050 prev_was_upper = false;
3051 }
3052 _ => {
3053 if !prev_was_underscore && !result.is_empty() {
3055 result.push('_');
3056 }
3057 prev_was_upper = false;
3058 prev_was_underscore = true;
3059 }
3060 }
3061 }
3062
3063 let mut result = result.trim_matches('_').to_string();
3065 if result.is_empty() {
3066 return "field".to_string();
3067 }
3068
3069 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3071 result = format!("field_{leading_marker}{result}");
3072 } else if !leading_marker.is_empty() {
3073 result = format!("{leading_marker}{result}");
3074 }
3075
3076 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3080 return format!("{result}_field");
3081 }
3082 if Self::is_rust_keyword(&result) {
3084 format!("r#{result}")
3085 } else {
3086 result
3087 }
3088 }
3089
3090 pub fn is_rust_keyword(s: &str) -> bool {
3092 matches!(
3093 s,
3094 "type"
3095 | "match"
3096 | "fn"
3097 | "struct"
3098 | "enum"
3099 | "impl"
3100 | "trait"
3101 | "mod"
3102 | "use"
3103 | "pub"
3104 | "const"
3105 | "static"
3106 | "let"
3107 | "mut"
3108 | "ref"
3109 | "move"
3110 | "return"
3111 | "if"
3112 | "else"
3113 | "while"
3114 | "for"
3115 | "loop"
3116 | "break"
3117 | "continue"
3118 | "self"
3119 | "super"
3120 | "crate"
3121 | "async"
3122 | "await"
3123 | "override"
3124 | "box"
3125 | "dyn"
3126 | "where"
3127 | "in"
3128 | "abstract"
3129 | "become"
3130 | "do"
3131 | "final"
3132 | "macro"
3133 | "priv"
3134 | "try"
3135 | "typeof"
3136 | "unsized"
3137 | "virtual"
3138 | "yield"
3139 | "gen"
3141 )
3142 }
3143
3144 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3146 if let Some(raw) = name.strip_prefix("r#") {
3147 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3148 } else {
3149 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3150 }
3151 }
3152
3153 fn to_camel_case(&self, s: &str) -> String {
3154 let mut result = String::new();
3156 let mut capitalize_next = false;
3157
3158 for (i, c) in s.chars().enumerate() {
3159 match c {
3160 '_' | '-' | '.' | ' ' => {
3161 capitalize_next = true;
3163 }
3164 'A'..='Z' => {
3165 if i == 0 {
3166 result.push(c.to_ascii_lowercase());
3168 } else if capitalize_next {
3169 result.push(c);
3170 capitalize_next = false;
3171 } else {
3172 result.push(c.to_ascii_lowercase());
3173 }
3174 }
3175 'a'..='z' | '0'..='9' => {
3176 if capitalize_next {
3177 result.push(c.to_ascii_uppercase());
3178 capitalize_next = false;
3179 } else {
3180 result.push(c);
3181 }
3182 }
3183 _ => {
3184 capitalize_next = true;
3186 }
3187 }
3188 }
3189
3190 if result.is_empty() {
3191 return "field".to_string();
3192 }
3193
3194 result
3195 }
3196
3197 fn generate_composition_struct(
3198 &self,
3199 schema: &crate::analysis::AnalyzedSchema,
3200 schemas: &[crate::analysis::SchemaRef],
3201 ) -> Result<TokenStream> {
3202 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3203
3204 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3210 let field_name = format_ident!("part_{}", i);
3211 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3212
3213 quote! {
3214 #[serde(flatten)]
3215 pub #field_name: #field_type,
3216 }
3217 });
3218
3219 let doc_comment = if let Some(desc) = &schema.description {
3220 quote! { #[doc = #desc] }
3221 } else {
3222 TokenStream::new()
3223 };
3224
3225 let derives = if self.config.enable_specta {
3227 quote! {
3228 #[derive(Debug, Clone, Deserialize, Serialize)]
3229 #[cfg_attr(feature = "specta", derive(specta::Type))]
3230 }
3231 } else {
3232 quote! {
3233 #[derive(Debug, Clone, Deserialize, Serialize)]
3234 }
3235 };
3236
3237 Ok(quote! {
3238 #doc_comment
3239 #derives
3240 pub struct #struct_name {
3241 #(#fields)*
3242 }
3243 })
3244 }
3245
3246 #[allow(dead_code)]
3247 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3248 let mut missing = std::collections::HashSet::new();
3249 let defined_types: std::collections::HashSet<String> =
3250 analysis.schemas.keys().cloned().collect();
3251
3252 for schema in analysis.schemas.values() {
3254 match &schema.schema_type {
3255 crate::analysis::SchemaType::Union { variants } => {
3256 for variant in variants {
3257 if !defined_types.contains(&variant.target) {
3258 missing.insert(variant.target.clone());
3259 }
3260 }
3261 }
3262 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3263 for variant in variants {
3264 if !defined_types.contains(&variant.type_name) {
3265 missing.insert(variant.type_name.clone());
3266 }
3267 }
3268 }
3269 crate::analysis::SchemaType::Object { properties, .. } => {
3270 let mut sorted_props: Vec<_> = properties.iter().collect();
3272 sorted_props.sort_by_key(|(name, _)| name.as_str());
3273 for (_, prop) in sorted_props {
3274 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3275 {
3276 if !defined_types.contains(target) {
3277 missing.insert(target.clone());
3278 }
3279 }
3280 }
3281 }
3282 crate::analysis::SchemaType::Reference { target }
3283 if !defined_types.contains(target) =>
3284 {
3285 missing.insert(target.clone());
3286 }
3287 _ => {}
3288 }
3289 }
3290
3291 missing
3292 }
3293
3294 #[allow(clippy::only_used_in_recursion)]
3295 fn generate_array_item_type(
3296 &self,
3297 item_type: &crate::analysis::SchemaType,
3298 analysis: &crate::analysis::SchemaAnalysis,
3299 ) -> TokenStream {
3300 use crate::analysis::SchemaType;
3301
3302 match item_type {
3303 SchemaType::Primitive { rust_type, .. } => {
3304 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3309 quote! { #parsed }
3310 } else if rust_type.contains("::") {
3311 let parts: Vec<_> = rust_type
3312 .split("::")
3313 .map(|p| format_ident!("{}", p))
3314 .collect();
3315 quote! { #(#parts)::* }
3316 } else {
3317 let type_ident = format_ident!("{}", rust_type);
3318 quote! { #type_ident }
3319 }
3320 }
3321 SchemaType::Reference { target } => {
3322 let target_type = format_ident!("{}", self.to_rust_type_name(target));
3323 if analysis.dependencies.recursive_schemas.contains(target) {
3325 quote! { Box<#target_type> }
3326 } else {
3327 quote! { #target_type }
3328 }
3329 }
3330 SchemaType::Array { item_type } => {
3331 let inner_type = self.generate_array_item_type(item_type, analysis);
3333 quote! { Vec<#inner_type> }
3334 }
3335 _ => {
3336 quote! { serde_json::Value }
3338 }
3339 }
3340 }
3341
3342 fn type_name_to_variant_name(&self, type_name: &str) -> String {
3344 match type_name {
3346 "bool" => return "Boolean".to_string(),
3347 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3348 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3349 "f32" | "f64" => return "Number".to_string(),
3350 "String" => return "String".to_string(),
3351 "serde_json::Value" => return "Value".to_string(),
3352 "bytes::Bytes" => return "Binary".to_string(),
3356 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3357 "chrono::NaiveDate" => return "Date".to_string(),
3358 "chrono::NaiveTime" => return "Time".to_string(),
3359 "uuid::Uuid" => return "Uuid".to_string(),
3360 "url::Url" => return "Url".to_string(),
3361 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3362 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3363 _ => {}
3364 }
3365
3366 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3368 let inner = &type_name[4..type_name.len() - 1];
3369 if inner.starts_with("Vec<") && inner.ends_with(">") {
3371 let inner_inner = &inner[4..inner.len() - 1];
3372 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3373 }
3374 return format!("{}Array", self.type_name_to_variant_name(inner));
3375 }
3376
3377 let clean_name = type_name
3383 .trim_end_matches("Type")
3384 .trim_end_matches("Schema")
3385 .trim_end_matches("Item");
3386
3387 self.to_rust_type_name(clean_name)
3389 }
3390
3391 fn ensure_unique_variant_name_generator(
3393 &self,
3394 base_name: String,
3395 used_names: &mut std::collections::HashSet<String>,
3396 fallback_index: usize,
3397 ) -> String {
3398 if used_names.insert(base_name.clone()) {
3399 return base_name;
3400 }
3401
3402 for i in 2..100 {
3404 let numbered_name = format!("{base_name}{i}");
3405 if used_names.insert(numbered_name.clone()) {
3406 return numbered_name;
3407 }
3408 }
3409
3410 let fallback = format!("Variant{fallback_index}");
3412 used_names.insert(fallback.clone());
3413 fallback
3414 }
3415
3416 fn find_request_type_for_operation(
3418 &self,
3419 operation_id: &str,
3420 analysis: &SchemaAnalysis,
3421 ) -> Option<String> {
3422 analysis.operations.get(operation_id).and_then(|op| {
3424 op.request_body
3425 .as_ref()
3426 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3427 })
3428 }
3429
3430 fn resolve_streaming_event_type(
3432 &self,
3433 endpoint: &crate::streaming::StreamingEndpoint,
3434 analysis: &SchemaAnalysis,
3435 ) -> Result<String> {
3436 match &endpoint.event_flow {
3437 crate::streaming::EventFlow::Simple => {
3438 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3441 Ok(endpoint.event_union_type.to_string())
3442 } else {
3443 Err(crate::error::GeneratorError::ValidationError(format!(
3444 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3445 endpoint.event_union_type, endpoint.operation_id
3446 )))
3447 }
3448 }
3449 crate::streaming::EventFlow::StartDeltaStop { .. } => {
3450 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3453 Ok(endpoint.event_union_type.to_string())
3454 } else {
3455 Err(crate::error::GeneratorError::ValidationError(format!(
3456 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3457 endpoint.event_union_type, endpoint.operation_id
3458 )))
3459 }
3460 }
3461 }
3462 }
3463
3464 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3466 Ok(quote! {
3467 #[derive(Debug, thiserror::Error)]
3469 pub enum StreamingError {
3470 #[error("Connection error: {0}")]
3471 Connection(String),
3472 #[error("HTTP error: {status}")]
3473 Http { status: u16 },
3474 #[error("SSE parsing error: {0}")]
3475 Parsing(String),
3476 #[error("Authentication error: {0}")]
3477 Authentication(String),
3478 #[error("Rate limit error: {0}")]
3479 RateLimit(String),
3480 #[error("API error: {0}")]
3481 Api(String),
3482 #[error("Timeout error: {0}")]
3483 Timeout(String),
3484 #[error("JSON serialization/deserialization error: {0}")]
3485 Json(#[from] serde_json::Error),
3486 #[error("Request error: {0}")]
3487 Request(reqwest::Error),
3488 }
3489
3490 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3491 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3492 StreamingError::Api(format!("Invalid header value: {}", err))
3493 }
3494 }
3495
3496 impl From<reqwest::Error> for StreamingError {
3497 fn from(err: reqwest::Error) -> Self {
3498 if err.is_timeout() {
3499 StreamingError::Timeout(err.to_string())
3500 } else if err.is_status() {
3501 if let Some(status) = err.status() {
3502 StreamingError::Http { status: status.as_u16() }
3503 } else {
3504 StreamingError::Connection(err.to_string())
3505 }
3506 } else {
3507 StreamingError::Request(err)
3508 }
3509 }
3510 }
3511 })
3512 }
3513
3514 fn generate_endpoint_trait(
3516 &self,
3517 endpoint: &crate::streaming::StreamingEndpoint,
3518 analysis: &SchemaAnalysis,
3519 ) -> Result<TokenStream> {
3520 use crate::streaming::HttpMethod;
3521
3522 let trait_name = format_ident!(
3523 "{}StreamingClient",
3524 self.to_rust_type_name(&endpoint.operation_id)
3525 );
3526 let method_name =
3527 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3528 let event_type =
3529 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3530
3531 let method_signature = match endpoint.http_method {
3533 HttpMethod::Get => {
3534 let mut param_defs = Vec::new();
3536 for qp in &endpoint.query_parameters {
3537 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3538 if qp.required {
3539 param_defs.push(quote! { #param_name: &str });
3540 } else {
3541 param_defs.push(quote! { #param_name: Option<&str> });
3542 }
3543 }
3544 quote! {
3545 async fn #method_name(
3546 &self,
3547 #(#param_defs),*
3548 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3549 }
3550 }
3551 HttpMethod::Post => {
3552 let request_type = self
3554 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3555 .unwrap_or_else(|| "serde_json::Value".to_string());
3556 let request_type_ident = if request_type.contains("::") {
3557 let parts: Vec<&str> = request_type.split("::").collect();
3558 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3559 quote! { #(#path_parts)::* }
3560 } else {
3561 let ident = format_ident!("{}", request_type);
3562 quote! { #ident }
3563 };
3564 quote! {
3565 async fn #method_name(
3566 &self,
3567 request: #request_type_ident,
3568 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3569 }
3570 }
3571 };
3572
3573 Ok(quote! {
3574 #[async_trait]
3576 pub trait #trait_name {
3577 type Error: std::error::Error + Send + Sync + 'static;
3578
3579 #method_signature
3581 }
3582 })
3583 }
3584
3585 fn generate_streaming_client_impl(
3587 &self,
3588 streaming_config: &crate::streaming::StreamingConfig,
3589 analysis: &SchemaAnalysis,
3590 ) -> Result<TokenStream> {
3591 let client_name = format_ident!(
3592 "{}Client",
3593 self.to_rust_type_name(&streaming_config.client_module_name)
3594 );
3595
3596 let mut struct_fields = vec![
3599 quote! { base_url: String },
3600 quote! { api_key: Option<String> },
3601 quote! { http_client: reqwest::Client },
3602 quote! { custom_headers: std::collections::BTreeMap<String, String> },
3603 ];
3604
3605 let has_optional_headers = !streaming_config
3606 .endpoints
3607 .iter()
3608 .all(|e| e.optional_headers.is_empty());
3609
3610 if has_optional_headers {
3611 struct_fields
3612 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3613 }
3614
3615 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3618 streaming_config
3619 .endpoints
3620 .first()
3621 .and_then(|e| e.base_url.as_deref())
3622 .unwrap_or("https://api.example.com")
3623 } else {
3624 "https://api.example.com"
3625 };
3626
3627 let constructor_fields = if has_optional_headers {
3629 quote! {
3630 base_url: #default_base_url.to_string(),
3631 api_key: None,
3632 http_client: reqwest::Client::new(),
3633 custom_headers: std::collections::BTreeMap::new(),
3634 optional_headers: std::collections::BTreeMap::new(),
3635 }
3636 } else {
3637 quote! {
3638 base_url: #default_base_url.to_string(),
3639 api_key: None,
3640 http_client: reqwest::Client::new(),
3641 custom_headers: std::collections::BTreeMap::new(),
3642 }
3643 };
3644
3645 let optional_headers_method = if has_optional_headers {
3647 quote! {
3648 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3650 self.optional_headers = headers;
3651 }
3652 }
3653 } else {
3654 TokenStream::new()
3655 };
3656
3657 let constructor = quote! {
3658 impl #client_name {
3659 pub fn new() -> Self {
3661 Self {
3662 #constructor_fields
3663 }
3664 }
3665
3666 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3668 self.base_url = base_url.into();
3669 self
3670 }
3671
3672 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3674 self.api_key = Some(api_key.into());
3675 self
3676 }
3677
3678 pub fn with_header(
3680 mut self,
3681 name: impl Into<String>,
3682 value: impl Into<String>,
3683 ) -> Self {
3684 self.custom_headers.insert(name.into(), value.into());
3685 self
3686 }
3687
3688 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3690 self.http_client = client;
3691 self
3692 }
3693
3694 #optional_headers_method
3695 }
3696 };
3697
3698 let mut trait_impls = Vec::new();
3700 for endpoint in &streaming_config.endpoints {
3701 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3702 trait_impls.push(trait_impl);
3703 }
3704
3705 let default_impl = quote! {
3707 impl Default for #client_name {
3708 fn default() -> Self {
3709 Self::new()
3710 }
3711 }
3712 };
3713
3714 Ok(quote! {
3715 #[derive(Debug, Clone)]
3717 pub struct #client_name {
3718 #(#struct_fields,)*
3719 }
3720
3721 #constructor
3722
3723 #default_impl
3724
3725 #(#trait_impls)*
3726 })
3727 }
3728
3729 fn generate_endpoint_trait_impl(
3731 &self,
3732 endpoint: &crate::streaming::StreamingEndpoint,
3733 client_name: &proc_macro2::Ident,
3734 analysis: &SchemaAnalysis,
3735 ) -> Result<TokenStream> {
3736 use crate::streaming::HttpMethod;
3737
3738 let trait_name = format_ident!(
3739 "{}StreamingClient",
3740 self.to_rust_type_name(&endpoint.operation_id)
3741 );
3742 let method_name =
3743 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3744 let event_type =
3745 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3746
3747 let mut header_setup = Vec::new();
3749 for (name, value) in &endpoint.required_headers {
3750 header_setup.push(quote! {
3751 headers.insert(#name, HeaderValue::from_static(#value));
3752 });
3753 }
3754
3755 if let Some(auth_header) = &endpoint.auth_header {
3758 match auth_header {
3759 crate::streaming::AuthHeader::Bearer(header_name) => {
3760 header_setup.push(quote! {
3761 if let Some(ref api_key) = self.api_key {
3762 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3763 }
3764 });
3765 }
3766 crate::streaming::AuthHeader::ApiKey(header_name) => {
3767 header_setup.push(quote! {
3768 if let Some(ref api_key) = self.api_key {
3769 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3770 }
3771 });
3772 }
3773 }
3774 } else {
3775 header_setup.push(quote! {
3777 if let Some(ref api_key) = self.api_key {
3778 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3779 }
3780 });
3781 }
3782
3783 header_setup.push(quote! {
3785 for (name, value) in &self.custom_headers {
3786 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3787 headers.insert(header_name, header_value);
3788 }
3789 }
3790 });
3791
3792 if !endpoint.optional_headers.is_empty() {
3794 header_setup.push(quote! {
3795 for (key, value) in &self.optional_headers {
3796 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3797 headers.insert(header_name, header_value);
3798 }
3799 }
3800 });
3801 }
3802
3803 match endpoint.http_method {
3805 HttpMethod::Get => self.generate_get_streaming_impl(
3806 endpoint,
3807 client_name,
3808 &trait_name,
3809 &method_name,
3810 &event_type,
3811 &header_setup,
3812 ),
3813 HttpMethod::Post => self.generate_post_streaming_impl(
3814 endpoint,
3815 client_name,
3816 &trait_name,
3817 &method_name,
3818 &event_type,
3819 &header_setup,
3820 analysis,
3821 ),
3822 }
3823 }
3824
3825 fn generate_get_streaming_impl(
3827 &self,
3828 endpoint: &crate::streaming::StreamingEndpoint,
3829 client_name: &proc_macro2::Ident,
3830 trait_name: &proc_macro2::Ident,
3831 method_name: &proc_macro2::Ident,
3832 event_type: &proc_macro2::Ident,
3833 header_setup: &[TokenStream],
3834 ) -> Result<TokenStream> {
3835 let path = &endpoint.path;
3836
3837 let mut param_defs = Vec::new();
3839 let mut query_params = Vec::new();
3840
3841 for qp in &endpoint.query_parameters {
3842 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3843 let param_name_str = &qp.name;
3844
3845 if qp.required {
3846 param_defs.push(quote! { #param_name: &str });
3847 query_params.push(quote! {
3848 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3849 });
3850 } else {
3851 param_defs.push(quote! { #param_name: Option<&str> });
3852 query_params.push(quote! {
3853 if let Some(v) = #param_name {
3854 url.query_pairs_mut().append_pair(#param_name_str, v);
3855 }
3856 });
3857 }
3858 }
3859
3860 let url_construction = quote! {
3862 let base_url = url::Url::parse(&self.base_url)
3863 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3864 let path_to_join = #path.trim_start_matches('/');
3865 let mut url = base_url.join(path_to_join)
3866 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3867 #(#query_params)*
3868 };
3869
3870 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3871
3872 Ok(quote! {
3873 #[async_trait]
3874 impl #trait_name for #client_name {
3875 type Error = StreamingError;
3876
3877 #instrument_skip
3878 async fn #method_name(
3879 &self,
3880 #(#param_defs),*
3881 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3882 debug!("Starting streaming GET request");
3883
3884 let mut headers = HeaderMap::new();
3885 #(#header_setup)*
3886
3887 #url_construction
3888 let url_str = url.to_string();
3889 debug!("Making streaming GET request to: {}", url_str);
3890
3891 let request_builder = self.http_client
3892 .get(url_str)
3893 .headers(headers);
3894
3895 debug!("Creating SSE stream from request");
3896 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3897 info!("SSE stream created successfully");
3898 Ok(Box::pin(stream))
3899 }
3900 }
3901 })
3902 }
3903
3904 #[allow(clippy::too_many_arguments)]
3906 fn generate_post_streaming_impl(
3907 &self,
3908 endpoint: &crate::streaming::StreamingEndpoint,
3909 client_name: &proc_macro2::Ident,
3910 trait_name: &proc_macro2::Ident,
3911 method_name: &proc_macro2::Ident,
3912 event_type: &proc_macro2::Ident,
3913 header_setup: &[TokenStream],
3914 analysis: &SchemaAnalysis,
3915 ) -> Result<TokenStream> {
3916 let path = &endpoint.path;
3917
3918 let request_type = self
3920 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3921 .unwrap_or_else(|| "serde_json::Value".to_string());
3922 let request_type_ident = if request_type.contains("::") {
3923 let parts: Vec<&str> = request_type.split("::").collect();
3924 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3925 quote! { #(#path_parts)::* }
3926 } else {
3927 let ident = format_ident!("{}", request_type);
3928 quote! { #ident }
3929 };
3930
3931 let url_construction = quote! {
3933 let base_url = url::Url::parse(&self.base_url)
3934 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3935 let path_to_join = #path.trim_start_matches('/');
3936 let url = base_url.join(path_to_join)
3937 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3938 .to_string();
3939 };
3940
3941 let stream_param = &endpoint.stream_parameter;
3943 let stream_setup = if stream_param.is_empty() {
3944 quote! {
3945 let streaming_request = request;
3946 }
3947 } else {
3948 quote! {
3949 let mut streaming_request = request;
3951 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3952 if let Some(obj) = request_value.as_object_mut() {
3953 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3954 }
3955 streaming_request = serde_json::from_value(request_value)?;
3956 }
3957 }
3958 };
3959
3960 Ok(quote! {
3961 #[async_trait]
3962 impl #trait_name for #client_name {
3963 type Error = StreamingError;
3964
3965 #[instrument(skip(self, request), name = "streaming_post_request")]
3966 async fn #method_name(
3967 &self,
3968 request: #request_type_ident,
3969 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3970 debug!("Starting streaming POST request");
3971
3972 #stream_setup
3973
3974 let mut headers = HeaderMap::new();
3975 #(#header_setup)*
3976
3977 #url_construction
3978 debug!("Making streaming POST request to: {}", url);
3979
3980 let request_builder = self.http_client
3981 .post(&url)
3982 .headers(headers)
3983 .json(&streaming_request);
3984
3985 debug!("Creating SSE stream from request");
3986 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3987 info!("SSE stream created successfully");
3988 Ok(Box::pin(stream))
3989 }
3990 }
3991 })
3992 }
3993
3994 fn generate_sse_parser_utilities(
3996 &self,
3997 _streaming_config: &crate::streaming::StreamingConfig,
3998 ) -> Result<TokenStream> {
3999 Ok(quote! {
4000 pub async fn parse_sse_stream<T>(
4002 request_builder: reqwest::RequestBuilder
4003 ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
4004 where
4005 T: serde::de::DeserializeOwned + Send + 'static,
4006 {
4007 let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
4008 StreamingError::Connection(format!("Failed to create event source: {}", e))
4009 })?;
4010
4011 let stream = event_source.filter_map(|event_result| async move {
4012 match event_result {
4013 Ok(reqwest_eventsource::Event::Open) => {
4014 debug!("SSE connection opened");
4015 None
4016 }
4017 Ok(reqwest_eventsource::Event::Message(message)) => {
4018 if message.event == "ping" {
4020 debug!("Received SSE ping event, skipping");
4021 return None;
4022 }
4023
4024 if message.data.trim().is_empty() {
4026 debug!("Empty SSE data, skipping");
4027 return None;
4028 }
4029
4030 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
4032 if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
4033 if event_type == "ping" {
4034 debug!("Received ping event in JSON data, skipping");
4035 return None;
4036 }
4037 }
4038
4039 match serde_json::from_value::<T>(json_value) {
4041 Ok(parsed_event) => {
4042 Some(Ok(parsed_event))
4043 }
4044 Err(e) => {
4045 if message.data.contains("ping") || message.event.contains("ping") {
4046 debug!("Ignoring ping-related event: {}", message.data);
4047 None
4048 } else {
4049 Some(Err(StreamingError::Parsing(
4050 format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
4051 )))
4052 }
4053 }
4054 }
4055 } else {
4056 Some(Err(StreamingError::Parsing(
4058 format!("SSE event is not valid JSON: {}", message.data)
4059 )))
4060 }
4061 }
4062 Err(e) => {
4063 match e {
4065 reqwest_eventsource::Error::StreamEnded => {
4066 debug!("SSE stream completed normally");
4067 None }
4069 reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
4070 let status_code = status.as_u16();
4072
4073 let error_body = match response.text().await {
4075 Ok(body) => body,
4076 Err(_) => "Failed to read error response body".to_string()
4077 };
4078
4079 error!("SSE connection error - HTTP {}: {}", status_code, error_body);
4080
4081 let detailed_error = format!(
4082 "HTTP {} error: {}",
4083 status_code,
4084 error_body
4085 );
4086
4087 Some(Err(StreamingError::Connection(detailed_error)))
4088 }
4089 _ => {
4090 let error_str = e.to_string();
4091 if error_str.contains("stream closed") {
4092 debug!("SSE stream closed");
4093 None
4094 } else {
4095 error!("SSE connection error: {}", e);
4096 Some(Err(StreamingError::Connection(error_str)))
4097 }
4098 }
4099 }
4100 }
4101 }
4102 });
4103
4104 Ok(stream)
4105 }
4106 })
4107 }
4108
4109 fn generate_reconnection_utilities(
4111 &self,
4112 reconnect_config: &crate::streaming::ReconnectionConfig,
4113 ) -> Result<TokenStream> {
4114 let max_retries = reconnect_config.max_retries;
4115 let initial_delay = reconnect_config.initial_delay_ms;
4116 let max_delay = reconnect_config.max_delay_ms;
4117 let backoff_multiplier = reconnect_config.backoff_multiplier;
4118
4119 Ok(quote! {
4120 #[derive(Debug, Clone)]
4122 pub struct ReconnectionManager {
4123 max_retries: u32,
4124 initial_delay_ms: u64,
4125 max_delay_ms: u64,
4126 backoff_multiplier: f64,
4127 current_attempt: u32,
4128 }
4129
4130 impl ReconnectionManager {
4131 pub fn new() -> Self {
4133 Self {
4134 max_retries: #max_retries,
4135 initial_delay_ms: #initial_delay,
4136 max_delay_ms: #max_delay,
4137 backoff_multiplier: #backoff_multiplier,
4138 current_attempt: 0,
4139 }
4140 }
4141
4142 pub fn should_retry(&self) -> bool {
4144 self.current_attempt < self.max_retries
4145 }
4146
4147 pub fn next_retry_delay(&mut self) -> Duration {
4149 if !self.should_retry() {
4150 return Duration::from_secs(0);
4151 }
4152
4153 let delay_ms = (self.initial_delay_ms as f64
4154 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
4155 let delay_ms = delay_ms.min(self.max_delay_ms);
4156
4157 self.current_attempt += 1;
4158 Duration::from_millis(delay_ms)
4159 }
4160
4161 pub fn reset(&mut self) {
4163 self.current_attempt = 0;
4164 }
4165
4166 pub fn current_attempt(&self) -> u32 {
4168 self.current_attempt
4169 }
4170 }
4171
4172 impl Default for ReconnectionManager {
4173 fn default() -> Self {
4174 Self::new()
4175 }
4176 }
4177 })
4178 }
4179}