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