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)]
981 pub struct ApiError<E> {
982 pub status: u16,
983 pub headers: reqwest::header::HeaderMap,
984 pub body: String,
985 pub typed: Option<E>,
986 pub parse_error: Option<String>,
987 }
988
989 impl<E> ApiError<E> {
990 pub fn is_client_error(&self) -> bool {
991 (400..500).contains(&self.status)
992 }
993
994 pub fn is_server_error(&self) -> bool {
995 (500..600).contains(&self.status)
996 }
997
998 pub fn is_retryable(&self) -> bool {
1001 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1002 }
1003 }
1004
1005 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1006 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007 write!(f, "API error {}: {}", self.status, self.body)
1008 }
1009 }
1010
1011 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1012
1013 #[derive(Debug, Error)]
1021 pub enum ApiOpError<E: std::fmt::Debug> {
1022 #[error(transparent)]
1023 Transport(#[from] HttpError),
1024
1025 #[error(transparent)]
1026 Api(ApiError<E>),
1027 }
1028
1029 impl<E: std::fmt::Debug> ApiOpError<E> {
1030 pub fn api(&self) -> Option<&ApiError<E>> {
1032 match self {
1033 Self::Api(e) => Some(e),
1034 Self::Transport(_) => None,
1035 }
1036 }
1037
1038 pub fn is_api_error(&self) -> bool {
1041 matches!(self, Self::Api(_))
1042 }
1043 }
1044
1045 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1048 fn from(e: reqwest::Error) -> Self {
1049 Self::Transport(HttpError::Network(e))
1050 }
1051 }
1052
1053 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1054 fn from(e: reqwest_middleware::Error) -> Self {
1055 Self::Transport(HttpError::Middleware(e))
1056 }
1057 }
1058
1059 pub type HttpResult<T> = Result<T, HttpError>;
1063 }
1064 }
1065
1066 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1068 let mut module_names = std::collections::BTreeSet::new();
1069
1070 for file in files {
1071 let module_name = if file.path.components().count() > 1 {
1072 file.path.iter().next().and_then(|part| part.to_str())
1073 } else {
1074 file.path.file_stem().and_then(|stem| stem.to_str())
1075 };
1076 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1077 module_names.insert(module_name.to_string());
1078 }
1079 }
1080 let module_declarations = module_names
1081 .iter()
1082 .map(|name| format!("pub mod {name};"))
1083 .collect::<Vec<_>>();
1084 let pub_uses = module_names
1085 .iter()
1086 .map(|name| format!("pub use {name}::*;"))
1087 .collect::<Vec<_>>();
1088
1089 let mount_hint = format!(
1096 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1097 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1098 name = self.config.module_name,
1099 );
1100 let source_hint = self
1101 .source_provenance
1102 .as_ref()
1103 .map(|source| {
1104 format!(
1105 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1106 env!("CARGO_PKG_VERSION")
1107 )
1108 })
1109 .unwrap_or_default();
1110
1111 let content = format!(
1112 r#"//! Generated API modules
1113//!
1114//! This module exports all generated API types and clients.
1115//! Do not edit manually - regenerate using the appropriate script.
1116//!
1117{source_hint}
1118{mount_hint}
1119#![allow(unused_imports)]
1120
1121{decls}
1122
1123{uses}
1124"#,
1125 mount_hint = mount_hint,
1126 source_hint = source_hint,
1127 decls = module_declarations.join("\n"),
1128 uses = pub_uses.join("\n"),
1129 );
1130
1131 Ok(content)
1132 }
1133
1134 pub fn output_artifacts(
1136 &self,
1137 result: &GenerationResult,
1138 ) -> std::collections::BTreeMap<PathBuf, String> {
1139 let mut artifacts = std::collections::BTreeMap::new();
1140 for file in &result.files {
1141 artifacts.insert(file.path.clone(), file.content.clone());
1142 }
1143 artifacts.insert(
1144 result.mod_file.path.clone(),
1145 result.mod_file.content.clone(),
1146 );
1147 if let Some(mut fragment) =
1148 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1149 {
1150 if let Some(source) = &self.source_provenance {
1151 let header = format!(
1152 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1153 env!("CARGO_PKG_VERSION")
1154 );
1155 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1156 }
1157 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1158 }
1159 artifacts
1160 }
1161
1162 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1165 use std::fs;
1166
1167 fs::create_dir_all(&self.config.output_dir)?;
1169
1170 let artifacts = self.output_artifacts(result);
1171 for (relative, content) in &artifacts {
1172 let file_path = self.config.output_dir.join(relative);
1173 if let Some(parent) = file_path.parent() {
1174 fs::create_dir_all(parent)?;
1175 }
1176 fs::write(&file_path, content)?;
1177 }
1178
1179 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1180 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1181 {
1182 fs::remove_file(&deps_path)?;
1183 }
1184
1185 Ok(())
1186 }
1187
1188 fn generate_type_definition(
1189 &self,
1190 schema: &crate::analysis::AnalyzedSchema,
1191 analysis: &crate::analysis::SchemaAnalysis,
1192 type_context: &TypeGenerationContext<'_>,
1193 ) -> Result<TokenStream> {
1194 use crate::analysis::SchemaType;
1195
1196 match &schema.schema_type {
1197 SchemaType::Primitive { rust_type, .. } => {
1198 self.generate_type_alias(schema, rust_type)
1200 }
1201 SchemaType::StringEnum { values } => {
1202 let ext = analysis.enum_extensions.get(&schema.name);
1203 let rust_name = self.to_rust_type_name(&schema.name);
1210 let force_extensible = self
1211 .config
1212 .extensible_enum_overrides
1213 .get(&schema.name)
1214 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1215 .copied()
1216 .unwrap_or(false);
1217 if force_extensible {
1218 self.generate_extensible_enum(schema, values, ext)
1219 } else {
1220 self.generate_string_enum(schema, values, ext)
1221 }
1222 }
1223 SchemaType::ExtensibleEnum { known_values } => {
1224 let ext = analysis.enum_extensions.get(&schema.name);
1225 self.generate_extensible_enum(schema, known_values, ext)
1226 }
1227 SchemaType::Object {
1228 properties,
1229 required,
1230 additional_properties,
1231 } => self.generate_struct(
1232 schema,
1233 properties,
1234 required,
1235 additional_properties,
1236 analysis,
1237 type_context,
1238 ),
1239 SchemaType::DiscriminatedUnion {
1240 discriminator_field,
1241 variants,
1242 } => {
1243 if self.should_use_untagged_discriminated_union(schema, analysis) {
1245 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1247 .iter()
1248 .map(|v| crate::analysis::SchemaRef {
1249 target: v.type_name.clone(),
1250 nullable: false,
1251 })
1252 .collect();
1253 self.generate_union_enum(schema, &schema_refs, analysis)
1254 } else {
1255 self.generate_discriminated_enum(
1256 schema,
1257 discriminator_field,
1258 variants,
1259 analysis,
1260 )
1261 }
1262 }
1263 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1264 SchemaType::Reference { target } => {
1265 if schema.name != *target {
1268 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1270 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1271
1272 let doc_comment = if let Some(desc) = &schema.description {
1273 quote! { #[doc = #desc] }
1274 } else {
1275 TokenStream::new()
1276 };
1277
1278 Ok(quote! {
1279 #doc_comment
1280 pub type #alias_name = #target_type;
1281 })
1282 } else {
1283 Ok(TokenStream::new())
1285 }
1286 }
1287 SchemaType::Array { item_type } => {
1288 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1296
1297 if let SchemaType::Reference { target } = item_type.as_ref() {
1299 if let Some(info) = type_context.discriminated_variants.get(target) {
1300 if !info.is_parent_untagged {
1301 let wrapper_name =
1303 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1304 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1305 let disc_field = &info.discriminator_field;
1306 let disc_value = &info.discriminator_value;
1307
1308 let doc_comment = if let Some(desc) = &schema.description {
1309 quote! { #[doc = #desc] }
1310 } else {
1311 TokenStream::new()
1312 };
1313
1314 return Ok(quote! {
1315 #[derive(Debug, Clone, Deserialize, Serialize)]
1319 #[serde(tag = #disc_field)]
1320 pub enum #wrapper_name {
1321 #[serde(rename = #disc_value)]
1322 #variant_type(#variant_type),
1323 }
1324 #doc_comment
1325 pub type #array_name = Vec<#wrapper_name>;
1326 });
1327 }
1328 }
1329 }
1330
1331 let inner_type = self.generate_array_item_type(item_type, analysis);
1332
1333 let doc_comment = if let Some(desc) = &schema.description {
1334 quote! { #[doc = #desc] }
1335 } else {
1336 TokenStream::new()
1337 };
1338
1339 Ok(quote! {
1340 #doc_comment
1341 pub type #array_name = Vec<#inner_type>;
1342 })
1343 }
1344 SchemaType::Composition { schemas } => {
1345 self.generate_composition_struct(schema, schemas)
1346 }
1347 }
1348 }
1349
1350 fn generate_type_alias(
1351 &self,
1352 schema: &crate::analysis::AnalyzedSchema,
1353 rust_type: &str,
1354 ) -> Result<TokenStream> {
1355 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1356 let base_type = parse_rust_type(rust_type)?;
1360
1361 let doc_comment = if let Some(desc) = &schema.description {
1362 let sanitized_desc = self.sanitize_doc_comment(desc);
1363 quote! { #[doc = #sanitized_desc] }
1364 } else {
1365 TokenStream::new()
1366 };
1367
1368 Ok(quote! {
1369 #doc_comment
1370 pub type #type_name = #base_type;
1371 })
1372 }
1373
1374 fn generate_extensible_enum(
1375 &self,
1376 schema: &crate::analysis::AnalyzedSchema,
1377 known_values: &[String],
1378 ext: Option<&crate::analysis::EnumExtensions>,
1379 ) -> Result<TokenStream> {
1380 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1381
1382 let doc_comment = if let Some(desc) = &schema.description {
1383 quote! { #[doc = #desc] }
1384 } else {
1385 TokenStream::new()
1386 };
1387
1388 let varnames_override: Option<&Vec<String>> = ext
1392 .filter(|_| self.config.types.x_enum_varnames_enabled())
1393 .map(|e| &e.varnames)
1394 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1395 let descriptions_override: Option<&Vec<String>> = ext
1396 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1397 .map(|e| &e.descriptions)
1398 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1399
1400 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1401 let name = match varnames_override {
1402 Some(v) => v[index].clone(),
1403 None => self.to_rust_enum_variant(value),
1404 };
1405 format_ident!("{}", name)
1406 };
1407
1408 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1413 let variant_ident = variant_ident_for(i, value);
1414 let doc = descriptions_override
1415 .map(|d| {
1416 let s = self.sanitize_doc_comment(&d[i]);
1417 quote! { #[doc = #s] }
1418 })
1419 .unwrap_or_default();
1420 quote! {
1421 #doc
1422 #variant_ident,
1423 }
1424 });
1425
1426 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1427 let variant_ident = variant_ident_for(i, value);
1428 quote! {
1429 #value => Ok(#enum_name::#variant_ident),
1430 }
1431 });
1432
1433 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1434 let variant_ident = variant_ident_for(i, value);
1435 quote! {
1436 #enum_name::#variant_ident => #value,
1437 }
1438 });
1439
1440 let derives = if self.config.enable_specta {
1441 quote! {
1442 #[derive(Debug, Clone, PartialEq, Eq)]
1443 #[cfg_attr(feature = "specta", derive(specta::Type))]
1444 }
1445 } else {
1446 quote! {
1447 #[derive(Debug, Clone, PartialEq, Eq)]
1448 }
1449 };
1450
1451 Ok(quote! {
1452 #doc_comment
1453 #derives
1454 pub enum #enum_name {
1455 #(#known_variants)*
1456 Custom(String),
1458 }
1459
1460 impl<'de> serde::Deserialize<'de> for #enum_name {
1461 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1462 where
1463 D: serde::Deserializer<'de>,
1464 {
1465 let value = String::deserialize(deserializer)?;
1466 match value.as_str() {
1467 #(#match_arms_de)*
1468 _ => Ok(#enum_name::Custom(value)),
1469 }
1470 }
1471 }
1472
1473 impl serde::Serialize for #enum_name {
1474 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1475 where
1476 S: serde::Serializer,
1477 {
1478 let value = match self {
1479 #(#match_arms_ser)*
1480 #enum_name::Custom(s) => s.as_str(),
1481 };
1482 serializer.serialize_str(value)
1483 }
1484 }
1485 })
1486 }
1487
1488 fn generate_string_enum(
1489 &self,
1490 schema: &crate::analysis::AnalyzedSchema,
1491 values: &[String],
1492 ext: Option<&crate::analysis::EnumExtensions>,
1493 ) -> Result<TokenStream> {
1494 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1495
1496 let default_value = schema
1503 .default
1504 .as_ref()
1505 .and_then(|v| v.as_str())
1506 .map(|s| s.to_string());
1507 let has_default_match = match &default_value {
1508 Some(d) => values.iter().any(|v| v == d),
1509 None => !values.is_empty(),
1510 };
1511
1512 let varnames_override: Option<&Vec<String>> = ext
1516 .filter(|_| self.config.types.x_enum_varnames_enabled())
1517 .map(|e| &e.varnames)
1518 .filter(|v| !v.is_empty() && v.len() == values.len());
1519 let descriptions_override: Option<&Vec<String>> = ext
1520 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1521 .map(|e| &e.descriptions)
1522 .filter(|v| !v.is_empty() && v.len() == values.len());
1523
1524 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1531 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1532 .iter()
1533 .enumerate()
1534 .map(|(i, value)| {
1535 let base = match varnames_override {
1536 Some(v) => v[i].clone(),
1537 None => self.to_rust_enum_variant(value),
1538 };
1539 let mut variant_name = base.clone();
1540 let mut suffix = 2;
1541 while !used.insert(variant_name.clone()) {
1542 variant_name = format!("{base}_{suffix}");
1543 suffix += 1;
1544 }
1545 let variant_ident = format_ident!("{}", variant_name);
1546 let is_default = if let Some(ref default) = default_value {
1547 value == default
1548 } else {
1549 i == 0
1550 };
1551 let description = descriptions_override.map(|d| d[i].clone());
1552 (variant_ident, value, is_default, description)
1553 })
1554 .collect();
1555
1556 let variants =
1557 variant_pairs
1558 .iter()
1559 .map(|(variant_ident, value, is_default, description)| {
1560 let doc = description
1561 .as_ref()
1562 .map(|d| {
1563 let s = self.sanitize_doc_comment(d);
1564 quote! { #[doc = #s] }
1565 })
1566 .unwrap_or_default();
1567 if *is_default {
1568 quote! {
1569 #doc
1570 #[default]
1571 #[serde(rename = #value)]
1572 #variant_ident,
1573 }
1574 } else {
1575 quote! {
1576 #doc
1577 #[serde(rename = #value)]
1578 #variant_ident,
1579 }
1580 }
1581 });
1582
1583 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1587 quote! { Self::#variant_ident => #value, }
1588 });
1589
1590 let doc_comment = if let Some(desc) = &schema.description {
1591 quote! { #[doc = #desc] }
1592 } else {
1593 TokenStream::new()
1594 };
1595
1596 let derives = match (self.config.enable_specta, has_default_match) {
1599 (true, true) => quote! {
1600 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1601 #[cfg_attr(feature = "specta", derive(specta::Type))]
1602 },
1603 (true, false) => quote! {
1604 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1605 #[cfg_attr(feature = "specta", derive(specta::Type))]
1606 },
1607 (false, true) => quote! {
1608 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1609 },
1610 (false, false) => quote! {
1611 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1612 },
1613 };
1614
1615 Ok(quote! {
1616 #doc_comment
1617 #derives
1618 pub enum #enum_name {
1619 #(#variants)*
1620 }
1621
1622 impl #enum_name {
1623 pub fn as_str(&self) -> &'static str {
1624 match self {
1625 #(#as_str_arms)*
1626 }
1627 }
1628 }
1629
1630 impl ::std::fmt::Display for #enum_name {
1631 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1632 f.write_str(self.as_str())
1633 }
1634 }
1635
1636 impl AsRef<str> for #enum_name {
1637 fn as_ref(&self) -> &str {
1638 self.as_str()
1639 }
1640 }
1641 })
1642 }
1643
1644 fn generate_struct(
1645 &self,
1646 schema: &crate::analysis::AnalyzedSchema,
1647 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1648 required: &std::collections::HashSet<String>,
1649 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1650 analysis: &crate::analysis::SchemaAnalysis,
1651 type_context: &TypeGenerationContext<'_>,
1652 ) -> Result<TokenStream> {
1653 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1654 let emitted_properties = self.emitted_object_properties(
1655 &schema.name,
1656 properties,
1657 required,
1658 additional_properties,
1659 analysis,
1660 type_context.discriminated_variants.get(&schema.name),
1661 );
1662
1663 let mut fields: Vec<TokenStream> = emitted_properties
1664 .iter()
1665 .map(|emitted| {
1666 let field_name = emitted.wire_name;
1667 let property = emitted.property;
1668 let field_ident = &emitted.ident;
1669 let field_type = &emitted.field_type;
1670 let serde_attrs = self.generate_serde_field_attrs(
1671 &schema.name,
1672 field_name,
1673 field_ident,
1674 property,
1675 emitted.is_required,
1676 analysis,
1677 );
1678 let specta_attrs = self.generate_specta_field_attrs(field_name);
1679
1680 let doc_comment = if let Some(desc) = &property.description {
1681 let sanitized_desc = self.sanitize_doc_comment(desc);
1682 quote! { #[doc = #sanitized_desc] }
1683 } else {
1684 TokenStream::new()
1685 };
1686 let constraint_doc = self.generate_constraint_doc(&property.constraints);
1687
1688 quote! {
1689 #doc_comment
1690 #constraint_doc
1691 #serde_attrs
1692 #specta_attrs
1693 pub #field_ident: #field_type,
1694 }
1695 })
1696 .collect();
1697
1698 match additional_properties {
1704 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1705 crate::analysis::ObjectAdditionalProperties::Untyped => {
1706 fields.push(quote! {
1707 #[serde(flatten)]
1709 pub additional_properties:
1710 std::collections::BTreeMap<String, serde_json::Value>,
1711 });
1712 }
1713 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1714 let value_tokens = self.generate_array_item_type(value_type, analysis);
1715 fields.push(quote! {
1716 #[serde(flatten)]
1719 pub additional_properties:
1720 std::collections::BTreeMap<String, #value_tokens>,
1721 });
1722 }
1723 }
1724
1725 let doc_comment = if let Some(desc) = &schema.description {
1726 quote! { #[doc = #desc] }
1727 } else {
1728 TokenStream::new()
1729 };
1730
1731 let can_derive_default = emitted_properties
1737 .iter()
1738 .all(|property| !property.is_required);
1739
1740 let derives = match (self.config.enable_specta, can_derive_default) {
1744 (true, true) => quote! {
1745 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1746 #[cfg_attr(feature = "specta", derive(specta::Type))]
1747 },
1748 (true, false) => quote! {
1749 #[derive(Debug, Clone, Deserialize, Serialize)]
1750 #[cfg_attr(feature = "specta", derive(specta::Type))]
1751 },
1752 (false, true) => quote! {
1753 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1754 },
1755 (false, false) => quote! {
1756 #[derive(Debug, Clone, Deserialize, Serialize)]
1757 },
1758 };
1759
1760 let builder = if type_context.index.request_body_roots.contains(&schema.name)
1761 && emitted_properties
1762 .iter()
1763 .any(|property| property.is_required)
1764 && (emitted_properties
1765 .iter()
1766 .any(|property| !property.is_required)
1767 || !matches!(
1768 additional_properties,
1769 crate::analysis::ObjectAdditionalProperties::Forbidden
1770 )) {
1771 self.generate_request_model_builder(
1772 schema,
1773 &emitted_properties,
1774 additional_properties,
1775 analysis,
1776 type_context.index,
1777 )
1778 } else {
1779 TokenStream::new()
1780 };
1781
1782 Ok(quote! {
1783 #doc_comment
1784 #derives
1785 pub struct #struct_name {
1786 #(#fields)*
1787 }
1788
1789 #builder
1790 })
1791 }
1792
1793 pub(crate) fn emitted_object_properties<'a>(
1798 &self,
1799 schema_name: &str,
1800 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
1801 required: &std::collections::HashSet<String>,
1802 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1803 analysis: &crate::analysis::SchemaAnalysis,
1804 discriminator_info: Option<&DiscriminatedVariantInfo>,
1805 ) -> Vec<EmittedObjectProperty<'a>> {
1806 let mut sorted_properties: Vec<_> = properties.iter().collect();
1807 sorted_properties.sort_by_key(|(name, _)| name.as_str());
1808
1809 let mut used_field_idents = std::collections::HashSet::new();
1810 if !matches!(
1811 additional_properties,
1812 crate::analysis::ObjectAdditionalProperties::Forbidden
1813 ) {
1814 used_field_idents.insert("additional_properties".to_string());
1815 }
1816
1817 let mut emitted = Vec::new();
1818 for (field_name, property) in sorted_properties {
1819 if discriminator_info.is_some_and(|info| {
1820 !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
1821 }) {
1822 continue;
1823 }
1824
1825 let raw = self.to_rust_field_name(field_name);
1826 let mut chosen = raw.clone();
1827 let mut suffix = 2;
1828 while !used_field_idents.insert(chosen.clone()) {
1829 chosen = format!("{raw}_{suffix}");
1830 suffix += 1;
1831 }
1832 let is_required = required.contains(field_name);
1833 emitted.push(EmittedObjectProperty {
1834 wire_name: field_name,
1835 property,
1836 ident: Self::to_field_ident(&chosen),
1837 is_required,
1838 field_type: self.generate_field_type(
1839 schema_name,
1840 field_name,
1841 property,
1842 is_required,
1843 analysis,
1844 ),
1845 });
1846 }
1847 emitted
1848 }
1849
1850 fn type_generation_index(
1851 &self,
1852 analysis: &crate::analysis::SchemaAnalysis,
1853 ) -> TypeGenerationIndex {
1854 let reserved_type_names = analysis
1855 .schemas
1856 .keys()
1857 .map(|name| self.to_rust_type_name(name))
1858 .collect();
1859 let mut request_body_roots = std::collections::HashSet::new();
1860 for operation in analysis.operations.values() {
1861 let Some(mut current) = operation
1862 .request_body
1863 .as_ref()
1864 .and_then(crate::analysis::RequestBodyContent::schema_name)
1865 else {
1866 continue;
1867 };
1868 while request_body_roots.insert(current.to_string()) {
1869 let Some(crate::analysis::AnalyzedSchema {
1870 schema_type: crate::analysis::SchemaType::Reference { target },
1871 ..
1872 }) = analysis.schemas.get(current)
1873 else {
1874 break;
1875 };
1876 current = target;
1877 }
1878 }
1879 TypeGenerationIndex {
1880 request_body_roots,
1881 reserved_type_names,
1882 }
1883 }
1884
1885 fn generate_request_model_builder(
1886 &self,
1887 schema: &crate::analysis::AnalyzedSchema,
1888 properties: &[EmittedObjectProperty<'_>],
1889 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1890 analysis: &crate::analysis::SchemaAnalysis,
1891 type_index: &TypeGenerationIndex,
1892 ) -> TokenStream {
1893 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1894 let builder_base = format!("{}Builder", struct_name);
1895 let mut builder_name = builder_base.clone();
1896 let mut suffix = 2;
1897 while type_index.reserved_type_names.contains(&builder_name) {
1898 builder_name = format!("{builder_base}{suffix}");
1899 suffix += 1;
1900 }
1901 let builder_name = format_ident!("{builder_name}");
1902
1903 let required_parameters: Vec<TokenStream> = properties
1904 .iter()
1905 .filter(|property| property.is_required)
1906 .map(|property| {
1907 let ident = &property.ident;
1908 let field_type = &property.field_type;
1909 quote! { #ident: #field_type }
1910 })
1911 .collect();
1912 let required_idents: Vec<&syn::Ident> = properties
1913 .iter()
1914 .filter(|property| property.is_required)
1915 .map(|property| &property.ident)
1916 .collect();
1917 let optional_initializers: Vec<TokenStream> = properties
1918 .iter()
1919 .filter(|property| !property.is_required)
1920 .map(|property| {
1921 let ident = &property.ident;
1922 quote! { #ident: None }
1923 })
1924 .collect();
1925
1926 let additional_initializer = match additional_properties {
1927 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
1928 crate::analysis::ObjectAdditionalProperties::Untyped
1929 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
1930 additional_properties: ::std::collections::BTreeMap::new(),
1931 },
1932 };
1933
1934 let mut used_builder_methods =
1935 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
1936 if !matches!(
1937 additional_properties,
1938 crate::analysis::ObjectAdditionalProperties::Forbidden
1939 ) {
1940 used_builder_methods.insert("additional_properties".to_string());
1941 }
1942 let optional_setters: Vec<TokenStream> = properties
1943 .iter()
1944 .filter(|property| !property.is_required)
1945 .map(|property| {
1946 let field_ident = &property.ident;
1947 let field_type = self.generate_property_base_type(
1948 &schema.name,
1949 property.wire_name,
1950 property.property,
1951 analysis,
1952 );
1953 let field_name = field_ident.to_string();
1957 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
1958 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
1959 format!("with_{plain_field_name}")
1960 } else {
1961 field_name.clone()
1962 };
1963 let setter_base = setter_name.clone();
1964 let mut suffix = 2;
1965 while !used_builder_methods.insert(setter_name.clone()) {
1966 setter_name = format!("{setter_base}_{suffix}");
1967 suffix += 1;
1968 }
1969 let setter_ident = Self::to_field_ident(&setter_name);
1970 let wire_name = property.wire_name;
1971 quote! {
1972 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
1973 #[must_use]
1974 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
1975 self.value.#field_ident = Some(#field_ident);
1976 self
1977 }
1978 }
1979 })
1980 .collect();
1981
1982 let additional_setter = match additional_properties {
1983 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
1984 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
1985 #[must_use]
1987 pub fn additional_properties(
1988 mut self,
1989 additional_properties: ::std::collections::BTreeMap<
1990 String,
1991 serde_json::Value,
1992 >,
1993 ) -> Self {
1994 self.value.additional_properties = additional_properties;
1995 self
1996 }
1997 },
1998 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1999 let value_type = self.generate_array_item_type(value_type, analysis);
2000 quote! {
2001 #[must_use]
2003 pub fn additional_properties(
2004 mut self,
2005 additional_properties: ::std::collections::BTreeMap<
2006 String,
2007 #value_type,
2008 >,
2009 ) -> Self {
2010 self.value.additional_properties = additional_properties;
2011 self
2012 }
2013 }
2014 }
2015 };
2016
2017 quote! {
2018 impl #struct_name {
2019 pub fn new(#(#required_parameters),*) -> Self {
2021 Self {
2022 #(#required_idents,)*
2023 #(#optional_initializers,)*
2024 #additional_initializer
2025 }
2026 }
2027
2028 pub fn builder(#(#required_parameters),*) -> #builder_name {
2030 #builder_name::new(#(#required_idents),*)
2031 }
2032 }
2033
2034 #[derive(Debug, Clone)]
2036 #[must_use]
2037 pub struct #builder_name {
2038 value: #struct_name,
2039 }
2040
2041 impl #builder_name {
2042 pub fn new(#(#required_parameters),*) -> Self {
2044 Self {
2045 value: #struct_name::new(#(#required_idents),*),
2046 }
2047 }
2048
2049 #(#optional_setters)*
2050 #additional_setter
2051
2052 pub fn build(self) -> #struct_name {
2054 self.value
2055 }
2056 }
2057 }
2058 }
2059
2060 fn generate_discriminated_enum(
2061 &self,
2062 schema: &crate::analysis::AnalyzedSchema,
2063 discriminator_field: &str,
2064 variants: &[crate::analysis::UnionVariant],
2065 analysis: &crate::analysis::SchemaAnalysis,
2066 ) -> Result<TokenStream> {
2067 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2068
2069 let has_nested_discriminated_union = variants.iter().any(|variant| {
2071 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2072 matches!(
2073 variant_schema.schema_type,
2074 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2075 )
2076 } else {
2077 false
2078 }
2079 });
2080
2081 if has_nested_discriminated_union {
2083 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2085 .iter()
2086 .map(|v| crate::analysis::SchemaRef {
2087 target: v.type_name.clone(),
2088 nullable: false,
2089 })
2090 .collect();
2091 return self.generate_union_enum(schema, &schema_refs, analysis);
2092 }
2093
2094 let enclosing = self.to_rust_type_name(&schema.name);
2095 let enum_variants = variants.iter().map(|variant| {
2096 let variant_name = format_ident!("{}", variant.rust_name);
2097 let variant_value = &variant.discriminator_value;
2098
2099 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2100 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2104 || analysis
2105 .dependencies
2106 .recursive_schemas
2107 .contains(&variant.type_name)
2108 {
2109 quote! { Box<#variant_type> }
2110 } else {
2111 quote! { #variant_type }
2112 };
2113 quote! {
2114 #[serde(rename = #variant_value)]
2115 #variant_name(#payload),
2116 }
2117 });
2118
2119 let doc_comment = if let Some(desc) = &schema.description {
2120 quote! { #[doc = #desc] }
2121 } else {
2122 TokenStream::new()
2123 };
2124
2125 let derives = if self.config.enable_specta {
2127 quote! {
2128 #[derive(Debug, Clone, Deserialize, Serialize)]
2129 #[cfg_attr(feature = "specta", derive(specta::Type))]
2130 #[serde(tag = #discriminator_field)]
2131 }
2132 } else {
2133 quote! {
2134 #[derive(Debug, Clone, Deserialize, Serialize)]
2135 #[serde(tag = #discriminator_field)]
2136 }
2137 };
2138
2139 Ok(quote! {
2140 #doc_comment
2141 #derives
2142 pub enum #enum_name {
2143 #(#enum_variants)*
2144 }
2145 })
2146 }
2147
2148 fn should_use_untagged_discriminated_union(
2150 &self,
2151 schema: &crate::analysis::AnalyzedSchema,
2152 analysis: &crate::analysis::SchemaAnalysis,
2153 ) -> bool {
2154 for other_schema in analysis.schemas.values() {
2159 if let crate::analysis::SchemaType::DiscriminatedUnion {
2160 variants,
2161 discriminator_field: _,
2162 } = &other_schema.schema_type
2163 {
2164 for variant in variants {
2165 if variant.type_name == schema.name {
2166 if let crate::analysis::SchemaType::DiscriminatedUnion {
2171 discriminator_field: current_discriminator,
2172 variants: current_variants,
2173 ..
2174 } = &schema.schema_type
2175 {
2176 for current_variant in current_variants {
2178 if let Some(variant_schema) =
2179 analysis.schemas.get(¤t_variant.type_name)
2180 {
2181 if let crate::analysis::SchemaType::Object {
2182 properties, ..
2183 } = &variant_schema.schema_type
2184 {
2185 if properties.contains_key(current_discriminator) {
2186 return false;
2189 }
2190 }
2191 }
2192 }
2193 }
2194
2195 return true;
2197 }
2198 }
2199 }
2200 }
2201 false
2202 }
2203
2204 fn generate_union_enum(
2205 &self,
2206 schema: &crate::analysis::AnalyzedSchema,
2207 variants: &[crate::analysis::SchemaRef],
2208 analysis: &crate::analysis::SchemaAnalysis,
2209 ) -> Result<TokenStream> {
2210 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2211
2212 let mut used_variant_names = std::collections::HashSet::new();
2214 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2215 let base_variant_name = self.type_name_to_variant_name(&variant.target);
2217 let variant_name = self.ensure_unique_variant_name_generator(
2218 base_variant_name,
2219 &mut used_variant_names,
2220 i,
2221 );
2222 let variant_name_ident = format_ident!("{}", variant_name);
2223
2224 let variant_type_tokens = if matches!(
2226 variant.target.as_str(),
2227 "bool"
2228 | "i8"
2229 | "i16"
2230 | "i32"
2231 | "i64"
2232 | "i128"
2233 | "u8"
2234 | "u16"
2235 | "u32"
2236 | "u64"
2237 | "u128"
2238 | "f32"
2239 | "f64"
2240 | "String"
2241 ) {
2242 let type_ident = format_ident!("{}", variant.target);
2243 quote! { #type_ident }
2244 } else if variant.target == "serde_json::Value" {
2245 quote! { serde_json::Value }
2248 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2249 let inner = &variant.target[4..variant.target.len() - 1];
2251
2252 if inner.starts_with("Vec<") && inner.ends_with(">") {
2254 let inner_inner = &inner[4..inner.len() - 1];
2255 if inner_inner == "serde_json::Value" {
2256 quote! { Vec<Vec<serde_json::Value>> }
2257 } else {
2258 let inner_inner_type = if matches!(
2259 inner_inner,
2260 "bool"
2261 | "i8"
2262 | "i16"
2263 | "i32"
2264 | "i64"
2265 | "i128"
2266 | "u8"
2267 | "u16"
2268 | "u32"
2269 | "u64"
2270 | "u128"
2271 | "f32"
2272 | "f64"
2273 | "String"
2274 ) {
2275 format_ident!("{}", inner_inner)
2276 } else {
2277 format_ident!("{}", self.to_rust_type_name(inner_inner))
2278 };
2279 quote! { Vec<Vec<#inner_inner_type>> }
2280 }
2281 } else if inner == "serde_json::Value" {
2282 quote! { Vec<serde_json::Value> }
2283 } else {
2284 let inner_type = if matches!(
2285 inner,
2286 "bool"
2287 | "i8"
2288 | "i16"
2289 | "i32"
2290 | "i64"
2291 | "i128"
2292 | "u8"
2293 | "u16"
2294 | "u32"
2295 | "u64"
2296 | "u128"
2297 | "f32"
2298 | "f64"
2299 | "String"
2300 ) {
2301 format_ident!("{}", inner)
2302 } else {
2303 format_ident!("{}", self.to_rust_type_name(inner))
2304 };
2305 quote! { Vec<#inner_type> }
2306 }
2307 } else if variant.target.contains("::") || variant.target.contains('<') {
2308 parse_rust_type(&variant.target).unwrap_or_else(|_| {
2313 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2314 quote! { #fallback }
2315 })
2316 } else {
2317 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2318 quote! { #type_ident }
2319 };
2320
2321 let target_rust_name = self.to_rust_type_name(&variant.target);
2325 let enclosing_name = self.to_rust_type_name(&schema.name);
2326 let is_self_ref = target_rust_name == enclosing_name;
2327 let is_recursive_target = analysis
2331 .dependencies
2332 .recursive_schemas
2333 .contains(&variant.target);
2334 let variant_type_tokens = if is_self_ref || is_recursive_target {
2335 quote! { Box<#variant_type_tokens> }
2336 } else {
2337 variant_type_tokens
2338 };
2339
2340 quote! {
2341 #variant_name_ident(#variant_type_tokens),
2342 }
2343 });
2344
2345 let doc_comment = if let Some(desc) = &schema.description {
2346 quote! { #[doc = #desc] }
2347 } else {
2348 TokenStream::new()
2349 };
2350
2351 let derives = if self.config.enable_specta {
2353 quote! {
2354 #[derive(Debug, Clone, Deserialize, Serialize)]
2355 #[cfg_attr(feature = "specta", derive(specta::Type))]
2356 #[serde(untagged)]
2357 }
2358 } else {
2359 quote! {
2360 #[derive(Debug, Clone, Deserialize, Serialize)]
2361 #[serde(untagged)]
2362 }
2363 };
2364
2365 Ok(quote! {
2366 #doc_comment
2367 #derives
2368 pub enum #enum_name {
2369 #(#enum_variants)*
2370 }
2371 })
2372 }
2373
2374 fn target_aliases_back_to(
2379 &self,
2380 target: &str,
2381 enclosing_rust_name: &str,
2382 analysis: &crate::analysis::SchemaAnalysis,
2383 ) -> bool {
2384 let mut current = target.to_string();
2385 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2386 for _ in 0..16 {
2387 if !visited.insert(current.clone()) {
2388 return true;
2389 }
2390 let Some(schema) = analysis.schemas.get(¤t) else {
2391 return false;
2392 };
2393 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2394 if self.to_rust_type_name(next) == enclosing_rust_name {
2395 return true;
2396 }
2397 current = next.clone();
2398 continue;
2399 }
2400 return false;
2401 }
2402 false
2403 }
2404
2405 fn generate_field_type(
2406 &self,
2407 schema_name: &str,
2408 field_name: &str,
2409 prop: &crate::analysis::PropertyInfo,
2410 is_required: bool,
2411 analysis: &crate::analysis::SchemaAnalysis,
2412 ) -> TokenStream {
2413 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2414
2415 if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2416 quote! { Option<#base_type> }
2417 } else {
2418 base_type
2419 }
2420 }
2421
2422 fn property_is_option_wrapped(
2423 &self,
2424 schema_name: &str,
2425 field_name: &str,
2426 prop: &crate::analysis::PropertyInfo,
2427 is_required: bool,
2428 analysis: &crate::analysis::SchemaAnalysis,
2429 ) -> bool {
2430 let override_key = format!("{schema_name}.{field_name}");
2431 let is_nullable_override = self
2432 .config
2433 .nullable_field_overrides
2434 .get(&override_key)
2435 .copied()
2436 .unwrap_or(false);
2437
2438 !is_required
2439 || prop.nullable
2440 || is_nullable_override
2441 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2442 }
2443
2444 pub(crate) fn generate_property_base_type(
2445 &self,
2446 schema_name: &str,
2447 _field_name: &str,
2448 prop: &crate::analysis::PropertyInfo,
2449 analysis: &crate::analysis::SchemaAnalysis,
2450 ) -> TokenStream {
2451 use crate::analysis::SchemaType;
2452
2453 match &prop.schema_type {
2454 SchemaType::Primitive { rust_type, .. } => {
2455 parse_rust_type(rust_type).unwrap_or_else(|_| {
2458 eprintln!(
2463 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
2464 falling back to String"
2465 );
2466 quote! { String }
2467 })
2468 }
2469 SchemaType::Reference { target } => {
2470 let target_rust_name = self.to_rust_type_name(target);
2471 let target_type = format_ident!("{}", target_rust_name);
2472 let enclosing_rust_name = self.to_rust_type_name(schema_name);
2485 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2486 let is_alias_chain_self =
2487 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2488 if analysis.dependencies.recursive_schemas.contains(target)
2489 || is_self_via_rust_name
2490 || is_alias_chain_self
2491 {
2492 quote! { Box<#target_type> }
2493 } else {
2494 quote! { #target_type }
2495 }
2496 }
2497 SchemaType::Array { item_type } => {
2498 let inner_type = self.generate_array_item_type(item_type, analysis);
2499 quote! { Vec<#inner_type> }
2500 }
2501 _ => {
2502 quote! { serde_json::Value }
2504 }
2505 }
2506 }
2507
2508 fn generate_serde_field_attrs(
2509 &self,
2510 schema_name: &str,
2511 field_name: &str,
2512 field_ident: &syn::Ident,
2513 prop: &crate::analysis::PropertyInfo,
2514 is_required: bool,
2515 analysis: &crate::analysis::SchemaAnalysis,
2516 ) -> TokenStream {
2517 let mut attrs = Vec::new();
2518
2519 let rust_field_name = field_ident.to_string();
2522 let comparison_name = rust_field_name
2523 .strip_prefix("r#")
2524 .unwrap_or(&rust_field_name);
2525 if comparison_name != field_name {
2526 attrs.push(quote! { rename = #field_name });
2527 }
2528
2529 if !is_required || prop.nullable {
2531 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2532 }
2533
2534 if prop.default.is_some()
2538 && (is_required && !prop.nullable)
2539 && !self.type_lacks_default(&prop.schema_type, analysis)
2540 {
2541 attrs.push(quote! { default });
2542 }
2543
2544 if let crate::analysis::SchemaType::Primitive {
2552 serde_with: Some(codec),
2553 ..
2554 } = &prop.schema_type
2555 {
2556 let is_option_wrapped = self.property_is_option_wrapped(
2557 schema_name,
2558 field_name,
2559 prop,
2560 is_required,
2561 analysis,
2562 );
2563 let codec_path = if is_option_wrapped {
2564 format!("{codec}::option")
2565 } else {
2566 codec.clone()
2567 };
2568 attrs.push(quote! { with = #codec_path });
2569 if is_option_wrapped {
2574 attrs.push(quote! { default });
2575 }
2576 }
2577
2578 if attrs.is_empty() {
2579 TokenStream::new()
2580 } else {
2581 quote! { #[serde(#(#attrs),*)] }
2582 }
2583 }
2584
2585 fn type_lacks_default(
2589 &self,
2590 schema_type: &crate::analysis::SchemaType,
2591 analysis: &crate::analysis::SchemaAnalysis,
2592 ) -> bool {
2593 use crate::analysis::SchemaType;
2594 match schema_type {
2595 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2596 SchemaType::Primitive { rust_type, .. } => matches!(
2600 rust_type.as_str(),
2601 "chrono::DateTime<chrono::Utc>"
2602 | "chrono::NaiveDate"
2603 | "chrono::NaiveTime"
2604 | "chrono::Duration"
2605 | "url::Url"
2606 | "time::OffsetDateTime"
2607 | "time::Date"
2608 | "time::Time"
2609 | "iso8601::Duration"
2610 | "email_address::EmailAddress"
2611 ),
2612 SchemaType::Reference { target } => {
2613 if let Some(schema) = analysis.schemas.get(target) {
2614 self.type_lacks_default(&schema.schema_type, analysis)
2615 } else {
2616 false
2617 }
2618 }
2619 _ => false,
2620 }
2621 }
2622
2623 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2624 if !self.config.enable_specta {
2625 return TokenStream::new();
2626 }
2627
2628 let camel_case_name = self.to_camel_case(field_name);
2630
2631 if camel_case_name != field_name {
2633 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2634 } else {
2635 TokenStream::new()
2636 }
2637 }
2638
2639 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2640 let neg_prefix =
2644 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2645 "Neg"
2646 } else {
2647 ""
2648 };
2649
2650 let mut result = String::new();
2652 let mut next_upper = true;
2653 let mut prev_was_upper = false;
2654
2655 for (i, c) in s.chars().enumerate() {
2656 match c {
2657 'a'..='z' => {
2658 if next_upper {
2659 result.push(c.to_ascii_uppercase());
2660 next_upper = false;
2661 } else {
2662 result.push(c);
2663 }
2664 prev_was_upper = false;
2665 }
2666 'A'..='Z' => {
2667 if next_upper || (!prev_was_upper && i > 0) {
2668 result.push(c);
2670 next_upper = false;
2671 } else {
2672 result.push(c.to_ascii_lowercase());
2674 }
2675 prev_was_upper = true;
2676 }
2677 '0'..='9' => {
2678 result.push(c);
2679 next_upper = false;
2680 prev_was_upper = false;
2681 }
2682 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2683 next_upper = true;
2685 prev_was_upper = false;
2686 }
2687 _ => {
2688 next_upper = true;
2690 prev_was_upper = false;
2691 }
2692 }
2693 }
2694
2695 if result.is_empty() {
2697 result = "Value".to_string();
2698 }
2699
2700 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2702 result = format!("Variant{neg_prefix}{result}");
2703 } else if !neg_prefix.is_empty() {
2704 result = format!("{neg_prefix}{result}");
2707 }
2708
2709 match result.as_str() {
2711 "Null" => "NullValue".to_string(),
2712 "True" => "TrueValue".to_string(),
2713 "False" => "FalseValue".to_string(),
2714 "Type" => "Type_".to_string(),
2715 "Match" => "Match_".to_string(),
2716 "Fn" => "Fn_".to_string(),
2717 "Impl" => "Impl_".to_string(),
2718 "Trait" => "Trait_".to_string(),
2719 "Struct" => "Struct_".to_string(),
2720 "Enum" => "Enum_".to_string(),
2721 "Mod" => "Mod_".to_string(),
2722 "Use" => "Use_".to_string(),
2723 "Pub" => "Pub_".to_string(),
2724 "Const" => "Const_".to_string(),
2725 "Static" => "Static_".to_string(),
2726 "Let" => "Let_".to_string(),
2727 "Mut" => "Mut_".to_string(),
2728 "Ref" => "Ref_".to_string(),
2729 "Move" => "Move_".to_string(),
2730 "Return" => "Return_".to_string(),
2731 "If" => "If_".to_string(),
2732 "Else" => "Else_".to_string(),
2733 "While" => "While_".to_string(),
2734 "For" => "For_".to_string(),
2735 "Loop" => "Loop_".to_string(),
2736 "Break" => "Break_".to_string(),
2737 "Continue" => "Continue_".to_string(),
2738 "Self" => "Self_".to_string(),
2739 "Super" => "Super_".to_string(),
2740 "Crate" => "Crate_".to_string(),
2741 "Async" => "Async_".to_string(),
2742 "Await" => "Await_".to_string(),
2743 _ => result,
2744 }
2745 }
2746
2747 #[allow(dead_code)]
2748 fn to_rust_identifier(&self, s: &str) -> String {
2749 let mut result = s
2751 .chars()
2752 .map(|c| match c {
2753 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2754 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2755 _ => '_',
2756 })
2757 .collect::<String>();
2758
2759 result = result.trim_matches('_').to_string();
2761
2762 if result.is_empty() {
2764 result = "value".to_string();
2765 }
2766
2767 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2769 result = format!("variant_{result}");
2770 }
2771
2772 match result.as_str() {
2774 "null" => "null_value".to_string(),
2775 "true" => "true_value".to_string(),
2776 "false" => "false_value".to_string(),
2777 "type" => "type_".to_string(),
2778 "match" => "match_".to_string(),
2779 "fn" => "fn_".to_string(),
2780 "impl" => "impl_".to_string(),
2781 "trait" => "trait_".to_string(),
2782 "struct" => "struct_".to_string(),
2783 "enum" => "enum_".to_string(),
2784 "mod" => "mod_".to_string(),
2785 "use" => "use_".to_string(),
2786 "pub" => "pub_".to_string(),
2787 "const" => "const_".to_string(),
2788 "static" => "static_".to_string(),
2789 "let" => "let_".to_string(),
2790 "mut" => "mut_".to_string(),
2791 "ref" => "ref_".to_string(),
2792 "move" => "move_".to_string(),
2793 "return" => "return_".to_string(),
2794 "if" => "if_".to_string(),
2795 "else" => "else_".to_string(),
2796 "while" => "while_".to_string(),
2797 "for" => "for_".to_string(),
2798 "loop" => "loop_".to_string(),
2799 "break" => "break_".to_string(),
2800 "continue" => "continue_".to_string(),
2801 "self" => "self_".to_string(),
2802 "super" => "super_".to_string(),
2803 "crate" => "crate_".to_string(),
2804 "async" => "async_".to_string(),
2805 "await" => "await_".to_string(),
2806 "override" => "override_".to_string(),
2808 "box" => "box_".to_string(),
2809 "dyn" => "dyn_".to_string(),
2810 "where" => "where_".to_string(),
2811 "in" => "in_".to_string(),
2812 "abstract" => "abstract_".to_string(),
2814 "become" => "become_".to_string(),
2815 "do" => "do_".to_string(),
2816 "final" => "final_".to_string(),
2817 "macro" => "macro_".to_string(),
2818 "priv" => "priv_".to_string(),
2819 "try" => "try_".to_string(),
2820 "typeof" => "typeof_".to_string(),
2821 "unsized" => "unsized_".to_string(),
2822 "virtual" => "virtual_".to_string(),
2823 "yield" => "yield_".to_string(),
2824 _ => result,
2825 }
2826 }
2827
2828 fn generate_constraint_doc(
2836 &self,
2837 constraints: &crate::analysis::PropertyConstraints,
2838 ) -> TokenStream {
2839 use crate::type_mapping::ConstraintMode;
2840
2841 if constraints.is_empty() {
2842 return TokenStream::new();
2843 }
2844 match self.config.types.constraint_mode() {
2845 ConstraintMode::Off => TokenStream::new(),
2846 ConstraintMode::Doc => {
2847 let formatted = format_constraints_doc(constraints);
2848 quote! { #[doc = #formatted] }
2849 }
2850 }
2851 }
2852
2853 fn sanitize_doc_comment(&self, desc: &str) -> String {
2854 let mut result = desc.to_string();
2856
2857 if result.contains('\n')
2865 && (result.contains('{')
2866 || result.contains("```")
2867 || result.contains("Human:")
2868 || result.contains("Assistant:")
2869 || result
2870 .lines()
2871 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2872 {
2873 if result.contains("```") {
2875 result = result.replace("```", "```ignore");
2876 } else {
2877 if result.lines().any(|line| {
2879 let trimmed = line.trim();
2880 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2881 }) {
2882 result = format!("```ignore\n{result}\n```");
2883 }
2884 }
2885 }
2886
2887 result
2888 }
2889
2890 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2891 let mut result = String::new();
2893 let mut next_upper = true;
2894 let mut prev_was_lower = false;
2895
2896 for c in s.chars() {
2897 match c {
2898 'a'..='z' => {
2899 if next_upper {
2900 result.push(c.to_ascii_uppercase());
2901 next_upper = false;
2902 } else {
2903 result.push(c);
2904 }
2905 prev_was_lower = true;
2906 }
2907 'A'..='Z' => {
2908 result.push(c);
2909 next_upper = false;
2910 prev_was_lower = false;
2911 }
2912 '0'..='9' => {
2913 if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2916 }
2918 result.push(c);
2919 next_upper = false;
2920 prev_was_lower = false;
2921 }
2922 '_' | '-' | '.' | ' ' => {
2923 next_upper = true;
2925 prev_was_lower = false;
2926 }
2927 _ => {
2928 next_upper = true;
2930 prev_was_lower = false;
2931 }
2932 }
2933 }
2934
2935 if result.is_empty() {
2937 result = "Type".to_string();
2938 }
2939
2940 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2942 result = format!("Type{result}");
2943 }
2944
2945 if matches!(
2952 result.as_str(),
2953 "Result"
2954 | "Option"
2955 | "Box"
2956 | "Vec"
2957 | "String"
2958 | "Some"
2959 | "None"
2960 | "Ok"
2961 | "Err"
2962 | "Default"
2963 | "Clone"
2964 | "Debug"
2965 | "Send"
2966 | "Sync"
2967 | "Sized"
2968 | "Iterator"
2969 | "From"
2970 | "Into"
2971 | "TryFrom"
2972 | "TryInto"
2973 | "AsRef"
2974 | "AsMut"
2975 ) {
2976 result.push_str("Type");
2977 }
2978
2979 result
2980 }
2981
2982 fn to_rust_field_name(&self, s: &str) -> String {
2983 let leading_marker = match s.chars().next() {
2987 Some('-') if s.len() > 1 => "neg_",
2988 Some('+') if s.len() > 1 => "pos_",
2989 _ => "",
2990 };
2991
2992 let mut result = String::new();
2994 let mut prev_was_upper = false;
2995 let mut prev_was_underscore = false;
2996
2997 for (i, c) in s.chars().enumerate() {
2998 match c {
2999 'A'..='Z' => {
3000 if i > 0 && !prev_was_upper && !prev_was_underscore {
3002 result.push('_');
3003 }
3004 result.push(c.to_ascii_lowercase());
3005 prev_was_upper = true;
3006 prev_was_underscore = false;
3007 }
3008 'a'..='z' | '0'..='9' => {
3009 result.push(c);
3010 prev_was_upper = false;
3011 prev_was_underscore = false;
3012 }
3013 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3014 if !prev_was_underscore && !result.is_empty() {
3015 result.push('_');
3016 prev_was_underscore = true;
3017 }
3018 prev_was_upper = false;
3019 }
3020 _ => {
3021 if !prev_was_underscore && !result.is_empty() {
3023 result.push('_');
3024 }
3025 prev_was_upper = false;
3026 prev_was_underscore = true;
3027 }
3028 }
3029 }
3030
3031 let mut result = result.trim_matches('_').to_string();
3033 if result.is_empty() {
3034 return "field".to_string();
3035 }
3036
3037 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3039 result = format!("field_{leading_marker}{result}");
3040 } else if !leading_marker.is_empty() {
3041 result = format!("{leading_marker}{result}");
3042 }
3043
3044 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3048 return format!("{result}_field");
3049 }
3050 if Self::is_rust_keyword(&result) {
3052 format!("r#{result}")
3053 } else {
3054 result
3055 }
3056 }
3057
3058 pub fn is_rust_keyword(s: &str) -> bool {
3060 matches!(
3061 s,
3062 "type"
3063 | "match"
3064 | "fn"
3065 | "struct"
3066 | "enum"
3067 | "impl"
3068 | "trait"
3069 | "mod"
3070 | "use"
3071 | "pub"
3072 | "const"
3073 | "static"
3074 | "let"
3075 | "mut"
3076 | "ref"
3077 | "move"
3078 | "return"
3079 | "if"
3080 | "else"
3081 | "while"
3082 | "for"
3083 | "loop"
3084 | "break"
3085 | "continue"
3086 | "self"
3087 | "super"
3088 | "crate"
3089 | "async"
3090 | "await"
3091 | "override"
3092 | "box"
3093 | "dyn"
3094 | "where"
3095 | "in"
3096 | "abstract"
3097 | "become"
3098 | "do"
3099 | "final"
3100 | "macro"
3101 | "priv"
3102 | "try"
3103 | "typeof"
3104 | "unsized"
3105 | "virtual"
3106 | "yield"
3107 | "gen"
3109 )
3110 }
3111
3112 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3114 if let Some(raw) = name.strip_prefix("r#") {
3115 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3116 } else {
3117 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3118 }
3119 }
3120
3121 fn to_camel_case(&self, s: &str) -> String {
3122 let mut result = String::new();
3124 let mut capitalize_next = false;
3125
3126 for (i, c) in s.chars().enumerate() {
3127 match c {
3128 '_' | '-' | '.' | ' ' => {
3129 capitalize_next = true;
3131 }
3132 'A'..='Z' => {
3133 if i == 0 {
3134 result.push(c.to_ascii_lowercase());
3136 } else if capitalize_next {
3137 result.push(c);
3138 capitalize_next = false;
3139 } else {
3140 result.push(c.to_ascii_lowercase());
3141 }
3142 }
3143 'a'..='z' | '0'..='9' => {
3144 if capitalize_next {
3145 result.push(c.to_ascii_uppercase());
3146 capitalize_next = false;
3147 } else {
3148 result.push(c);
3149 }
3150 }
3151 _ => {
3152 capitalize_next = true;
3154 }
3155 }
3156 }
3157
3158 if result.is_empty() {
3159 return "field".to_string();
3160 }
3161
3162 result
3163 }
3164
3165 fn generate_composition_struct(
3166 &self,
3167 schema: &crate::analysis::AnalyzedSchema,
3168 schemas: &[crate::analysis::SchemaRef],
3169 ) -> Result<TokenStream> {
3170 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3171
3172 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3178 let field_name = format_ident!("part_{}", i);
3179 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3180
3181 quote! {
3182 #[serde(flatten)]
3183 pub #field_name: #field_type,
3184 }
3185 });
3186
3187 let doc_comment = if let Some(desc) = &schema.description {
3188 quote! { #[doc = #desc] }
3189 } else {
3190 TokenStream::new()
3191 };
3192
3193 let derives = if self.config.enable_specta {
3195 quote! {
3196 #[derive(Debug, Clone, Deserialize, Serialize)]
3197 #[cfg_attr(feature = "specta", derive(specta::Type))]
3198 }
3199 } else {
3200 quote! {
3201 #[derive(Debug, Clone, Deserialize, Serialize)]
3202 }
3203 };
3204
3205 Ok(quote! {
3206 #doc_comment
3207 #derives
3208 pub struct #struct_name {
3209 #(#fields)*
3210 }
3211 })
3212 }
3213
3214 #[allow(dead_code)]
3215 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3216 let mut missing = std::collections::HashSet::new();
3217 let defined_types: std::collections::HashSet<String> =
3218 analysis.schemas.keys().cloned().collect();
3219
3220 for schema in analysis.schemas.values() {
3222 match &schema.schema_type {
3223 crate::analysis::SchemaType::Union { variants } => {
3224 for variant in variants {
3225 if !defined_types.contains(&variant.target) {
3226 missing.insert(variant.target.clone());
3227 }
3228 }
3229 }
3230 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3231 for variant in variants {
3232 if !defined_types.contains(&variant.type_name) {
3233 missing.insert(variant.type_name.clone());
3234 }
3235 }
3236 }
3237 crate::analysis::SchemaType::Object { properties, .. } => {
3238 let mut sorted_props: Vec<_> = properties.iter().collect();
3240 sorted_props.sort_by_key(|(name, _)| name.as_str());
3241 for (_, prop) in sorted_props {
3242 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3243 {
3244 if !defined_types.contains(target) {
3245 missing.insert(target.clone());
3246 }
3247 }
3248 }
3249 }
3250 crate::analysis::SchemaType::Reference { target }
3251 if !defined_types.contains(target) =>
3252 {
3253 missing.insert(target.clone());
3254 }
3255 _ => {}
3256 }
3257 }
3258
3259 missing
3260 }
3261
3262 #[allow(clippy::only_used_in_recursion)]
3263 fn generate_array_item_type(
3264 &self,
3265 item_type: &crate::analysis::SchemaType,
3266 analysis: &crate::analysis::SchemaAnalysis,
3267 ) -> TokenStream {
3268 use crate::analysis::SchemaType;
3269
3270 match item_type {
3271 SchemaType::Primitive { rust_type, .. } => {
3272 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3277 quote! { #parsed }
3278 } else if rust_type.contains("::") {
3279 let parts: Vec<_> = rust_type
3280 .split("::")
3281 .map(|p| format_ident!("{}", p))
3282 .collect();
3283 quote! { #(#parts)::* }
3284 } else {
3285 let type_ident = format_ident!("{}", rust_type);
3286 quote! { #type_ident }
3287 }
3288 }
3289 SchemaType::Reference { target } => {
3290 let target_type = format_ident!("{}", self.to_rust_type_name(target));
3291 if analysis.dependencies.recursive_schemas.contains(target) {
3293 quote! { Box<#target_type> }
3294 } else {
3295 quote! { #target_type }
3296 }
3297 }
3298 SchemaType::Array { item_type } => {
3299 let inner_type = self.generate_array_item_type(item_type, analysis);
3301 quote! { Vec<#inner_type> }
3302 }
3303 _ => {
3304 quote! { serde_json::Value }
3306 }
3307 }
3308 }
3309
3310 fn type_name_to_variant_name(&self, type_name: &str) -> String {
3312 match type_name {
3314 "bool" => return "Boolean".to_string(),
3315 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3316 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3317 "f32" | "f64" => return "Number".to_string(),
3318 "String" => return "String".to_string(),
3319 "serde_json::Value" => return "Value".to_string(),
3320 "bytes::Bytes" => return "Binary".to_string(),
3324 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3325 "chrono::NaiveDate" => return "Date".to_string(),
3326 "chrono::NaiveTime" => return "Time".to_string(),
3327 "uuid::Uuid" => return "Uuid".to_string(),
3328 "url::Url" => return "Url".to_string(),
3329 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3330 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3331 _ => {}
3332 }
3333
3334 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3336 let inner = &type_name[4..type_name.len() - 1];
3337 if inner.starts_with("Vec<") && inner.ends_with(">") {
3339 let inner_inner = &inner[4..inner.len() - 1];
3340 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3341 }
3342 return format!("{}Array", self.type_name_to_variant_name(inner));
3343 }
3344
3345 let clean_name = type_name
3351 .trim_end_matches("Type")
3352 .trim_end_matches("Schema")
3353 .trim_end_matches("Item");
3354
3355 self.to_rust_type_name(clean_name)
3357 }
3358
3359 fn ensure_unique_variant_name_generator(
3361 &self,
3362 base_name: String,
3363 used_names: &mut std::collections::HashSet<String>,
3364 fallback_index: usize,
3365 ) -> String {
3366 if used_names.insert(base_name.clone()) {
3367 return base_name;
3368 }
3369
3370 for i in 2..100 {
3372 let numbered_name = format!("{base_name}{i}");
3373 if used_names.insert(numbered_name.clone()) {
3374 return numbered_name;
3375 }
3376 }
3377
3378 let fallback = format!("Variant{fallback_index}");
3380 used_names.insert(fallback.clone());
3381 fallback
3382 }
3383
3384 fn find_request_type_for_operation(
3386 &self,
3387 operation_id: &str,
3388 analysis: &SchemaAnalysis,
3389 ) -> Option<String> {
3390 analysis.operations.get(operation_id).and_then(|op| {
3392 op.request_body
3393 .as_ref()
3394 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3395 })
3396 }
3397
3398 fn resolve_streaming_event_type(
3400 &self,
3401 endpoint: &crate::streaming::StreamingEndpoint,
3402 analysis: &SchemaAnalysis,
3403 ) -> Result<String> {
3404 match &endpoint.event_flow {
3405 crate::streaming::EventFlow::Simple => {
3406 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3409 Ok(endpoint.event_union_type.to_string())
3410 } else {
3411 Err(crate::error::GeneratorError::ValidationError(format!(
3412 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3413 endpoint.event_union_type, endpoint.operation_id
3414 )))
3415 }
3416 }
3417 crate::streaming::EventFlow::StartDeltaStop { .. } => {
3418 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3421 Ok(endpoint.event_union_type.to_string())
3422 } else {
3423 Err(crate::error::GeneratorError::ValidationError(format!(
3424 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3425 endpoint.event_union_type, endpoint.operation_id
3426 )))
3427 }
3428 }
3429 }
3430 }
3431
3432 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3434 Ok(quote! {
3435 #[derive(Debug, thiserror::Error)]
3437 pub enum StreamingError {
3438 #[error("Connection error: {0}")]
3439 Connection(String),
3440 #[error("HTTP error: {status}")]
3441 Http { status: u16 },
3442 #[error("SSE parsing error: {0}")]
3443 Parsing(String),
3444 #[error("Authentication error: {0}")]
3445 Authentication(String),
3446 #[error("Rate limit error: {0}")]
3447 RateLimit(String),
3448 #[error("API error: {0}")]
3449 Api(String),
3450 #[error("Timeout error: {0}")]
3451 Timeout(String),
3452 #[error("JSON serialization/deserialization error: {0}")]
3453 Json(#[from] serde_json::Error),
3454 #[error("Request error: {0}")]
3455 Request(reqwest::Error),
3456 }
3457
3458 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3459 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3460 StreamingError::Api(format!("Invalid header value: {}", err))
3461 }
3462 }
3463
3464 impl From<reqwest::Error> for StreamingError {
3465 fn from(err: reqwest::Error) -> Self {
3466 if err.is_timeout() {
3467 StreamingError::Timeout(err.to_string())
3468 } else if err.is_status() {
3469 if let Some(status) = err.status() {
3470 StreamingError::Http { status: status.as_u16() }
3471 } else {
3472 StreamingError::Connection(err.to_string())
3473 }
3474 } else {
3475 StreamingError::Request(err)
3476 }
3477 }
3478 }
3479 })
3480 }
3481
3482 fn generate_endpoint_trait(
3484 &self,
3485 endpoint: &crate::streaming::StreamingEndpoint,
3486 analysis: &SchemaAnalysis,
3487 ) -> Result<TokenStream> {
3488 use crate::streaming::HttpMethod;
3489
3490 let trait_name = format_ident!(
3491 "{}StreamingClient",
3492 self.to_rust_type_name(&endpoint.operation_id)
3493 );
3494 let method_name =
3495 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3496 let event_type =
3497 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3498
3499 let method_signature = match endpoint.http_method {
3501 HttpMethod::Get => {
3502 let mut param_defs = Vec::new();
3504 for qp in &endpoint.query_parameters {
3505 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3506 if qp.required {
3507 param_defs.push(quote! { #param_name: &str });
3508 } else {
3509 param_defs.push(quote! { #param_name: Option<&str> });
3510 }
3511 }
3512 quote! {
3513 async fn #method_name(
3514 &self,
3515 #(#param_defs),*
3516 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3517 }
3518 }
3519 HttpMethod::Post => {
3520 let request_type = self
3522 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3523 .unwrap_or_else(|| "serde_json::Value".to_string());
3524 let request_type_ident = if request_type.contains("::") {
3525 let parts: Vec<&str> = request_type.split("::").collect();
3526 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3527 quote! { #(#path_parts)::* }
3528 } else {
3529 let ident = format_ident!("{}", request_type);
3530 quote! { #ident }
3531 };
3532 quote! {
3533 async fn #method_name(
3534 &self,
3535 request: #request_type_ident,
3536 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3537 }
3538 }
3539 };
3540
3541 Ok(quote! {
3542 #[async_trait]
3544 pub trait #trait_name {
3545 type Error: std::error::Error + Send + Sync + 'static;
3546
3547 #method_signature
3549 }
3550 })
3551 }
3552
3553 fn generate_streaming_client_impl(
3555 &self,
3556 streaming_config: &crate::streaming::StreamingConfig,
3557 analysis: &SchemaAnalysis,
3558 ) -> Result<TokenStream> {
3559 let client_name = format_ident!(
3560 "{}Client",
3561 self.to_rust_type_name(&streaming_config.client_module_name)
3562 );
3563
3564 let mut struct_fields = vec![
3567 quote! { base_url: String },
3568 quote! { api_key: Option<String> },
3569 quote! { http_client: reqwest::Client },
3570 quote! { custom_headers: std::collections::BTreeMap<String, String> },
3571 ];
3572
3573 let has_optional_headers = !streaming_config
3574 .endpoints
3575 .iter()
3576 .all(|e| e.optional_headers.is_empty());
3577
3578 if has_optional_headers {
3579 struct_fields
3580 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3581 }
3582
3583 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3586 streaming_config
3587 .endpoints
3588 .first()
3589 .and_then(|e| e.base_url.as_deref())
3590 .unwrap_or("https://api.example.com")
3591 } else {
3592 "https://api.example.com"
3593 };
3594
3595 let constructor_fields = if has_optional_headers {
3597 quote! {
3598 base_url: #default_base_url.to_string(),
3599 api_key: None,
3600 http_client: reqwest::Client::new(),
3601 custom_headers: std::collections::BTreeMap::new(),
3602 optional_headers: std::collections::BTreeMap::new(),
3603 }
3604 } else {
3605 quote! {
3606 base_url: #default_base_url.to_string(),
3607 api_key: None,
3608 http_client: reqwest::Client::new(),
3609 custom_headers: std::collections::BTreeMap::new(),
3610 }
3611 };
3612
3613 let optional_headers_method = if has_optional_headers {
3615 quote! {
3616 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3618 self.optional_headers = headers;
3619 }
3620 }
3621 } else {
3622 TokenStream::new()
3623 };
3624
3625 let constructor = quote! {
3626 impl #client_name {
3627 pub fn new() -> Self {
3629 Self {
3630 #constructor_fields
3631 }
3632 }
3633
3634 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3636 self.base_url = base_url.into();
3637 self
3638 }
3639
3640 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3642 self.api_key = Some(api_key.into());
3643 self
3644 }
3645
3646 pub fn with_header(
3648 mut self,
3649 name: impl Into<String>,
3650 value: impl Into<String>,
3651 ) -> Self {
3652 self.custom_headers.insert(name.into(), value.into());
3653 self
3654 }
3655
3656 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3658 self.http_client = client;
3659 self
3660 }
3661
3662 #optional_headers_method
3663 }
3664 };
3665
3666 let mut trait_impls = Vec::new();
3668 for endpoint in &streaming_config.endpoints {
3669 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3670 trait_impls.push(trait_impl);
3671 }
3672
3673 let default_impl = quote! {
3675 impl Default for #client_name {
3676 fn default() -> Self {
3677 Self::new()
3678 }
3679 }
3680 };
3681
3682 Ok(quote! {
3683 #[derive(Debug, Clone)]
3685 pub struct #client_name {
3686 #(#struct_fields,)*
3687 }
3688
3689 #constructor
3690
3691 #default_impl
3692
3693 #(#trait_impls)*
3694 })
3695 }
3696
3697 fn generate_endpoint_trait_impl(
3699 &self,
3700 endpoint: &crate::streaming::StreamingEndpoint,
3701 client_name: &proc_macro2::Ident,
3702 analysis: &SchemaAnalysis,
3703 ) -> Result<TokenStream> {
3704 use crate::streaming::HttpMethod;
3705
3706 let trait_name = format_ident!(
3707 "{}StreamingClient",
3708 self.to_rust_type_name(&endpoint.operation_id)
3709 );
3710 let method_name =
3711 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3712 let event_type =
3713 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3714
3715 let mut header_setup = Vec::new();
3717 for (name, value) in &endpoint.required_headers {
3718 header_setup.push(quote! {
3719 headers.insert(#name, HeaderValue::from_static(#value));
3720 });
3721 }
3722
3723 if let Some(auth_header) = &endpoint.auth_header {
3726 match auth_header {
3727 crate::streaming::AuthHeader::Bearer(header_name) => {
3728 header_setup.push(quote! {
3729 if let Some(ref api_key) = self.api_key {
3730 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3731 }
3732 });
3733 }
3734 crate::streaming::AuthHeader::ApiKey(header_name) => {
3735 header_setup.push(quote! {
3736 if let Some(ref api_key) = self.api_key {
3737 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3738 }
3739 });
3740 }
3741 }
3742 } else {
3743 header_setup.push(quote! {
3745 if let Some(ref api_key) = self.api_key {
3746 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3747 }
3748 });
3749 }
3750
3751 header_setup.push(quote! {
3753 for (name, value) in &self.custom_headers {
3754 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3755 headers.insert(header_name, header_value);
3756 }
3757 }
3758 });
3759
3760 if !endpoint.optional_headers.is_empty() {
3762 header_setup.push(quote! {
3763 for (key, value) in &self.optional_headers {
3764 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3765 headers.insert(header_name, header_value);
3766 }
3767 }
3768 });
3769 }
3770
3771 match endpoint.http_method {
3773 HttpMethod::Get => self.generate_get_streaming_impl(
3774 endpoint,
3775 client_name,
3776 &trait_name,
3777 &method_name,
3778 &event_type,
3779 &header_setup,
3780 ),
3781 HttpMethod::Post => self.generate_post_streaming_impl(
3782 endpoint,
3783 client_name,
3784 &trait_name,
3785 &method_name,
3786 &event_type,
3787 &header_setup,
3788 analysis,
3789 ),
3790 }
3791 }
3792
3793 fn generate_get_streaming_impl(
3795 &self,
3796 endpoint: &crate::streaming::StreamingEndpoint,
3797 client_name: &proc_macro2::Ident,
3798 trait_name: &proc_macro2::Ident,
3799 method_name: &proc_macro2::Ident,
3800 event_type: &proc_macro2::Ident,
3801 header_setup: &[TokenStream],
3802 ) -> Result<TokenStream> {
3803 let path = &endpoint.path;
3804
3805 let mut param_defs = Vec::new();
3807 let mut query_params = Vec::new();
3808
3809 for qp in &endpoint.query_parameters {
3810 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3811 let param_name_str = &qp.name;
3812
3813 if qp.required {
3814 param_defs.push(quote! { #param_name: &str });
3815 query_params.push(quote! {
3816 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3817 });
3818 } else {
3819 param_defs.push(quote! { #param_name: Option<&str> });
3820 query_params.push(quote! {
3821 if let Some(v) = #param_name {
3822 url.query_pairs_mut().append_pair(#param_name_str, v);
3823 }
3824 });
3825 }
3826 }
3827
3828 let url_construction = quote! {
3830 let base_url = url::Url::parse(&self.base_url)
3831 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3832 let path_to_join = #path.trim_start_matches('/');
3833 let mut url = base_url.join(path_to_join)
3834 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3835 #(#query_params)*
3836 };
3837
3838 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3839
3840 Ok(quote! {
3841 #[async_trait]
3842 impl #trait_name for #client_name {
3843 type Error = StreamingError;
3844
3845 #instrument_skip
3846 async fn #method_name(
3847 &self,
3848 #(#param_defs),*
3849 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3850 debug!("Starting streaming GET request");
3851
3852 let mut headers = HeaderMap::new();
3853 #(#header_setup)*
3854
3855 #url_construction
3856 let url_str = url.to_string();
3857 debug!("Making streaming GET request to: {}", url_str);
3858
3859 let request_builder = self.http_client
3860 .get(url_str)
3861 .headers(headers);
3862
3863 debug!("Creating SSE stream from request");
3864 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3865 info!("SSE stream created successfully");
3866 Ok(Box::pin(stream))
3867 }
3868 }
3869 })
3870 }
3871
3872 #[allow(clippy::too_many_arguments)]
3874 fn generate_post_streaming_impl(
3875 &self,
3876 endpoint: &crate::streaming::StreamingEndpoint,
3877 client_name: &proc_macro2::Ident,
3878 trait_name: &proc_macro2::Ident,
3879 method_name: &proc_macro2::Ident,
3880 event_type: &proc_macro2::Ident,
3881 header_setup: &[TokenStream],
3882 analysis: &SchemaAnalysis,
3883 ) -> Result<TokenStream> {
3884 let path = &endpoint.path;
3885
3886 let request_type = self
3888 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3889 .unwrap_or_else(|| "serde_json::Value".to_string());
3890 let request_type_ident = if request_type.contains("::") {
3891 let parts: Vec<&str> = request_type.split("::").collect();
3892 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3893 quote! { #(#path_parts)::* }
3894 } else {
3895 let ident = format_ident!("{}", request_type);
3896 quote! { #ident }
3897 };
3898
3899 let url_construction = quote! {
3901 let base_url = url::Url::parse(&self.base_url)
3902 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3903 let path_to_join = #path.trim_start_matches('/');
3904 let url = base_url.join(path_to_join)
3905 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3906 .to_string();
3907 };
3908
3909 let stream_param = &endpoint.stream_parameter;
3911 let stream_setup = if stream_param.is_empty() {
3912 quote! {
3913 let streaming_request = request;
3914 }
3915 } else {
3916 quote! {
3917 let mut streaming_request = request;
3919 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3920 if let Some(obj) = request_value.as_object_mut() {
3921 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3922 }
3923 streaming_request = serde_json::from_value(request_value)?;
3924 }
3925 }
3926 };
3927
3928 Ok(quote! {
3929 #[async_trait]
3930 impl #trait_name for #client_name {
3931 type Error = StreamingError;
3932
3933 #[instrument(skip(self, request), name = "streaming_post_request")]
3934 async fn #method_name(
3935 &self,
3936 request: #request_type_ident,
3937 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3938 debug!("Starting streaming POST request");
3939
3940 #stream_setup
3941
3942 let mut headers = HeaderMap::new();
3943 #(#header_setup)*
3944
3945 #url_construction
3946 debug!("Making streaming POST request to: {}", url);
3947
3948 let request_builder = self.http_client
3949 .post(&url)
3950 .headers(headers)
3951 .json(&streaming_request);
3952
3953 debug!("Creating SSE stream from request");
3954 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3955 info!("SSE stream created successfully");
3956 Ok(Box::pin(stream))
3957 }
3958 }
3959 })
3960 }
3961
3962 fn generate_sse_parser_utilities(
3964 &self,
3965 _streaming_config: &crate::streaming::StreamingConfig,
3966 ) -> Result<TokenStream> {
3967 Ok(quote! {
3968 pub async fn parse_sse_stream<T>(
3970 request_builder: reqwest::RequestBuilder
3971 ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
3972 where
3973 T: serde::de::DeserializeOwned + Send + 'static,
3974 {
3975 let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
3976 StreamingError::Connection(format!("Failed to create event source: {}", e))
3977 })?;
3978
3979 let stream = event_source.filter_map(|event_result| async move {
3980 match event_result {
3981 Ok(reqwest_eventsource::Event::Open) => {
3982 debug!("SSE connection opened");
3983 None
3984 }
3985 Ok(reqwest_eventsource::Event::Message(message)) => {
3986 if message.event == "ping" {
3988 debug!("Received SSE ping event, skipping");
3989 return None;
3990 }
3991
3992 if message.data.trim().is_empty() {
3994 debug!("Empty SSE data, skipping");
3995 return None;
3996 }
3997
3998 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
4000 if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
4001 if event_type == "ping" {
4002 debug!("Received ping event in JSON data, skipping");
4003 return None;
4004 }
4005 }
4006
4007 match serde_json::from_value::<T>(json_value) {
4009 Ok(parsed_event) => {
4010 Some(Ok(parsed_event))
4011 }
4012 Err(e) => {
4013 if message.data.contains("ping") || message.event.contains("ping") {
4014 debug!("Ignoring ping-related event: {}", message.data);
4015 None
4016 } else {
4017 Some(Err(StreamingError::Parsing(
4018 format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
4019 )))
4020 }
4021 }
4022 }
4023 } else {
4024 Some(Err(StreamingError::Parsing(
4026 format!("SSE event is not valid JSON: {}", message.data)
4027 )))
4028 }
4029 }
4030 Err(e) => {
4031 match e {
4033 reqwest_eventsource::Error::StreamEnded => {
4034 debug!("SSE stream completed normally");
4035 None }
4037 reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
4038 let status_code = status.as_u16();
4040
4041 let error_body = match response.text().await {
4043 Ok(body) => body,
4044 Err(_) => "Failed to read error response body".to_string()
4045 };
4046
4047 error!("SSE connection error - HTTP {}: {}", status_code, error_body);
4048
4049 let detailed_error = format!(
4050 "HTTP {} error: {}",
4051 status_code,
4052 error_body
4053 );
4054
4055 Some(Err(StreamingError::Connection(detailed_error)))
4056 }
4057 _ => {
4058 let error_str = e.to_string();
4059 if error_str.contains("stream closed") {
4060 debug!("SSE stream closed");
4061 None
4062 } else {
4063 error!("SSE connection error: {}", e);
4064 Some(Err(StreamingError::Connection(error_str)))
4065 }
4066 }
4067 }
4068 }
4069 }
4070 });
4071
4072 Ok(stream)
4073 }
4074 })
4075 }
4076
4077 fn generate_reconnection_utilities(
4079 &self,
4080 reconnect_config: &crate::streaming::ReconnectionConfig,
4081 ) -> Result<TokenStream> {
4082 let max_retries = reconnect_config.max_retries;
4083 let initial_delay = reconnect_config.initial_delay_ms;
4084 let max_delay = reconnect_config.max_delay_ms;
4085 let backoff_multiplier = reconnect_config.backoff_multiplier;
4086
4087 Ok(quote! {
4088 #[derive(Debug, Clone)]
4090 pub struct ReconnectionManager {
4091 max_retries: u32,
4092 initial_delay_ms: u64,
4093 max_delay_ms: u64,
4094 backoff_multiplier: f64,
4095 current_attempt: u32,
4096 }
4097
4098 impl ReconnectionManager {
4099 pub fn new() -> Self {
4101 Self {
4102 max_retries: #max_retries,
4103 initial_delay_ms: #initial_delay,
4104 max_delay_ms: #max_delay,
4105 backoff_multiplier: #backoff_multiplier,
4106 current_attempt: 0,
4107 }
4108 }
4109
4110 pub fn should_retry(&self) -> bool {
4112 self.current_attempt < self.max_retries
4113 }
4114
4115 pub fn next_retry_delay(&mut self) -> Duration {
4117 if !self.should_retry() {
4118 return Duration::from_secs(0);
4119 }
4120
4121 let delay_ms = (self.initial_delay_ms as f64
4122 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
4123 let delay_ms = delay_ms.min(self.max_delay_ms);
4124
4125 self.current_attempt += 1;
4126 Duration::from_millis(delay_ms)
4127 }
4128
4129 pub fn reset(&mut self) {
4131 self.current_attempt = 0;
4132 }
4133
4134 pub fn current_attempt(&self) -> u32 {
4136 self.current_attempt
4137 }
4138 }
4139
4140 impl Default for ReconnectionManager {
4141 fn default() -> Self {
4142 Self::new()
4143 }
4144 }
4145 })
4146 }
4147}