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
212impl GeneratorConfig {
213 pub fn apply_spec_server_default(&mut self, spec: &serde_json::Value) {
224 let already_configured = self
225 .http_client_config
226 .as_ref()
227 .and_then(|http| http.base_url.as_deref())
228 .is_some_and(|url| !url.is_empty());
229 if already_configured {
230 return;
231 }
232
233 let Some(url) = spec
234 .pointer("/servers/0/url")
235 .and_then(serde_json::Value::as_str)
236 .map(str::trim)
237 .filter(|url| !url.is_empty())
238 .filter(|url| !url.starts_with('/'))
239 .filter(|url| !url.contains('{'))
240 else {
241 return;
242 };
243
244 match self.http_client_config.as_mut() {
245 Some(http) => http.base_url = Some(url.to_string()),
246 None => {
247 self.http_client_config = Some(crate::http_config::HttpClientConfig {
248 base_url: Some(url.to_string()),
249 timeout_seconds: None,
250 default_headers: Default::default(),
251 })
252 }
253 }
254 }
255}
256
257pub fn default_type_mappings() -> BTreeMap<String, String> {
258 let mut mappings = BTreeMap::new();
259 mappings.insert("integer".to_string(), "i64".to_string());
260 mappings.insert("number".to_string(), "f64".to_string());
261 mappings.insert("string".to_string(), "String".to_string());
262 mappings.insert("boolean".to_string(), "bool".to_string());
263 mappings
264}
265
266#[derive(Debug, Clone)]
268pub struct GeneratedFile {
269 pub path: PathBuf,
271 pub content: String,
273}
274
275#[derive(Debug, Clone)]
277pub struct GenerationResult {
278 pub files: Vec<GeneratedFile>,
280 pub mod_file: GeneratedFile,
282 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
286 pub pruned_schemas: usize,
288}
289
290#[derive(Debug)]
291struct OperationScopes {
292 client_ids: Option<std::collections::BTreeSet<String>>,
294 server_ids: std::collections::BTreeSet<String>,
295 streaming_ids: std::collections::BTreeSet<String>,
296 prune_models: bool,
297 extra_schema_roots: Vec<String>,
298}
299
300pub struct CodeGenerator {
301 config: GeneratorConfig,
302 source_provenance: Option<String>,
303}
304
305impl CodeGenerator {
306 pub fn new(config: GeneratorConfig) -> Self {
307 Self {
308 config,
309 source_provenance: None,
310 }
311 }
312
313 pub fn with_source_provenance(mut self, source: impl Into<String>) -> Self {
315 self.source_provenance = Some(source.into());
316 self
317 }
318
319 pub fn config(&self) -> &GeneratorConfig {
321 &self.config
322 }
323
324 pub(crate) fn provenance_attribute(&self) -> TokenStream {
325 self.source_provenance
326 .as_ref()
327 .map(|source| {
328 let provenance = format!(
329 " Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
330 env!("CARGO_PKG_VERSION")
331 );
332 quote! { #![doc = #provenance] }
333 })
334 .unwrap_or_default()
335 }
336
337 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
339 let scopes = self.resolve_operation_scopes(analysis)?;
342 let pruned_schemas = self.prune_models_to_scopes(analysis, &scopes);
343 let mut files = Vec::new();
344
345 if !self.config.registry_only {
346 let types_content = self.generate_types(analysis)?;
348 files.push(GeneratedFile {
349 path: "types.rs".into(),
350 content: types_content,
351 });
352
353 if self.config.enable_sse_client
355 && let Some(ref streaming_config) = self.config.streaming_config
356 {
357 if streaming_config.generate_client && !streaming_config.event_parser_helpers {
358 return Err(GeneratorError::ValidationError(
359 "streaming generate_client=true requires event_parser_helpers=true"
360 .to_string(),
361 ));
362 }
363 let streaming_content =
364 self.generate_streaming_client(streaming_config, analysis)?;
365 files.push(GeneratedFile {
366 path: "streaming.rs".into(),
367 content: streaming_content,
368 });
369 }
370
371 if self.config.enable_async_client {
373 let operations = self.client_operations(analysis, scopes.client_ids.as_ref());
374 let http_content =
375 self.generate_http_client_for_operations(analysis, &operations)?;
376 files.push(GeneratedFile {
377 path: "client.rs".into(),
378 content: http_content,
379 });
380 }
381 }
382
383 if self.config.enable_registry || self.config.registry_only {
385 let registry_content = self.generate_registry(analysis)?;
386 files.push(GeneratedFile {
387 path: "registry.rs".into(),
388 content: registry_content,
389 });
390 }
391
392 if !self.config.registry_only
396 && let Some(server) = self
397 .config
398 .server
399 .as_ref()
400 .filter(|server| !server.operations.is_empty())
401 {
402 let server_files =
403 crate::server::codegen::ServerCodegen::new(&self.config, analysis, server)
404 .with_source_provenance(self.source_provenance.as_deref())
405 .generate()
406 .map_err(|error| {
407 GeneratorError::CodeGenError(format!(
408 "server code generation failed: {error}"
409 ))
410 })?;
411 files.extend(server_files);
412 }
413
414 let mod_content = self.generate_mod_file(&files)?;
416 let mod_file = GeneratedFile {
417 path: "mod.rs".into(),
418 content: mod_content,
419 };
420
421 let required_deps = crate::type_mapping::collect_generated_dep_requirements(
422 files.iter().map(|file| file.content.as_str()),
423 self.config.enable_specta,
424 );
425
426 Ok(GenerationResult {
427 files,
428 mod_file,
429 required_deps,
430 pruned_schemas,
431 })
432 }
433
434 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
436 self.generate_types(analysis)
437 }
438
439 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
441 let provenance_attribute = self.provenance_attribute();
442 let mut type_definitions = TokenStream::new();
443
444 let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
447 BTreeMap::new();
448
449 let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
451 sorted_schemas.sort_by_key(|(name, _)| name.as_str());
452
453 for (_parent_name, schema) in sorted_schemas {
454 if let crate::analysis::SchemaType::DiscriminatedUnion {
455 variants,
456 discriminator_field,
457 } = &schema.schema_type
458 {
459 let is_parent_untagged =
461 self.should_use_untagged_discriminated_union(schema, analysis);
462
463 for variant in variants {
464 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
467 if let crate::analysis::SchemaType::Object { properties, .. } =
468 &variant_schema.schema_type
469 {
470 if properties.contains_key(discriminator_field) {
471 discriminated_variant_info.insert(
472 variant.type_name.clone(),
473 DiscriminatedVariantInfo {
474 discriminator_field: discriminator_field.clone(),
475 discriminator_value: variant.discriminator_value.clone(),
476 is_parent_untagged,
477 },
478 );
479 }
480 }
481 }
482 }
483 }
484 }
485
486 let type_index = self.type_generation_index(analysis);
487 let type_context = TypeGenerationContext {
488 discriminated_variants: &discriminated_variant_info,
489 index: &type_index,
490 };
491
492 let generation_order = analysis.dependencies.topological_sort()?;
494
495 let mut emitted_rust_names: std::collections::HashSet<String> =
503 std::collections::HashSet::new();
504 let mut processed = std::collections::HashSet::new();
505
506 for schema_name in generation_order {
508 if let Some(schema) = analysis.schemas.get(&schema_name) {
509 let rust_name = self.to_rust_type_name(&schema.name);
510 if !emitted_rust_names.insert(rust_name) {
511 processed.insert(schema_name);
512 continue;
513 }
514 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
515 if !type_def.is_empty() {
516 type_definitions.extend(type_def);
517 }
518 processed.insert(schema_name);
519 }
520 }
521
522 let mut remaining_schemas: Vec<_> = analysis
524 .schemas
525 .iter()
526 .filter(|(name, _)| !processed.contains(*name))
527 .collect();
528 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
529
530 for (_schema_name, schema) in remaining_schemas {
531 let rust_name = self.to_rust_type_name(&schema.name);
532 if !emitted_rust_names.insert(rust_name) {
533 continue;
534 }
535 let type_def = self.generate_type_definition(schema, analysis, &type_context)?;
536 if !type_def.is_empty() {
537 type_definitions.extend(type_def);
538 }
539 }
540
541 let base64_helper = if analysis
546 .used_type_features
547 .contains(crate::type_mapping::TypeFeature::Base64)
548 {
549 let engine = match self.config.types.byte {
550 crate::type_mapping::ByteStrategy::Base64UrlUnpadded => {
551 quote::format_ident!("URL_SAFE_NO_PAD")
552 }
553 _ => quote::format_ident!("STANDARD"),
554 };
555 quote! {
556 mod base64_serde {
561 use base64::{Engine as _, engine::general_purpose::#engine as ENGINE};
562 use serde::{Deserialize, Deserializer, Serializer};
563
564 pub fn serialize<S: Serializer>(
565 bytes: &Vec<u8>,
566 ser: S,
567 ) -> Result<S::Ok, S::Error> {
568 ser.serialize_str(&ENGINE.encode(bytes))
569 }
570
571 pub fn deserialize<'de, D: Deserializer<'de>>(
572 de: D,
573 ) -> Result<Vec<u8>, D::Error> {
574 let s = String::deserialize(de)?;
575 ENGINE
576 .decode(s.as_bytes())
577 .map_err(serde::de::Error::custom)
578 }
579
580 pub mod option {
586 use super::*;
587 use serde::{Deserialize, Deserializer, Serializer};
588
589 pub fn serialize<S: Serializer>(
590 opt: &Option<Vec<u8>>,
591 ser: S,
592 ) -> Result<S::Ok, S::Error> {
593 match opt {
594 Some(bytes) => super::serialize(bytes, ser),
595 None => ser.serialize_none(),
596 }
597 }
598
599 pub fn deserialize<'de, D: Deserializer<'de>>(
600 de: D,
601 ) -> Result<Option<Vec<u8>>, D::Error> {
602 let opt = Option::<String>::deserialize(de)?;
603 opt.map(|s| {
604 ENGINE
605 .decode(s.as_bytes())
606 .map_err(serde::de::Error::custom)
607 })
608 .transpose()
609 }
610 }
611 }
612 }
613 } else {
614 TokenStream::new()
615 };
616
617 let time_date_helper = if analysis
624 .used_type_features
625 .contains(crate::type_mapping::TypeFeature::TimeDate)
626 {
627 quote! {
628 time::serde::format_description!(
629 time_date_format,
630 Date,
631 "[year]-[month]-[day]"
632 );
633 }
634 } else {
635 TokenStream::new()
636 };
637
638 let time_time_helper = if analysis
643 .used_type_features
644 .contains(crate::type_mapping::TypeFeature::TimeTime)
645 {
646 quote! {
647 time::serde::format_description!(
648 version = 2,
649 time_time_format,
650 Time,
651 "[hour]:[minute]:[second][optional [.[subsecond]]]"
652 );
653 }
654 } else {
655 TokenStream::new()
656 };
657
658 let generated = quote! {
660 #provenance_attribute
666
667 #![allow(clippy::large_enum_variant)]
668 #![allow(clippy::format_in_format_args)]
669 #![allow(clippy::let_unit_value)]
670 #![allow(unreachable_patterns)]
671
672 use serde::{Deserialize, Serialize};
673
674 #base64_helper
675
676 #time_date_helper
677
678 #time_time_helper
679
680 #type_definitions
681 };
682
683 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
685 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
686 })?;
687
688 let formatted = prettyplease::unparse(&syntax_tree);
689
690 Ok(formatted)
691 }
692
693 fn generate_streaming_client(
695 &self,
696 streaming_config: &StreamingConfig,
697 analysis: &SchemaAnalysis,
698 ) -> Result<String> {
699 let mut client_code = TokenStream::new();
700 let provenance_attribute = self.provenance_attribute();
701
702 let imports = quote! {
704 #provenance_attribute
709 #![allow(clippy::format_in_format_args)]
710 #![allow(clippy::let_unit_value)]
711 #![allow(unused_mut)]
712
713 use super::types::*;
714 use async_trait::async_trait;
715 use futures_util::{Stream, StreamExt};
716 use std::pin::Pin;
717 use std::time::Duration;
718 use reqwest::header::{HeaderMap, HeaderValue};
719 use tracing::{debug, error, info, warn, instrument};
720 };
721 client_code.extend(imports);
722
723 if streaming_config.generate_client {
725 let error_types = self.generate_streaming_error_types()?;
726 client_code.extend(error_types);
727 }
728
729 for endpoint in &streaming_config.endpoints {
731 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
732 client_code.extend(trait_code);
733 }
734
735 if streaming_config.generate_client {
737 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
738 client_code.extend(client_impl);
739 }
740
741 if streaming_config.event_parser_helpers {
743 let parser_code = self.generate_sse_parser_utilities(streaming_config)?;
744 client_code.extend(parser_code);
745 }
746
747 if let Some(reconnect_config) = &streaming_config.reconnection_config {
749 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
750 client_code.extend(reconnect_code);
751 }
752
753 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
754 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
755 })?;
756
757 Ok(prettyplease::unparse(&syntax_tree))
758 }
759
760 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
766 let client_ids = self.resolve_client_operation_ids(analysis)?;
767 let operations = self.client_operations(analysis, client_ids.as_ref());
768 self.generate_http_client_for_operations(analysis, &operations)
769 }
770
771 fn generate_http_client_for_operations(
772 &self,
773 analysis: &SchemaAnalysis,
774 operations: &[&crate::analysis::OperationInfo],
775 ) -> Result<String> {
776 let provenance_attribute = self.provenance_attribute();
777 let error_types = self.generate_http_error_types();
778 let client_struct = self.generate_http_client_struct();
779 let operation_methods = self.generate_operation_methods_for(analysis, operations);
780
781 let generated = quote! {
782 #provenance_attribute
787 #![allow(clippy::format_in_format_args)]
788 #![allow(clippy::let_unit_value)]
789
790 use super::types::*;
791
792 #error_types
793
794 #client_struct
795
796 #operation_methods
797 };
798
799 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
800 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
801 })?;
802
803 Ok(prettyplease::unparse(&syntax_tree))
804 }
805
806 fn resolve_operation_scopes(&self, analysis: &SchemaAnalysis) -> Result<OperationScopes> {
807 let client_ids = if self.config.enable_async_client && !self.config.registry_only {
808 self.resolve_client_operation_ids(analysis)?
809 } else {
810 None
811 };
812
813 let server_ids = match &self.config.server {
814 Some(server) if !server.operations.is_empty() => {
815 crate::server::resolve_operation_selectors(&server.operations, analysis)
816 .map_err(|error| {
817 GeneratorError::ValidationError(format!(
818 "Invalid [server].operations: {error}"
819 ))
820 })?
821 .operations
822 .into_iter()
823 .map(|operation| operation.operation_id)
824 .collect()
825 }
826 _ => Default::default(),
827 };
828
829 let streaming_ids = if self.config.registry_only || !self.config.enable_sse_client {
830 Default::default()
831 } else if let Some(streaming) = &self.config.streaming_config {
832 let mut ids = std::collections::BTreeSet::new();
833 for (index, endpoint) in streaming.endpoints.iter().enumerate() {
834 let resolution =
835 crate::server::resolve_operation_id(&endpoint.operation_id, analysis).map_err(
836 |error| {
837 GeneratorError::ValidationError(format!(
838 "Invalid [streaming].endpoints[{index}].operation_id: {error}"
839 ))
840 },
841 )?;
842 ids.extend(
843 resolution
844 .operations
845 .into_iter()
846 .map(|operation| operation.operation_id),
847 );
848 }
849 ids
850 } else {
851 Default::default()
852 };
853
854 let client_prunes = self.config.enable_async_client
855 && !self.config.registry_only
856 && self
857 .config
858 .client
859 .as_ref()
860 .is_some_and(|client| client.prune_models);
861 let server_prunes = self
862 .config
863 .server
864 .as_ref()
865 .is_some_and(|server| server.prune_models && !server.operations.is_empty());
866 let extra_schema_roots = if self.config.registry_only || !self.config.enable_sse_client {
867 Vec::new()
868 } else {
869 self.config
870 .streaming_config
871 .as_ref()
872 .map(|streaming| {
873 streaming
874 .endpoints
875 .iter()
876 .map(|endpoint| endpoint.event_union_type.clone())
877 .collect()
878 })
879 .unwrap_or_default()
880 };
881
882 Ok(OperationScopes {
883 client_ids,
884 server_ids,
885 streaming_ids,
886 prune_models: client_prunes || server_prunes,
887 extra_schema_roots,
888 })
889 }
890
891 fn resolve_client_operation_ids(
892 &self,
893 analysis: &SchemaAnalysis,
894 ) -> Result<Option<std::collections::BTreeSet<String>>> {
895 match &self.config.client {
896 Some(client) if !client.operations.is_empty() => {
897 let resolution =
898 crate::server::resolve_operation_selectors(&client.operations, analysis)
899 .map_err(|error| {
900 GeneratorError::ValidationError(format!(
901 "Invalid [client].operations: {error}"
902 ))
903 })?;
904 Ok(Some(
905 resolution
906 .operations
907 .into_iter()
908 .map(|operation| operation.operation_id)
909 .collect(),
910 ))
911 }
912 _ => Ok(None),
913 }
914 }
915
916 fn client_operations<'a>(
917 &self,
918 analysis: &'a SchemaAnalysis,
919 selected: Option<&std::collections::BTreeSet<String>>,
920 ) -> Vec<&'a crate::analysis::OperationInfo> {
921 analysis
922 .operations
923 .iter()
924 .filter(|(operation_id, _)| selected.is_none_or(|ids| ids.contains(*operation_id)))
925 .map(|(_, operation)| operation)
926 .collect()
927 }
928
929 fn prune_models_to_scopes(
930 &self,
931 analysis: &mut SchemaAnalysis,
932 scopes: &OperationScopes,
933 ) -> usize {
934 if !scopes.prune_models {
935 return 0;
936 }
937
938 let mut consumer_ids = scopes.server_ids.clone();
939 if self.config.enable_async_client && !self.config.registry_only {
940 match &scopes.client_ids {
941 Some(ids) => consumer_ids.extend(ids.iter().cloned()),
942 None => consumer_ids.extend(analysis.operations.keys().cloned()),
943 }
944 }
945 consumer_ids.extend(scopes.streaming_ids.iter().cloned());
946
947 let operations: Vec<&crate::analysis::OperationInfo> = consumer_ids
948 .iter()
949 .filter_map(|operation_id| analysis.operations.get(operation_id))
950 .collect();
951 let keep = crate::server::codegen::reachable_schemas_with_roots(
952 analysis,
953 &operations,
954 &scopes.extra_schema_roots,
955 );
956 let before = analysis.schemas.len();
957 analysis.schemas.retain(|name, _| keep.contains(name));
958 before - analysis.schemas.len()
959 }
960
961 fn generate_http_error_types(&self) -> TokenStream {
963 quote! {
964 use thiserror::Error;
965
966 pub mod openapi_to_rust_problem {
969 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
970 pub struct ProblemDetails {
971 #[serde(rename = "type")]
972 pub type_uri: String,
973 pub title: String,
974 pub status: u16,
975 pub code: String,
976 #[serde(default)]
977 pub errors: Vec<InvalidParameter>,
978 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub detail: Option<String>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub instance: Option<String>,
982 }
983
984 #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
985 pub struct InvalidParameter {
986 pub code: String,
987 pub location: String,
988 pub message: String,
989 }
990 }
991
992 #[derive(Error, Debug)]
1000 pub enum HttpError {
1001 #[error("Network error: {0}")]
1003 Network(#[from] reqwest::Error),
1004
1005 #[error("Middleware error: {0}")]
1007 Middleware(#[from] reqwest_middleware::Error),
1008
1009 #[error("Failed to serialize request: {0}")]
1011 Serialization(String),
1012
1013 #[error("Authentication error: {0}")]
1015 Auth(String),
1016
1017 #[error("Request timeout")]
1019 Timeout,
1020
1021 #[error("Configuration error: {0}")]
1023 Config(String),
1024
1025 #[error("{0}")]
1027 Other(String),
1028 }
1029
1030 impl HttpError {
1031 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
1033 Self::Serialization(error.to_string())
1034 }
1035
1036 pub fn is_retryable(&self) -> bool {
1038 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
1039 }
1040 }
1041
1042 #[derive(Debug, Clone)]
1054 pub struct ApiError<E> {
1055 pub status: u16,
1056 pub headers: reqwest::header::HeaderMap,
1057 pub body: String,
1058 pub typed: Option<E>,
1059 pub parse_error: Option<String>,
1060 }
1061
1062 const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
1063 const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
1064
1065 fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
1066 let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
1067 return std::borrow::Cow::Borrowed(body);
1068 };
1069
1070 let mut displayed =
1071 String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1072 displayed.push_str(&body[..end]);
1073 displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1074 std::borrow::Cow::Owned(displayed)
1075 }
1076
1077 impl<E> ApiError<E> {
1078 pub fn is_client_error(&self) -> bool {
1079 (400..500).contains(&self.status)
1080 }
1081
1082 pub fn is_server_error(&self) -> bool {
1083 (500..600).contains(&self.status)
1084 }
1085
1086 pub fn is_retryable(&self) -> bool {
1089 matches!(self.status, 429 | 500 | 502 | 503 | 504)
1090 }
1091
1092 pub fn problem_details(
1104 &self,
1105 ) -> Option<openapi_to_rust_problem::ProblemDetails> {
1106 let content_type = self
1107 .headers
1108 .get(reqwest::header::CONTENT_TYPE)?
1109 .to_str()
1110 .ok()?;
1111 let media_type = content_type.split(';').next()?.trim();
1112 if !media_type.eq_ignore_ascii_case("application/problem+json") {
1113 return None;
1114 }
1115 serde_json::from_str(&self.body).ok()
1116 }
1117 }
1118
1119 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
1120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1121 write!(
1122 f,
1123 "API error {}: {}",
1124 self.status,
1125 display_api_error_body(&self.body)
1126 )?;
1127
1128 if let Some(typed) = &self.typed {
1129 write!(f, "; typed: {typed:?}")?;
1130 }
1131
1132 if let Some(parse_error) = &self.parse_error {
1133 write!(f, "; parse error: {parse_error}")?;
1134 }
1135
1136 Ok(())
1137 }
1138 }
1139
1140 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
1141
1142 #[derive(Debug, Error)]
1150 pub enum ApiOpError<E: std::fmt::Debug> {
1151 #[error(transparent)]
1152 Transport(#[from] HttpError),
1153
1154 #[error(transparent)]
1155 Api(ApiError<E>),
1156 }
1157
1158 impl<E: std::fmt::Debug> ApiOpError<E> {
1159 pub fn api(&self) -> Option<&ApiError<E>> {
1161 match self {
1162 Self::Api(e) => Some(e),
1163 Self::Transport(_) => None,
1164 }
1165 }
1166
1167 pub fn is_api_error(&self) -> bool {
1170 matches!(self, Self::Api(_))
1171 }
1172 }
1173
1174 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
1177 fn from(e: reqwest::Error) -> Self {
1178 Self::Transport(HttpError::Network(e))
1179 }
1180 }
1181
1182 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
1183 fn from(e: reqwest_middleware::Error) -> Self {
1184 Self::Transport(HttpError::Middleware(e))
1185 }
1186 }
1187
1188 pub type HttpResult<T> = Result<T, HttpError>;
1192 }
1193 }
1194
1195 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
1197 let mut module_names = std::collections::BTreeSet::new();
1198
1199 for file in files {
1200 let module_name = if file.path.components().count() > 1 {
1201 file.path.iter().next().and_then(|part| part.to_str())
1202 } else {
1203 file.path.file_stem().and_then(|stem| stem.to_str())
1204 };
1205 if let Some(module_name) = module_name.filter(|name| *name != "mod") {
1206 module_names.insert(module_name.to_string());
1207 }
1208 }
1209 let module_declarations = module_names
1210 .iter()
1211 .map(|name| format!("pub mod {name};"))
1212 .collect::<Vec<_>>();
1213 let pub_uses = module_names
1214 .iter()
1215 .map(|name| format!("pub use {name}::*;"))
1216 .collect::<Vec<_>>();
1217
1218 let mount_hint = format!(
1225 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
1226 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
1227 name = self.config.module_name,
1228 );
1229 let source_hint = self
1230 .source_provenance
1231 .as_ref()
1232 .map(|source| {
1233 format!(
1234 "//! Generated by openapi-to-rust v{}. Source OpenAPI document: {source}\n",
1235 env!("CARGO_PKG_VERSION")
1236 )
1237 })
1238 .unwrap_or_default();
1239
1240 let content = format!(
1241 r#"//! Generated API modules
1242//!
1243//! This module exports all generated API types and clients.
1244//! Do not edit manually - regenerate using the appropriate script.
1245//!
1246{source_hint}
1247{mount_hint}
1248#![allow(unused_imports)]
1249
1250{decls}
1251
1252{uses}
1253"#,
1254 mount_hint = mount_hint,
1255 source_hint = source_hint,
1256 decls = module_declarations.join("\n"),
1257 uses = pub_uses.join("\n"),
1258 );
1259
1260 Ok(content)
1261 }
1262
1263 pub fn output_artifacts(
1265 &self,
1266 result: &GenerationResult,
1267 ) -> std::collections::BTreeMap<PathBuf, String> {
1268 let mut artifacts = std::collections::BTreeMap::new();
1269 for file in &result.files {
1270 artifacts.insert(file.path.clone(), file.content.clone());
1271 }
1272 artifacts.insert(
1273 result.mod_file.path.clone(),
1274 result.mod_file.content.clone(),
1275 );
1276 if let Some(mut fragment) =
1277 crate::type_mapping::render_required_deps_toml(&result.required_deps)
1278 {
1279 if let Some(source) = &self.source_provenance {
1280 let header = format!(
1281 "# Generated by openapi-to-rust v{}. Source OpenAPI document: {source}",
1282 env!("CARGO_PKG_VERSION")
1283 );
1284 fragment = fragment.replacen("# Generated by openapi-to-rust.", &header, 1);
1285 }
1286 artifacts.insert(PathBuf::from("REQUIRED_DEPS.toml"), fragment);
1287 }
1288 artifacts
1289 }
1290
1291 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
1294 use std::fs;
1295
1296 fs::create_dir_all(&self.config.output_dir)?;
1298
1299 let artifacts = self.output_artifacts(result);
1300 for (relative, content) in &artifacts {
1301 let file_path = self.config.output_dir.join(relative);
1302 if let Some(parent) = file_path.parent() {
1303 fs::create_dir_all(parent)?;
1304 }
1305 fs::write(&file_path, content)?;
1306 }
1307
1308 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
1309 if !artifacts.contains_key(std::path::Path::new("REQUIRED_DEPS.toml")) && deps_path.exists()
1310 {
1311 fs::remove_file(&deps_path)?;
1312 }
1313
1314 Ok(())
1315 }
1316
1317 fn generate_type_definition(
1318 &self,
1319 schema: &crate::analysis::AnalyzedSchema,
1320 analysis: &crate::analysis::SchemaAnalysis,
1321 type_context: &TypeGenerationContext<'_>,
1322 ) -> Result<TokenStream> {
1323 use crate::analysis::SchemaType;
1324
1325 match &schema.schema_type {
1326 SchemaType::Primitive { rust_type, .. } => {
1327 self.generate_type_alias(schema, rust_type)
1329 }
1330 SchemaType::StringEnum { values } => {
1331 let ext = analysis.enum_extensions.get(&schema.name);
1332 let rust_name = self.to_rust_type_name(&schema.name);
1339 let force_extensible = self
1340 .config
1341 .extensible_enum_overrides
1342 .get(&schema.name)
1343 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
1344 .copied()
1345 .unwrap_or(false);
1346 if force_extensible {
1347 self.generate_extensible_enum(schema, values, ext)
1348 } else {
1349 self.generate_string_enum(schema, values, ext)
1350 }
1351 }
1352 SchemaType::ExtensibleEnum { known_values } => {
1353 let ext = analysis.enum_extensions.get(&schema.name);
1354 self.generate_extensible_enum(schema, known_values, ext)
1355 }
1356 SchemaType::Object {
1357 properties,
1358 required,
1359 additional_properties,
1360 } => self.generate_struct(
1361 schema,
1362 properties,
1363 required,
1364 additional_properties,
1365 analysis,
1366 type_context,
1367 ),
1368 SchemaType::DiscriminatedUnion {
1369 discriminator_field,
1370 variants,
1371 } => {
1372 if self.should_use_untagged_discriminated_union(schema, analysis) {
1374 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1376 .iter()
1377 .map(|v| crate::analysis::SchemaRef {
1378 target: v.type_name.clone(),
1379 nullable: false,
1380 })
1381 .collect();
1382 self.generate_union_enum(schema, &schema_refs, analysis)
1383 } else {
1384 self.generate_discriminated_enum(
1385 schema,
1386 discriminator_field,
1387 variants,
1388 analysis,
1389 )
1390 }
1391 }
1392 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
1393 SchemaType::Reference { target } => {
1394 if schema.name != *target {
1397 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1399 let target_type = format_ident!("{}", self.to_rust_type_name(target));
1400
1401 let doc_comment = if let Some(desc) = &schema.description {
1402 quote! { #[doc = #desc] }
1403 } else {
1404 TokenStream::new()
1405 };
1406
1407 Ok(quote! {
1408 #doc_comment
1409 pub type #alias_name = #target_type;
1410 })
1411 } else {
1412 Ok(TokenStream::new())
1414 }
1415 }
1416 SchemaType::Array { item_type } => {
1417 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1425
1426 if let SchemaType::Reference { target } = item_type.as_ref() {
1428 if let Some(info) = type_context.discriminated_variants.get(target) {
1429 if !info.is_parent_untagged {
1430 let wrapper_name =
1432 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
1433 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
1434 let disc_field = &info.discriminator_field;
1435 let disc_value = &info.discriminator_value;
1436
1437 let doc_comment = if let Some(desc) = &schema.description {
1438 quote! { #[doc = #desc] }
1439 } else {
1440 TokenStream::new()
1441 };
1442
1443 return Ok(quote! {
1444 #[derive(Debug, Clone, Deserialize, Serialize)]
1448 #[serde(tag = #disc_field)]
1449 pub enum #wrapper_name {
1450 #[serde(rename = #disc_value)]
1451 #variant_type(#variant_type),
1452 }
1453 #doc_comment
1454 pub type #array_name = Vec<#wrapper_name>;
1455 });
1456 }
1457 }
1458 }
1459
1460 let inner_type = self.generate_array_item_type(item_type, analysis);
1461
1462 let doc_comment = if let Some(desc) = &schema.description {
1463 quote! { #[doc = #desc] }
1464 } else {
1465 TokenStream::new()
1466 };
1467
1468 Ok(quote! {
1469 #doc_comment
1470 pub type #array_name = Vec<#inner_type>;
1471 })
1472 }
1473 SchemaType::Composition { schemas } => {
1474 self.generate_composition_struct(schema, schemas)
1475 }
1476 }
1477 }
1478
1479 fn generate_type_alias(
1480 &self,
1481 schema: &crate::analysis::AnalyzedSchema,
1482 rust_type: &str,
1483 ) -> Result<TokenStream> {
1484 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1485 let base_type = parse_rust_type(rust_type)?;
1489
1490 let doc_comment = if let Some(desc) = &schema.description {
1491 let sanitized_desc = self.sanitize_doc_comment(desc);
1492 quote! { #[doc = #sanitized_desc] }
1493 } else {
1494 TokenStream::new()
1495 };
1496
1497 Ok(quote! {
1498 #doc_comment
1499 pub type #type_name = #base_type;
1500 })
1501 }
1502
1503 fn generate_extensible_enum(
1504 &self,
1505 schema: &crate::analysis::AnalyzedSchema,
1506 known_values: &[String],
1507 ext: Option<&crate::analysis::EnumExtensions>,
1508 ) -> Result<TokenStream> {
1509 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1510
1511 let doc_comment = if let Some(desc) = &schema.description {
1512 quote! { #[doc = #desc] }
1513 } else {
1514 TokenStream::new()
1515 };
1516
1517 let varnames_override: Option<&Vec<String>> = ext
1521 .filter(|_| self.config.types.x_enum_varnames_enabled())
1522 .map(|e| &e.varnames)
1523 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1524 let descriptions_override: Option<&Vec<String>> = ext
1525 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1526 .map(|e| &e.descriptions)
1527 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1528
1529 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1530 let name = match varnames_override {
1531 Some(v) => v[index].clone(),
1532 None => self.to_rust_enum_variant(value),
1533 };
1534 format_ident!("{}", name)
1535 };
1536
1537 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1542 let variant_ident = variant_ident_for(i, value);
1543 let doc = descriptions_override
1544 .map(|d| {
1545 let s = self.sanitize_doc_comment(&d[i]);
1546 quote! { #[doc = #s] }
1547 })
1548 .unwrap_or_default();
1549 quote! {
1550 #doc
1551 #variant_ident,
1552 }
1553 });
1554
1555 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1556 let variant_ident = variant_ident_for(i, value);
1557 quote! {
1558 #value => Ok(#enum_name::#variant_ident),
1559 }
1560 });
1561
1562 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1563 let variant_ident = variant_ident_for(i, value);
1564 quote! {
1565 #enum_name::#variant_ident => #value,
1566 }
1567 });
1568
1569 let derives = if self.config.enable_specta {
1570 quote! {
1571 #[derive(Debug, Clone, PartialEq, Eq)]
1572 #[cfg_attr(feature = "specta", derive(specta::Type))]
1573 }
1574 } else {
1575 quote! {
1576 #[derive(Debug, Clone, PartialEq, Eq)]
1577 }
1578 };
1579
1580 Ok(quote! {
1581 #doc_comment
1582 #derives
1583 pub enum #enum_name {
1584 #(#known_variants)*
1585 Custom(String),
1587 }
1588
1589 impl<'de> serde::Deserialize<'de> for #enum_name {
1590 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1591 where
1592 D: serde::Deserializer<'de>,
1593 {
1594 let value = String::deserialize(deserializer)?;
1595 match value.as_str() {
1596 #(#match_arms_de)*
1597 _ => Ok(#enum_name::Custom(value)),
1598 }
1599 }
1600 }
1601
1602 impl serde::Serialize for #enum_name {
1603 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1604 where
1605 S: serde::Serializer,
1606 {
1607 let value = match self {
1608 #(#match_arms_ser)*
1609 #enum_name::Custom(s) => s.as_str(),
1610 };
1611 serializer.serialize_str(value)
1612 }
1613 }
1614 })
1615 }
1616
1617 fn generate_string_enum(
1618 &self,
1619 schema: &crate::analysis::AnalyzedSchema,
1620 values: &[String],
1621 ext: Option<&crate::analysis::EnumExtensions>,
1622 ) -> Result<TokenStream> {
1623 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1624
1625 let default_value = schema
1632 .default
1633 .as_ref()
1634 .and_then(|v| v.as_str())
1635 .map(|s| s.to_string());
1636 let has_default_match = match &default_value {
1637 Some(d) => values.iter().any(|v| v == d),
1638 None => !values.is_empty(),
1639 };
1640
1641 let varnames_override: Option<&Vec<String>> = ext
1645 .filter(|_| self.config.types.x_enum_varnames_enabled())
1646 .map(|e| &e.varnames)
1647 .filter(|v| !v.is_empty() && v.len() == values.len());
1648 let descriptions_override: Option<&Vec<String>> = ext
1649 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1650 .map(|e| &e.descriptions)
1651 .filter(|v| !v.is_empty() && v.len() == values.len());
1652
1653 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1660 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1661 .iter()
1662 .enumerate()
1663 .map(|(i, value)| {
1664 let base = match varnames_override {
1665 Some(v) => v[i].clone(),
1666 None => self.to_rust_enum_variant(value),
1667 };
1668 let mut variant_name = base.clone();
1669 let mut suffix = 2;
1670 while !used.insert(variant_name.clone()) {
1671 variant_name = format!("{base}_{suffix}");
1672 suffix += 1;
1673 }
1674 let variant_ident = format_ident!("{}", variant_name);
1675 let is_default = if let Some(ref default) = default_value {
1676 value == default
1677 } else {
1678 i == 0
1679 };
1680 let description = descriptions_override.map(|d| d[i].clone());
1681 (variant_ident, value, is_default, description)
1682 })
1683 .collect();
1684
1685 let variants =
1686 variant_pairs
1687 .iter()
1688 .map(|(variant_ident, value, is_default, description)| {
1689 let doc = description
1690 .as_ref()
1691 .map(|d| {
1692 let s = self.sanitize_doc_comment(d);
1693 quote! { #[doc = #s] }
1694 })
1695 .unwrap_or_default();
1696 if *is_default {
1697 quote! {
1698 #doc
1699 #[default]
1700 #[serde(rename = #value)]
1701 #variant_ident,
1702 }
1703 } else {
1704 quote! {
1705 #doc
1706 #[serde(rename = #value)]
1707 #variant_ident,
1708 }
1709 }
1710 });
1711
1712 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1716 quote! { Self::#variant_ident => #value, }
1717 });
1718
1719 let doc_comment = if let Some(desc) = &schema.description {
1720 quote! { #[doc = #desc] }
1721 } else {
1722 TokenStream::new()
1723 };
1724
1725 let derives = match (self.config.enable_specta, has_default_match) {
1728 (true, true) => quote! {
1729 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1730 #[cfg_attr(feature = "specta", derive(specta::Type))]
1731 },
1732 (true, false) => quote! {
1733 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1734 #[cfg_attr(feature = "specta", derive(specta::Type))]
1735 },
1736 (false, true) => quote! {
1737 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1738 },
1739 (false, false) => quote! {
1740 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1741 },
1742 };
1743
1744 Ok(quote! {
1745 #doc_comment
1746 #derives
1747 pub enum #enum_name {
1748 #(#variants)*
1749 }
1750
1751 impl #enum_name {
1752 pub fn as_str(&self) -> &'static str {
1753 match self {
1754 #(#as_str_arms)*
1755 }
1756 }
1757 }
1758
1759 impl ::std::fmt::Display for #enum_name {
1760 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1761 f.write_str(self.as_str())
1762 }
1763 }
1764
1765 impl AsRef<str> for #enum_name {
1766 fn as_ref(&self) -> &str {
1767 self.as_str()
1768 }
1769 }
1770 })
1771 }
1772
1773 fn generate_struct(
1774 &self,
1775 schema: &crate::analysis::AnalyzedSchema,
1776 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1777 required: &std::collections::HashSet<String>,
1778 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1779 analysis: &crate::analysis::SchemaAnalysis,
1780 type_context: &TypeGenerationContext<'_>,
1781 ) -> Result<TokenStream> {
1782 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1783 let emitted_properties = self.emitted_object_properties(
1784 &schema.name,
1785 properties,
1786 required,
1787 additional_properties,
1788 analysis,
1789 type_context.discriminated_variants.get(&schema.name),
1790 );
1791
1792 let mut fields: Vec<TokenStream> = emitted_properties
1793 .iter()
1794 .map(|emitted| {
1795 let field_name = emitted.wire_name;
1796 let property = emitted.property;
1797 let field_ident = &emitted.ident;
1798 let field_type = &emitted.field_type;
1799 let serde_attrs = self.generate_serde_field_attrs(
1800 &schema.name,
1801 field_name,
1802 field_ident,
1803 property,
1804 emitted.is_required,
1805 analysis,
1806 );
1807 let specta_attrs = self.generate_specta_field_attrs(field_name);
1808
1809 let doc_comment = if let Some(desc) = &property.description {
1810 let sanitized_desc = self.sanitize_doc_comment(desc);
1811 quote! { #[doc = #sanitized_desc] }
1812 } else {
1813 TokenStream::new()
1814 };
1815 let constraint_doc = self.generate_constraint_doc(&property.constraints);
1816
1817 quote! {
1818 #doc_comment
1819 #constraint_doc
1820 #serde_attrs
1821 #specta_attrs
1822 pub #field_ident: #field_type,
1823 }
1824 })
1825 .collect();
1826
1827 match additional_properties {
1833 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1834 crate::analysis::ObjectAdditionalProperties::Untyped => {
1835 fields.push(quote! {
1836 #[serde(flatten)]
1838 pub additional_properties:
1839 std::collections::BTreeMap<String, serde_json::Value>,
1840 });
1841 }
1842 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1843 let value_tokens = self.generate_array_item_type(value_type, analysis);
1844 fields.push(quote! {
1845 #[serde(flatten)]
1848 pub additional_properties:
1849 std::collections::BTreeMap<String, #value_tokens>,
1850 });
1851 }
1852 }
1853
1854 let doc_comment = if let Some(desc) = &schema.description {
1855 quote! { #[doc = #desc] }
1856 } else {
1857 TokenStream::new()
1858 };
1859
1860 let can_derive_default = emitted_properties
1866 .iter()
1867 .all(|property| !property.is_required);
1868
1869 let derives = match (self.config.enable_specta, can_derive_default) {
1873 (true, true) => quote! {
1874 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1875 #[cfg_attr(feature = "specta", derive(specta::Type))]
1876 },
1877 (true, false) => quote! {
1878 #[derive(Debug, Clone, Deserialize, Serialize)]
1879 #[cfg_attr(feature = "specta", derive(specta::Type))]
1880 },
1881 (false, true) => quote! {
1882 #[derive(Debug, Clone, Deserialize, Serialize, Default)]
1883 },
1884 (false, false) => quote! {
1885 #[derive(Debug, Clone, Deserialize, Serialize)]
1886 },
1887 };
1888
1889 let builder = if type_context.index.request_body_roots.contains(&schema.name)
1890 && emitted_properties
1891 .iter()
1892 .any(|property| property.is_required)
1893 && (emitted_properties
1894 .iter()
1895 .any(|property| !property.is_required)
1896 || !matches!(
1897 additional_properties,
1898 crate::analysis::ObjectAdditionalProperties::Forbidden
1899 )) {
1900 self.generate_request_model_builder(
1901 schema,
1902 &emitted_properties,
1903 additional_properties,
1904 analysis,
1905 type_context.index,
1906 )
1907 } else {
1908 TokenStream::new()
1909 };
1910
1911 Ok(quote! {
1912 #doc_comment
1913 #derives
1914 pub struct #struct_name {
1915 #(#fields)*
1916 }
1917
1918 #builder
1919 })
1920 }
1921
1922 pub(crate) fn emitted_object_properties<'a>(
1927 &self,
1928 schema_name: &str,
1929 properties: &'a BTreeMap<String, crate::analysis::PropertyInfo>,
1930 required: &std::collections::HashSet<String>,
1931 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1932 analysis: &crate::analysis::SchemaAnalysis,
1933 discriminator_info: Option<&DiscriminatedVariantInfo>,
1934 ) -> Vec<EmittedObjectProperty<'a>> {
1935 let mut sorted_properties: Vec<_> = properties.iter().collect();
1936 sorted_properties.sort_by_key(|(name, _)| name.as_str());
1937
1938 let mut used_field_idents = std::collections::HashSet::new();
1939 if !matches!(
1940 additional_properties,
1941 crate::analysis::ObjectAdditionalProperties::Forbidden
1942 ) {
1943 used_field_idents.insert("additional_properties".to_string());
1944 }
1945
1946 let mut emitted = Vec::new();
1947 for (field_name, property) in sorted_properties {
1948 if discriminator_info.is_some_and(|info| {
1949 !info.is_parent_untagged && field_name.as_str() == info.discriminator_field.as_str()
1950 }) {
1951 continue;
1952 }
1953
1954 let raw = self.to_rust_field_name(field_name);
1955 let mut chosen = raw.clone();
1956 let mut suffix = 2;
1957 while !used_field_idents.insert(chosen.clone()) {
1958 chosen = format!("{raw}_{suffix}");
1959 suffix += 1;
1960 }
1961 let is_required = required.contains(field_name);
1962 emitted.push(EmittedObjectProperty {
1963 wire_name: field_name,
1964 property,
1965 ident: Self::to_field_ident(&chosen),
1966 is_required,
1967 field_type: self.generate_field_type(
1968 schema_name,
1969 field_name,
1970 property,
1971 is_required,
1972 analysis,
1973 ),
1974 });
1975 }
1976 emitted
1977 }
1978
1979 fn type_generation_index(
1980 &self,
1981 analysis: &crate::analysis::SchemaAnalysis,
1982 ) -> TypeGenerationIndex {
1983 let reserved_type_names = analysis
1984 .schemas
1985 .keys()
1986 .map(|name| self.to_rust_type_name(name))
1987 .collect();
1988 let mut request_body_roots = std::collections::HashSet::new();
1989 for operation in analysis.operations.values() {
1990 let Some(mut current) = operation
1991 .request_body
1992 .as_ref()
1993 .and_then(crate::analysis::RequestBodyContent::schema_name)
1994 else {
1995 continue;
1996 };
1997 while request_body_roots.insert(current.to_string()) {
1998 let Some(crate::analysis::AnalyzedSchema {
1999 schema_type: crate::analysis::SchemaType::Reference { target },
2000 ..
2001 }) = analysis.schemas.get(current)
2002 else {
2003 break;
2004 };
2005 current = target;
2006 }
2007 }
2008 TypeGenerationIndex {
2009 request_body_roots,
2010 reserved_type_names,
2011 }
2012 }
2013
2014 fn generate_request_model_builder(
2015 &self,
2016 schema: &crate::analysis::AnalyzedSchema,
2017 properties: &[EmittedObjectProperty<'_>],
2018 additional_properties: &crate::analysis::ObjectAdditionalProperties,
2019 analysis: &crate::analysis::SchemaAnalysis,
2020 type_index: &TypeGenerationIndex,
2021 ) -> TokenStream {
2022 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2023 let builder_base = format!("{}Builder", struct_name);
2024 let mut builder_name = builder_base.clone();
2025 let mut suffix = 2;
2026 while type_index.reserved_type_names.contains(&builder_name) {
2027 builder_name = format!("{builder_base}{suffix}");
2028 suffix += 1;
2029 }
2030 let builder_name = format_ident!("{builder_name}");
2031
2032 let required_parameters: Vec<TokenStream> = properties
2033 .iter()
2034 .filter(|property| property.is_required)
2035 .map(|property| {
2036 let ident = &property.ident;
2037 let field_type = &property.field_type;
2038 quote! { #ident: #field_type }
2039 })
2040 .collect();
2041 let required_idents: Vec<&syn::Ident> = properties
2042 .iter()
2043 .filter(|property| property.is_required)
2044 .map(|property| &property.ident)
2045 .collect();
2046 let optional_initializers: Vec<TokenStream> = properties
2047 .iter()
2048 .filter(|property| !property.is_required)
2049 .map(|property| {
2050 let ident = &property.ident;
2051 quote! { #ident: None }
2052 })
2053 .collect();
2054
2055 let additional_initializer = match additional_properties {
2056 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2057 crate::analysis::ObjectAdditionalProperties::Untyped
2058 | crate::analysis::ObjectAdditionalProperties::Typed { .. } => quote! {
2059 additional_properties: ::std::collections::BTreeMap::new(),
2060 },
2061 };
2062
2063 let mut used_builder_methods =
2064 std::collections::HashSet::from(["new".to_string(), "build".to_string()]);
2065 if !matches!(
2066 additional_properties,
2067 crate::analysis::ObjectAdditionalProperties::Forbidden
2068 ) {
2069 used_builder_methods.insert("additional_properties".to_string());
2070 }
2071 let optional_setters: Vec<TokenStream> = properties
2072 .iter()
2073 .filter(|property| !property.is_required)
2074 .map(|property| {
2075 let field_ident = &property.ident;
2076 let field_type = self.generate_property_base_type(
2077 &schema.name,
2078 property.wire_name,
2079 property.property,
2080 analysis,
2081 );
2082 let field_name = field_ident.to_string();
2086 let plain_field_name = field_name.strip_prefix("r#").unwrap_or(&field_name);
2087 let mut setter_name = if matches!(plain_field_name, "new" | "build") {
2088 format!("with_{plain_field_name}")
2089 } else {
2090 field_name.clone()
2091 };
2092 let setter_base = setter_name.clone();
2093 let mut suffix = 2;
2094 while !used_builder_methods.insert(setter_name.clone()) {
2095 setter_name = format!("{setter_base}_{suffix}");
2096 suffix += 1;
2097 }
2098 let setter_ident = Self::to_field_ident(&setter_name);
2099 let wire_name = property.wire_name;
2100 quote! {
2101 #[doc = concat!("Set the optional `", #wire_name, "` request field.")]
2102 #[must_use]
2103 pub fn #setter_ident(mut self, #field_ident: #field_type) -> Self {
2104 self.value.#field_ident = Some(#field_ident);
2105 self
2106 }
2107 }
2108 })
2109 .collect();
2110
2111 let additional_setter = match additional_properties {
2112 crate::analysis::ObjectAdditionalProperties::Forbidden => TokenStream::new(),
2113 crate::analysis::ObjectAdditionalProperties::Untyped => quote! {
2114 #[must_use]
2116 pub fn additional_properties(
2117 mut self,
2118 additional_properties: ::std::collections::BTreeMap<
2119 String,
2120 serde_json::Value,
2121 >,
2122 ) -> Self {
2123 self.value.additional_properties = additional_properties;
2124 self
2125 }
2126 },
2127 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
2128 let value_type = self.generate_array_item_type(value_type, analysis);
2129 quote! {
2130 #[must_use]
2132 pub fn additional_properties(
2133 mut self,
2134 additional_properties: ::std::collections::BTreeMap<
2135 String,
2136 #value_type,
2137 >,
2138 ) -> Self {
2139 self.value.additional_properties = additional_properties;
2140 self
2141 }
2142 }
2143 }
2144 };
2145
2146 quote! {
2147 impl #struct_name {
2148 pub fn new(#(#required_parameters),*) -> Self {
2150 Self {
2151 #(#required_idents,)*
2152 #(#optional_initializers,)*
2153 #additional_initializer
2154 }
2155 }
2156
2157 pub fn builder(#(#required_parameters),*) -> #builder_name {
2159 #builder_name::new(#(#required_idents),*)
2160 }
2161 }
2162
2163 #[derive(Debug, Clone)]
2165 #[must_use]
2166 pub struct #builder_name {
2167 value: #struct_name,
2168 }
2169
2170 impl #builder_name {
2171 pub fn new(#(#required_parameters),*) -> Self {
2173 Self {
2174 value: #struct_name::new(#(#required_idents),*),
2175 }
2176 }
2177
2178 #(#optional_setters)*
2179 #additional_setter
2180
2181 pub fn build(self) -> #struct_name {
2183 self.value
2184 }
2185 }
2186 }
2187 }
2188
2189 fn generate_discriminated_enum(
2190 &self,
2191 schema: &crate::analysis::AnalyzedSchema,
2192 discriminator_field: &str,
2193 variants: &[crate::analysis::UnionVariant],
2194 analysis: &crate::analysis::SchemaAnalysis,
2195 ) -> Result<TokenStream> {
2196 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2197
2198 let has_nested_discriminated_union = variants.iter().any(|variant| {
2200 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
2201 matches!(
2202 variant_schema.schema_type,
2203 crate::analysis::SchemaType::DiscriminatedUnion { .. }
2204 )
2205 } else {
2206 false
2207 }
2208 });
2209
2210 if has_nested_discriminated_union {
2212 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
2214 .iter()
2215 .map(|v| crate::analysis::SchemaRef {
2216 target: v.type_name.clone(),
2217 nullable: false,
2218 })
2219 .collect();
2220 return self.generate_union_enum(schema, &schema_refs, analysis);
2221 }
2222
2223 let enclosing = self.to_rust_type_name(&schema.name);
2224 let enum_variants = variants.iter().map(|variant| {
2225 let variant_name = format_ident!("{}", variant.rust_name);
2226 let variant_value = &variant.discriminator_value;
2227
2228 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
2229 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
2233 || analysis
2234 .dependencies
2235 .recursive_schemas
2236 .contains(&variant.type_name)
2237 {
2238 quote! { Box<#variant_type> }
2239 } else {
2240 quote! { #variant_type }
2241 };
2242 quote! {
2243 #[serde(rename = #variant_value)]
2244 #variant_name(#payload),
2245 }
2246 });
2247
2248 let doc_comment = if let Some(desc) = &schema.description {
2249 quote! { #[doc = #desc] }
2250 } else {
2251 TokenStream::new()
2252 };
2253
2254 let derives = if self.config.enable_specta {
2256 quote! {
2257 #[derive(Debug, Clone, Deserialize, Serialize)]
2258 #[cfg_attr(feature = "specta", derive(specta::Type))]
2259 #[serde(tag = #discriminator_field)]
2260 }
2261 } else {
2262 quote! {
2263 #[derive(Debug, Clone, Deserialize, Serialize)]
2264 #[serde(tag = #discriminator_field)]
2265 }
2266 };
2267
2268 Ok(quote! {
2269 #doc_comment
2270 #derives
2271 pub enum #enum_name {
2272 #(#enum_variants)*
2273 }
2274 })
2275 }
2276
2277 fn should_use_untagged_discriminated_union(
2279 &self,
2280 schema: &crate::analysis::AnalyzedSchema,
2281 analysis: &crate::analysis::SchemaAnalysis,
2282 ) -> bool {
2283 for other_schema in analysis.schemas.values() {
2288 if let crate::analysis::SchemaType::DiscriminatedUnion {
2289 variants,
2290 discriminator_field: _,
2291 } = &other_schema.schema_type
2292 {
2293 for variant in variants {
2294 if variant.type_name == schema.name {
2295 if let crate::analysis::SchemaType::DiscriminatedUnion {
2300 discriminator_field: current_discriminator,
2301 variants: current_variants,
2302 ..
2303 } = &schema.schema_type
2304 {
2305 for current_variant in current_variants {
2307 if let Some(variant_schema) =
2308 analysis.schemas.get(¤t_variant.type_name)
2309 {
2310 if let crate::analysis::SchemaType::Object {
2311 properties, ..
2312 } = &variant_schema.schema_type
2313 {
2314 if properties.contains_key(current_discriminator) {
2315 return false;
2318 }
2319 }
2320 }
2321 }
2322 }
2323
2324 return true;
2326 }
2327 }
2328 }
2329 }
2330 false
2331 }
2332
2333 fn generate_union_enum(
2334 &self,
2335 schema: &crate::analysis::AnalyzedSchema,
2336 variants: &[crate::analysis::SchemaRef],
2337 analysis: &crate::analysis::SchemaAnalysis,
2338 ) -> Result<TokenStream> {
2339 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2340
2341 let mut used_variant_names = std::collections::HashSet::new();
2343 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
2344 let base_variant_name = self.type_name_to_variant_name(&variant.target);
2346 let variant_name = self.ensure_unique_variant_name_generator(
2347 base_variant_name,
2348 &mut used_variant_names,
2349 i,
2350 );
2351 let variant_name_ident = format_ident!("{}", variant_name);
2352
2353 let variant_type_tokens = if matches!(
2355 variant.target.as_str(),
2356 "bool"
2357 | "i8"
2358 | "i16"
2359 | "i32"
2360 | "i64"
2361 | "i128"
2362 | "u8"
2363 | "u16"
2364 | "u32"
2365 | "u64"
2366 | "u128"
2367 | "f32"
2368 | "f64"
2369 | "String"
2370 ) {
2371 let type_ident = format_ident!("{}", variant.target);
2372 quote! { #type_ident }
2373 } else if variant.target == "serde_json::Value" {
2374 quote! { serde_json::Value }
2377 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
2378 let inner = &variant.target[4..variant.target.len() - 1];
2380
2381 if inner.starts_with("Vec<") && inner.ends_with(">") {
2383 let inner_inner = &inner[4..inner.len() - 1];
2384 if inner_inner == "serde_json::Value" {
2385 quote! { Vec<Vec<serde_json::Value>> }
2386 } else {
2387 let inner_inner_type = if matches!(
2388 inner_inner,
2389 "bool"
2390 | "i8"
2391 | "i16"
2392 | "i32"
2393 | "i64"
2394 | "i128"
2395 | "u8"
2396 | "u16"
2397 | "u32"
2398 | "u64"
2399 | "u128"
2400 | "f32"
2401 | "f64"
2402 | "String"
2403 ) {
2404 format_ident!("{}", inner_inner)
2405 } else {
2406 format_ident!("{}", self.to_rust_type_name(inner_inner))
2407 };
2408 quote! { Vec<Vec<#inner_inner_type>> }
2409 }
2410 } else if inner == "serde_json::Value" {
2411 quote! { Vec<serde_json::Value> }
2412 } else {
2413 let inner_type = if matches!(
2414 inner,
2415 "bool"
2416 | "i8"
2417 | "i16"
2418 | "i32"
2419 | "i64"
2420 | "i128"
2421 | "u8"
2422 | "u16"
2423 | "u32"
2424 | "u64"
2425 | "u128"
2426 | "f32"
2427 | "f64"
2428 | "String"
2429 ) {
2430 format_ident!("{}", inner)
2431 } else {
2432 format_ident!("{}", self.to_rust_type_name(inner))
2433 };
2434 quote! { Vec<#inner_type> }
2435 }
2436 } else if variant.target.contains("::") || variant.target.contains('<') {
2437 parse_rust_type(&variant.target).unwrap_or_else(|_| {
2442 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
2443 quote! { #fallback }
2444 })
2445 } else {
2446 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
2447 quote! { #type_ident }
2448 };
2449
2450 let target_rust_name = self.to_rust_type_name(&variant.target);
2454 let enclosing_name = self.to_rust_type_name(&schema.name);
2455 let is_self_ref = target_rust_name == enclosing_name;
2456 let is_recursive_target = analysis
2460 .dependencies
2461 .recursive_schemas
2462 .contains(&variant.target);
2463 let variant_type_tokens = if is_self_ref || is_recursive_target {
2464 quote! { Box<#variant_type_tokens> }
2465 } else {
2466 variant_type_tokens
2467 };
2468
2469 quote! {
2470 #variant_name_ident(#variant_type_tokens),
2471 }
2472 });
2473
2474 let doc_comment = if let Some(desc) = &schema.description {
2475 quote! { #[doc = #desc] }
2476 } else {
2477 TokenStream::new()
2478 };
2479
2480 let derives = if self.config.enable_specta {
2482 quote! {
2483 #[derive(Debug, Clone, Deserialize, Serialize)]
2484 #[cfg_attr(feature = "specta", derive(specta::Type))]
2485 #[serde(untagged)]
2486 }
2487 } else {
2488 quote! {
2489 #[derive(Debug, Clone, Deserialize, Serialize)]
2490 #[serde(untagged)]
2491 }
2492 };
2493
2494 Ok(quote! {
2495 #doc_comment
2496 #derives
2497 pub enum #enum_name {
2498 #(#enum_variants)*
2499 }
2500 })
2501 }
2502
2503 fn target_aliases_back_to(
2508 &self,
2509 target: &str,
2510 enclosing_rust_name: &str,
2511 analysis: &crate::analysis::SchemaAnalysis,
2512 ) -> bool {
2513 let mut current = target.to_string();
2514 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2515 for _ in 0..16 {
2516 if !visited.insert(current.clone()) {
2517 return true;
2518 }
2519 let Some(schema) = analysis.schemas.get(¤t) else {
2520 return false;
2521 };
2522 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
2523 if self.to_rust_type_name(next) == enclosing_rust_name {
2524 return true;
2525 }
2526 current = next.clone();
2527 continue;
2528 }
2529 return false;
2530 }
2531 false
2532 }
2533
2534 fn generate_field_type(
2535 &self,
2536 schema_name: &str,
2537 field_name: &str,
2538 prop: &crate::analysis::PropertyInfo,
2539 is_required: bool,
2540 analysis: &crate::analysis::SchemaAnalysis,
2541 ) -> TokenStream {
2542 let base_type = self.generate_property_base_type(schema_name, field_name, prop, analysis);
2543
2544 if self.property_is_option_wrapped(schema_name, field_name, prop, is_required, analysis) {
2545 quote! { Option<#base_type> }
2546 } else {
2547 base_type
2548 }
2549 }
2550
2551 fn property_is_option_wrapped(
2552 &self,
2553 schema_name: &str,
2554 field_name: &str,
2555 prop: &crate::analysis::PropertyInfo,
2556 is_required: bool,
2557 analysis: &crate::analysis::SchemaAnalysis,
2558 ) -> bool {
2559 let override_key = format!("{schema_name}.{field_name}");
2560 let is_nullable_override = self
2561 .config
2562 .nullable_field_overrides
2563 .get(&override_key)
2564 .copied()
2565 .unwrap_or(false);
2566
2567 !is_required
2568 || prop.nullable
2569 || is_nullable_override
2570 || (prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis))
2571 }
2572
2573 pub(crate) fn generate_property_base_type(
2574 &self,
2575 schema_name: &str,
2576 _field_name: &str,
2577 prop: &crate::analysis::PropertyInfo,
2578 analysis: &crate::analysis::SchemaAnalysis,
2579 ) -> TokenStream {
2580 use crate::analysis::SchemaType;
2581
2582 match &prop.schema_type {
2583 SchemaType::Primitive { rust_type, .. } => {
2584 parse_rust_type(rust_type).unwrap_or_else(|_| {
2587 eprintln!(
2592 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
2593 falling back to String"
2594 );
2595 quote! { String }
2596 })
2597 }
2598 SchemaType::Reference { target } => {
2599 let target_rust_name = self.to_rust_type_name(target);
2600 let target_type = format_ident!("{}", target_rust_name);
2601 let enclosing_rust_name = self.to_rust_type_name(schema_name);
2614 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
2615 let is_alias_chain_self =
2616 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
2617 if analysis.dependencies.recursive_schemas.contains(target)
2618 || is_self_via_rust_name
2619 || is_alias_chain_self
2620 {
2621 quote! { Box<#target_type> }
2622 } else {
2623 quote! { #target_type }
2624 }
2625 }
2626 SchemaType::Array { item_type } => {
2627 let inner_type = self.generate_array_item_type(item_type, analysis);
2628 quote! { Vec<#inner_type> }
2629 }
2630 _ => {
2631 quote! { serde_json::Value }
2633 }
2634 }
2635 }
2636
2637 fn generate_serde_field_attrs(
2638 &self,
2639 schema_name: &str,
2640 field_name: &str,
2641 field_ident: &syn::Ident,
2642 prop: &crate::analysis::PropertyInfo,
2643 is_required: bool,
2644 analysis: &crate::analysis::SchemaAnalysis,
2645 ) -> TokenStream {
2646 let mut attrs = Vec::new();
2647
2648 let rust_field_name = field_ident.to_string();
2651 let comparison_name = rust_field_name
2652 .strip_prefix("r#")
2653 .unwrap_or(&rust_field_name);
2654 if comparison_name != field_name {
2655 attrs.push(quote! { rename = #field_name });
2656 }
2657
2658 if !is_required || prop.nullable {
2660 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
2661 }
2662
2663 if prop.default.is_some()
2667 && (is_required && !prop.nullable)
2668 && !self.type_lacks_default(&prop.schema_type, analysis)
2669 {
2670 attrs.push(quote! { default });
2671 }
2672
2673 if let crate::analysis::SchemaType::Primitive {
2681 serde_with: Some(codec),
2682 ..
2683 } = &prop.schema_type
2684 {
2685 let is_option_wrapped = self.property_is_option_wrapped(
2686 schema_name,
2687 field_name,
2688 prop,
2689 is_required,
2690 analysis,
2691 );
2692 let codec_path = if is_option_wrapped {
2693 format!("{codec}::option")
2694 } else {
2695 codec.clone()
2696 };
2697 attrs.push(quote! { with = #codec_path });
2698 if is_option_wrapped {
2703 attrs.push(quote! { default });
2704 }
2705 }
2706
2707 if attrs.is_empty() {
2708 TokenStream::new()
2709 } else {
2710 quote! { #[serde(#(#attrs),*)] }
2711 }
2712 }
2713
2714 fn type_lacks_default(
2718 &self,
2719 schema_type: &crate::analysis::SchemaType,
2720 analysis: &crate::analysis::SchemaAnalysis,
2721 ) -> bool {
2722 use crate::analysis::SchemaType;
2723 match schema_type {
2724 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
2725 SchemaType::Primitive { rust_type, .. } => matches!(
2729 rust_type.as_str(),
2730 "chrono::DateTime<chrono::Utc>"
2731 | "chrono::NaiveDate"
2732 | "chrono::NaiveTime"
2733 | "chrono::Duration"
2734 | "url::Url"
2735 | "time::OffsetDateTime"
2736 | "time::Date"
2737 | "time::Time"
2738 | "iso8601::Duration"
2739 | "email_address::EmailAddress"
2740 ),
2741 SchemaType::Reference { target } => {
2742 if let Some(schema) = analysis.schemas.get(target) {
2743 self.type_lacks_default(&schema.schema_type, analysis)
2744 } else {
2745 false
2746 }
2747 }
2748 _ => false,
2749 }
2750 }
2751
2752 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
2753 if !self.config.enable_specta {
2754 return TokenStream::new();
2755 }
2756
2757 let camel_case_name = self.to_camel_case(field_name);
2759
2760 if camel_case_name != field_name {
2762 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2763 } else {
2764 TokenStream::new()
2765 }
2766 }
2767
2768 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2769 let neg_prefix =
2773 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2774 "Neg"
2775 } else {
2776 ""
2777 };
2778
2779 let mut result = String::new();
2781 let mut next_upper = true;
2782 let mut prev_was_upper = false;
2783
2784 for (i, c) in s.chars().enumerate() {
2785 match c {
2786 'a'..='z' => {
2787 if next_upper {
2788 result.push(c.to_ascii_uppercase());
2789 next_upper = false;
2790 } else {
2791 result.push(c);
2792 }
2793 prev_was_upper = false;
2794 }
2795 'A'..='Z' => {
2796 if next_upper || (!prev_was_upper && i > 0) {
2797 result.push(c);
2799 next_upper = false;
2800 } else {
2801 result.push(c.to_ascii_lowercase());
2803 }
2804 prev_was_upper = true;
2805 }
2806 '0'..='9' => {
2807 result.push(c);
2808 next_upper = false;
2809 prev_was_upper = false;
2810 }
2811 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2812 next_upper = true;
2814 prev_was_upper = false;
2815 }
2816 _ => {
2817 next_upper = true;
2819 prev_was_upper = false;
2820 }
2821 }
2822 }
2823
2824 if result.is_empty() {
2826 result = "Value".to_string();
2827 }
2828
2829 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2831 result = format!("Variant{neg_prefix}{result}");
2832 } else if !neg_prefix.is_empty() {
2833 result = format!("{neg_prefix}{result}");
2836 }
2837
2838 match result.as_str() {
2840 "Null" => "NullValue".to_string(),
2841 "True" => "TrueValue".to_string(),
2842 "False" => "FalseValue".to_string(),
2843 "Type" => "Type_".to_string(),
2844 "Match" => "Match_".to_string(),
2845 "Fn" => "Fn_".to_string(),
2846 "Impl" => "Impl_".to_string(),
2847 "Trait" => "Trait_".to_string(),
2848 "Struct" => "Struct_".to_string(),
2849 "Enum" => "Enum_".to_string(),
2850 "Mod" => "Mod_".to_string(),
2851 "Use" => "Use_".to_string(),
2852 "Pub" => "Pub_".to_string(),
2853 "Const" => "Const_".to_string(),
2854 "Static" => "Static_".to_string(),
2855 "Let" => "Let_".to_string(),
2856 "Mut" => "Mut_".to_string(),
2857 "Ref" => "Ref_".to_string(),
2858 "Move" => "Move_".to_string(),
2859 "Return" => "Return_".to_string(),
2860 "If" => "If_".to_string(),
2861 "Else" => "Else_".to_string(),
2862 "While" => "While_".to_string(),
2863 "For" => "For_".to_string(),
2864 "Loop" => "Loop_".to_string(),
2865 "Break" => "Break_".to_string(),
2866 "Continue" => "Continue_".to_string(),
2867 "Self" => "Self_".to_string(),
2868 "Super" => "Super_".to_string(),
2869 "Crate" => "Crate_".to_string(),
2870 "Async" => "Async_".to_string(),
2871 "Await" => "Await_".to_string(),
2872 _ => result,
2873 }
2874 }
2875
2876 #[allow(dead_code)]
2877 fn to_rust_identifier(&self, s: &str) -> String {
2878 let mut result = s
2880 .chars()
2881 .map(|c| match c {
2882 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2883 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2884 _ => '_',
2885 })
2886 .collect::<String>();
2887
2888 result = result.trim_matches('_').to_string();
2890
2891 if result.is_empty() {
2893 result = "value".to_string();
2894 }
2895
2896 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2898 result = format!("variant_{result}");
2899 }
2900
2901 match result.as_str() {
2903 "null" => "null_value".to_string(),
2904 "true" => "true_value".to_string(),
2905 "false" => "false_value".to_string(),
2906 "type" => "type_".to_string(),
2907 "match" => "match_".to_string(),
2908 "fn" => "fn_".to_string(),
2909 "impl" => "impl_".to_string(),
2910 "trait" => "trait_".to_string(),
2911 "struct" => "struct_".to_string(),
2912 "enum" => "enum_".to_string(),
2913 "mod" => "mod_".to_string(),
2914 "use" => "use_".to_string(),
2915 "pub" => "pub_".to_string(),
2916 "const" => "const_".to_string(),
2917 "static" => "static_".to_string(),
2918 "let" => "let_".to_string(),
2919 "mut" => "mut_".to_string(),
2920 "ref" => "ref_".to_string(),
2921 "move" => "move_".to_string(),
2922 "return" => "return_".to_string(),
2923 "if" => "if_".to_string(),
2924 "else" => "else_".to_string(),
2925 "while" => "while_".to_string(),
2926 "for" => "for_".to_string(),
2927 "loop" => "loop_".to_string(),
2928 "break" => "break_".to_string(),
2929 "continue" => "continue_".to_string(),
2930 "self" => "self_".to_string(),
2931 "super" => "super_".to_string(),
2932 "crate" => "crate_".to_string(),
2933 "async" => "async_".to_string(),
2934 "await" => "await_".to_string(),
2935 "override" => "override_".to_string(),
2937 "box" => "box_".to_string(),
2938 "dyn" => "dyn_".to_string(),
2939 "where" => "where_".to_string(),
2940 "in" => "in_".to_string(),
2941 "abstract" => "abstract_".to_string(),
2943 "become" => "become_".to_string(),
2944 "do" => "do_".to_string(),
2945 "final" => "final_".to_string(),
2946 "macro" => "macro_".to_string(),
2947 "priv" => "priv_".to_string(),
2948 "try" => "try_".to_string(),
2949 "typeof" => "typeof_".to_string(),
2950 "unsized" => "unsized_".to_string(),
2951 "virtual" => "virtual_".to_string(),
2952 "yield" => "yield_".to_string(),
2953 _ => result,
2954 }
2955 }
2956
2957 fn generate_constraint_doc(
2965 &self,
2966 constraints: &crate::analysis::PropertyConstraints,
2967 ) -> TokenStream {
2968 use crate::type_mapping::ConstraintMode;
2969
2970 if constraints.is_empty() {
2971 return TokenStream::new();
2972 }
2973 match self.config.types.constraint_mode() {
2974 ConstraintMode::Off => TokenStream::new(),
2975 ConstraintMode::Doc => {
2976 let formatted = format_constraints_doc(constraints);
2977 quote! { #[doc = #formatted] }
2978 }
2979 }
2980 }
2981
2982 fn sanitize_doc_comment(&self, desc: &str) -> String {
2983 let mut result = desc.to_string();
2985
2986 if result.contains('\n')
2994 && (result.contains('{')
2995 || result.contains("```")
2996 || result.contains("Human:")
2997 || result.contains("Assistant:")
2998 || result
2999 .lines()
3000 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
3001 {
3002 if result.contains("```") {
3004 result = result.replace("```", "```ignore");
3005 } else {
3006 if result.lines().any(|line| {
3008 let trimmed = line.trim();
3009 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
3010 }) {
3011 result = format!("```ignore\n{result}\n```");
3012 }
3013 }
3014 }
3015
3016 result
3017 }
3018
3019 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
3020 let mut result = String::new();
3022 let mut next_upper = true;
3023 let mut prev_was_lower = false;
3024
3025 for c in s.chars() {
3026 match c {
3027 'a'..='z' => {
3028 if next_upper {
3029 result.push(c.to_ascii_uppercase());
3030 next_upper = false;
3031 } else {
3032 result.push(c);
3033 }
3034 prev_was_lower = true;
3035 }
3036 'A'..='Z' => {
3037 result.push(c);
3038 next_upper = false;
3039 prev_was_lower = false;
3040 }
3041 '0'..='9' => {
3042 if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
3045 }
3047 result.push(c);
3048 next_upper = false;
3049 prev_was_lower = false;
3050 }
3051 '_' | '-' | '.' | ' ' => {
3052 next_upper = true;
3054 prev_was_lower = false;
3055 }
3056 _ => {
3057 next_upper = true;
3059 prev_was_lower = false;
3060 }
3061 }
3062 }
3063
3064 if result.is_empty() {
3066 result = "Type".to_string();
3067 }
3068
3069 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3071 result = format!("Type{result}");
3072 }
3073
3074 if matches!(
3081 result.as_str(),
3082 "Result"
3083 | "Option"
3084 | "Box"
3085 | "Vec"
3086 | "String"
3087 | "Some"
3088 | "None"
3089 | "Ok"
3090 | "Err"
3091 | "Default"
3092 | "Clone"
3093 | "Debug"
3094 | "Send"
3095 | "Sync"
3096 | "Sized"
3097 | "Iterator"
3098 | "From"
3099 | "Into"
3100 | "TryFrom"
3101 | "TryInto"
3102 | "AsRef"
3103 | "AsMut"
3104 ) {
3105 result.push_str("Type");
3106 }
3107
3108 result
3109 }
3110
3111 fn to_rust_field_name(&self, s: &str) -> String {
3112 let leading_marker = match s.chars().next() {
3116 Some('-') if s.len() > 1 => "neg_",
3117 Some('+') if s.len() > 1 => "pos_",
3118 _ => "",
3119 };
3120
3121 let mut result = String::new();
3123 let mut prev_was_upper = false;
3124 let mut prev_was_underscore = false;
3125
3126 for (i, c) in s.chars().enumerate() {
3127 match c {
3128 'A'..='Z' => {
3129 if i > 0 && !prev_was_upper && !prev_was_underscore {
3131 result.push('_');
3132 }
3133 result.push(c.to_ascii_lowercase());
3134 prev_was_upper = true;
3135 prev_was_underscore = false;
3136 }
3137 'a'..='z' | '0'..='9' => {
3138 result.push(c);
3139 prev_was_upper = false;
3140 prev_was_underscore = false;
3141 }
3142 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
3143 if !prev_was_underscore && !result.is_empty() {
3144 result.push('_');
3145 prev_was_underscore = true;
3146 }
3147 prev_was_upper = false;
3148 }
3149 _ => {
3150 if !prev_was_underscore && !result.is_empty() {
3152 result.push('_');
3153 }
3154 prev_was_upper = false;
3155 prev_was_underscore = true;
3156 }
3157 }
3158 }
3159
3160 let mut result = result.trim_matches('_').to_string();
3162 if result.is_empty() {
3163 return "field".to_string();
3164 }
3165
3166 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3168 result = format!("field_{leading_marker}{result}");
3169 } else if !leading_marker.is_empty() {
3170 result = format!("{leading_marker}{result}");
3171 }
3172
3173 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
3177 return format!("{result}_field");
3178 }
3179 if Self::is_rust_keyword(&result) {
3181 format!("r#{result}")
3182 } else {
3183 result
3184 }
3185 }
3186
3187 pub fn is_rust_keyword(s: &str) -> bool {
3189 matches!(
3190 s,
3191 "type"
3192 | "match"
3193 | "fn"
3194 | "struct"
3195 | "enum"
3196 | "impl"
3197 | "trait"
3198 | "mod"
3199 | "use"
3200 | "pub"
3201 | "const"
3202 | "static"
3203 | "let"
3204 | "mut"
3205 | "ref"
3206 | "move"
3207 | "return"
3208 | "if"
3209 | "else"
3210 | "while"
3211 | "for"
3212 | "loop"
3213 | "break"
3214 | "continue"
3215 | "self"
3216 | "super"
3217 | "crate"
3218 | "async"
3219 | "await"
3220 | "override"
3221 | "box"
3222 | "dyn"
3223 | "where"
3224 | "in"
3225 | "abstract"
3226 | "become"
3227 | "do"
3228 | "final"
3229 | "macro"
3230 | "priv"
3231 | "try"
3232 | "typeof"
3233 | "unsized"
3234 | "virtual"
3235 | "yield"
3236 | "gen"
3238 )
3239 }
3240
3241 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
3243 if let Some(raw) = name.strip_prefix("r#") {
3244 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
3245 } else {
3246 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
3247 }
3248 }
3249
3250 fn to_camel_case(&self, s: &str) -> String {
3251 let mut result = String::new();
3253 let mut capitalize_next = false;
3254
3255 for (i, c) in s.chars().enumerate() {
3256 match c {
3257 '_' | '-' | '.' | ' ' => {
3258 capitalize_next = true;
3260 }
3261 'A'..='Z' => {
3262 if i == 0 {
3263 result.push(c.to_ascii_lowercase());
3265 } else if capitalize_next {
3266 result.push(c);
3267 capitalize_next = false;
3268 } else {
3269 result.push(c.to_ascii_lowercase());
3270 }
3271 }
3272 'a'..='z' | '0'..='9' => {
3273 if capitalize_next {
3274 result.push(c.to_ascii_uppercase());
3275 capitalize_next = false;
3276 } else {
3277 result.push(c);
3278 }
3279 }
3280 _ => {
3281 capitalize_next = true;
3283 }
3284 }
3285 }
3286
3287 if result.is_empty() {
3288 return "field".to_string();
3289 }
3290
3291 result
3292 }
3293
3294 fn generate_composition_struct(
3295 &self,
3296 schema: &crate::analysis::AnalyzedSchema,
3297 schemas: &[crate::analysis::SchemaRef],
3298 ) -> Result<TokenStream> {
3299 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
3300
3301 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
3307 let field_name = format_ident!("part_{}", i);
3308 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
3309
3310 quote! {
3311 #[serde(flatten)]
3312 pub #field_name: #field_type,
3313 }
3314 });
3315
3316 let doc_comment = if let Some(desc) = &schema.description {
3317 quote! { #[doc = #desc] }
3318 } else {
3319 TokenStream::new()
3320 };
3321
3322 let derives = if self.config.enable_specta {
3324 quote! {
3325 #[derive(Debug, Clone, Deserialize, Serialize)]
3326 #[cfg_attr(feature = "specta", derive(specta::Type))]
3327 }
3328 } else {
3329 quote! {
3330 #[derive(Debug, Clone, Deserialize, Serialize)]
3331 }
3332 };
3333
3334 Ok(quote! {
3335 #doc_comment
3336 #derives
3337 pub struct #struct_name {
3338 #(#fields)*
3339 }
3340 })
3341 }
3342
3343 #[allow(dead_code)]
3344 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
3345 let mut missing = std::collections::HashSet::new();
3346 let defined_types: std::collections::HashSet<String> =
3347 analysis.schemas.keys().cloned().collect();
3348
3349 for schema in analysis.schemas.values() {
3351 match &schema.schema_type {
3352 crate::analysis::SchemaType::Union { variants } => {
3353 for variant in variants {
3354 if !defined_types.contains(&variant.target) {
3355 missing.insert(variant.target.clone());
3356 }
3357 }
3358 }
3359 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
3360 for variant in variants {
3361 if !defined_types.contains(&variant.type_name) {
3362 missing.insert(variant.type_name.clone());
3363 }
3364 }
3365 }
3366 crate::analysis::SchemaType::Object { properties, .. } => {
3367 let mut sorted_props: Vec<_> = properties.iter().collect();
3369 sorted_props.sort_by_key(|(name, _)| name.as_str());
3370 for (_, prop) in sorted_props {
3371 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
3372 {
3373 if !defined_types.contains(target) {
3374 missing.insert(target.clone());
3375 }
3376 }
3377 }
3378 }
3379 crate::analysis::SchemaType::Reference { target }
3380 if !defined_types.contains(target) =>
3381 {
3382 missing.insert(target.clone());
3383 }
3384 _ => {}
3385 }
3386 }
3387
3388 missing
3389 }
3390
3391 #[allow(clippy::only_used_in_recursion)]
3392 fn generate_array_item_type(
3393 &self,
3394 item_type: &crate::analysis::SchemaType,
3395 analysis: &crate::analysis::SchemaAnalysis,
3396 ) -> TokenStream {
3397 use crate::analysis::SchemaType;
3398
3399 match item_type {
3400 SchemaType::Primitive { rust_type, .. } => {
3401 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
3406 quote! { #parsed }
3407 } else if rust_type.contains("::") {
3408 let parts: Vec<_> = rust_type
3409 .split("::")
3410 .map(|p| format_ident!("{}", p))
3411 .collect();
3412 quote! { #(#parts)::* }
3413 } else {
3414 let type_ident = format_ident!("{}", rust_type);
3415 quote! { #type_ident }
3416 }
3417 }
3418 SchemaType::Reference { target } => {
3419 let target_type = format_ident!("{}", self.to_rust_type_name(target));
3420 if analysis.dependencies.recursive_schemas.contains(target) {
3422 quote! { Box<#target_type> }
3423 } else {
3424 quote! { #target_type }
3425 }
3426 }
3427 SchemaType::Array { item_type } => {
3428 let inner_type = self.generate_array_item_type(item_type, analysis);
3430 quote! { Vec<#inner_type> }
3431 }
3432 _ => {
3433 quote! { serde_json::Value }
3435 }
3436 }
3437 }
3438
3439 fn type_name_to_variant_name(&self, type_name: &str) -> String {
3441 match type_name {
3443 "bool" => return "Boolean".to_string(),
3444 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
3445 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
3446 "f32" | "f64" => return "Number".to_string(),
3447 "String" => return "String".to_string(),
3448 "serde_json::Value" => return "Value".to_string(),
3449 "bytes::Bytes" => return "Binary".to_string(),
3453 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
3454 "chrono::NaiveDate" => return "Date".to_string(),
3455 "chrono::NaiveTime" => return "Time".to_string(),
3456 "uuid::Uuid" => return "Uuid".to_string(),
3457 "url::Url" => return "Url".to_string(),
3458 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
3459 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
3460 _ => {}
3461 }
3462
3463 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
3465 let inner = &type_name[4..type_name.len() - 1];
3466 if inner.starts_with("Vec<") && inner.ends_with(">") {
3468 let inner_inner = &inner[4..inner.len() - 1];
3469 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
3470 }
3471 return format!("{}Array", self.type_name_to_variant_name(inner));
3472 }
3473
3474 let clean_name = type_name
3480 .trim_end_matches("Type")
3481 .trim_end_matches("Schema")
3482 .trim_end_matches("Item");
3483
3484 self.to_rust_type_name(clean_name)
3486 }
3487
3488 fn ensure_unique_variant_name_generator(
3490 &self,
3491 base_name: String,
3492 used_names: &mut std::collections::HashSet<String>,
3493 fallback_index: usize,
3494 ) -> String {
3495 if used_names.insert(base_name.clone()) {
3496 return base_name;
3497 }
3498
3499 for i in 2..100 {
3501 let numbered_name = format!("{base_name}{i}");
3502 if used_names.insert(numbered_name.clone()) {
3503 return numbered_name;
3504 }
3505 }
3506
3507 let fallback = format!("Variant{fallback_index}");
3509 used_names.insert(fallback.clone());
3510 fallback
3511 }
3512
3513 fn find_request_type_for_operation(
3515 &self,
3516 operation_id: &str,
3517 analysis: &SchemaAnalysis,
3518 ) -> Option<String> {
3519 analysis.operations.get(operation_id).and_then(|op| {
3521 op.request_body
3522 .as_ref()
3523 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
3524 })
3525 }
3526
3527 fn resolve_streaming_event_type(
3529 &self,
3530 endpoint: &crate::streaming::StreamingEndpoint,
3531 analysis: &SchemaAnalysis,
3532 ) -> Result<String> {
3533 match &endpoint.event_flow {
3534 crate::streaming::EventFlow::Simple => {
3535 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3538 Ok(endpoint.event_union_type.to_string())
3539 } else {
3540 Err(crate::error::GeneratorError::ValidationError(format!(
3541 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
3542 endpoint.event_union_type, endpoint.operation_id
3543 )))
3544 }
3545 }
3546 crate::streaming::EventFlow::StartDeltaStop { .. } => {
3547 if analysis.schemas.contains_key(&endpoint.event_union_type) {
3550 Ok(endpoint.event_union_type.to_string())
3551 } else {
3552 Err(crate::error::GeneratorError::ValidationError(format!(
3553 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
3554 endpoint.event_union_type, endpoint.operation_id
3555 )))
3556 }
3557 }
3558 }
3559 }
3560
3561 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
3563 Ok(quote! {
3564 #[derive(Debug, thiserror::Error)]
3566 pub enum StreamingError {
3567 #[error("Connection error: {0}")]
3568 Connection(String),
3569 #[error("HTTP error: {status}")]
3570 Http { status: u16 },
3571 #[error("SSE parsing error: {0}")]
3572 Parsing(String),
3573 #[error("Authentication error: {0}")]
3574 Authentication(String),
3575 #[error("Rate limit error: {0}")]
3576 RateLimit(String),
3577 #[error("API error: {0}")]
3578 Api(String),
3579 #[error("Timeout error: {0}")]
3580 Timeout(String),
3581 #[error("JSON serialization/deserialization error: {0}")]
3582 Json(#[from] serde_json::Error),
3583 #[error("Request error: {0}")]
3584 Request(reqwest::Error),
3585 }
3586
3587 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
3588 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
3589 StreamingError::Api(format!("Invalid header value: {}", err))
3590 }
3591 }
3592
3593 impl From<reqwest::Error> for StreamingError {
3594 fn from(err: reqwest::Error) -> Self {
3595 if err.is_timeout() {
3596 StreamingError::Timeout(err.to_string())
3597 } else if err.is_status() {
3598 if let Some(status) = err.status() {
3599 StreamingError::Http { status: status.as_u16() }
3600 } else {
3601 StreamingError::Connection(err.to_string())
3602 }
3603 } else {
3604 StreamingError::Request(err)
3605 }
3606 }
3607 }
3608 })
3609 }
3610
3611 fn generate_endpoint_trait(
3613 &self,
3614 endpoint: &crate::streaming::StreamingEndpoint,
3615 analysis: &SchemaAnalysis,
3616 ) -> Result<TokenStream> {
3617 use crate::streaming::HttpMethod;
3618
3619 let trait_name = format_ident!(
3620 "{}StreamingClient",
3621 self.to_rust_type_name(&endpoint.operation_id)
3622 );
3623 let method_name =
3624 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3625 let event_type =
3626 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3627
3628 let method_signature = match endpoint.http_method {
3630 HttpMethod::Get => {
3631 let mut param_defs = Vec::new();
3633 for qp in &endpoint.query_parameters {
3634 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3635 if qp.required {
3636 param_defs.push(quote! { #param_name: &str });
3637 } else {
3638 param_defs.push(quote! { #param_name: Option<&str> });
3639 }
3640 }
3641 quote! {
3642 async fn #method_name(
3643 &self,
3644 #(#param_defs),*
3645 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3646 }
3647 }
3648 HttpMethod::Post => {
3649 let request_type = self
3651 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3652 .unwrap_or_else(|| "serde_json::Value".to_string());
3653 let request_type_ident = if request_type.contains("::") {
3654 let parts: Vec<&str> = request_type.split("::").collect();
3655 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3656 quote! { #(#path_parts)::* }
3657 } else {
3658 let ident = format_ident!("{}", request_type);
3659 quote! { #ident }
3660 };
3661 quote! {
3662 async fn #method_name(
3663 &self,
3664 request: #request_type_ident,
3665 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
3666 }
3667 }
3668 };
3669
3670 Ok(quote! {
3671 #[async_trait]
3673 pub trait #trait_name {
3674 type Error: std::error::Error + Send + Sync + 'static;
3675
3676 #method_signature
3678 }
3679 })
3680 }
3681
3682 fn generate_streaming_client_impl(
3684 &self,
3685 streaming_config: &crate::streaming::StreamingConfig,
3686 analysis: &SchemaAnalysis,
3687 ) -> Result<TokenStream> {
3688 let client_name = format_ident!(
3689 "{}Client",
3690 self.to_rust_type_name(&streaming_config.client_module_name)
3691 );
3692
3693 let mut struct_fields = vec![
3696 quote! { base_url: String },
3697 quote! { api_key: Option<String> },
3698 quote! { http_client: reqwest::Client },
3699 quote! { custom_headers: std::collections::BTreeMap<String, String> },
3700 ];
3701
3702 let has_optional_headers = !streaming_config
3703 .endpoints
3704 .iter()
3705 .all(|e| e.optional_headers.is_empty());
3706
3707 if has_optional_headers {
3708 struct_fields
3709 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
3710 }
3711
3712 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
3715 streaming_config
3716 .endpoints
3717 .first()
3718 .and_then(|e| e.base_url.as_deref())
3719 .unwrap_or("https://api.example.com")
3720 } else {
3721 "https://api.example.com"
3722 };
3723
3724 let constructor_fields = if has_optional_headers {
3726 quote! {
3727 base_url: #default_base_url.to_string(),
3728 api_key: None,
3729 http_client: reqwest::Client::new(),
3730 custom_headers: std::collections::BTreeMap::new(),
3731 optional_headers: std::collections::BTreeMap::new(),
3732 }
3733 } else {
3734 quote! {
3735 base_url: #default_base_url.to_string(),
3736 api_key: None,
3737 http_client: reqwest::Client::new(),
3738 custom_headers: std::collections::BTreeMap::new(),
3739 }
3740 };
3741
3742 let optional_headers_method = if has_optional_headers {
3744 quote! {
3745 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
3747 self.optional_headers = headers;
3748 }
3749 }
3750 } else {
3751 TokenStream::new()
3752 };
3753
3754 let constructor = quote! {
3755 impl #client_name {
3756 pub fn new() -> Self {
3758 Self {
3759 #constructor_fields
3760 }
3761 }
3762
3763 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3765 self.base_url = base_url.into();
3766 self
3767 }
3768
3769 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3771 self.api_key = Some(api_key.into());
3772 self
3773 }
3774
3775 pub fn with_header(
3777 mut self,
3778 name: impl Into<String>,
3779 value: impl Into<String>,
3780 ) -> Self {
3781 self.custom_headers.insert(name.into(), value.into());
3782 self
3783 }
3784
3785 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3787 self.http_client = client;
3788 self
3789 }
3790
3791 #optional_headers_method
3792 }
3793 };
3794
3795 let mut trait_impls = Vec::new();
3797 for endpoint in &streaming_config.endpoints {
3798 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3799 trait_impls.push(trait_impl);
3800 }
3801
3802 let default_impl = quote! {
3804 impl Default for #client_name {
3805 fn default() -> Self {
3806 Self::new()
3807 }
3808 }
3809 };
3810
3811 Ok(quote! {
3812 #[derive(Debug, Clone)]
3814 pub struct #client_name {
3815 #(#struct_fields,)*
3816 }
3817
3818 #constructor
3819
3820 #default_impl
3821
3822 #(#trait_impls)*
3823 })
3824 }
3825
3826 fn generate_endpoint_trait_impl(
3828 &self,
3829 endpoint: &crate::streaming::StreamingEndpoint,
3830 client_name: &proc_macro2::Ident,
3831 analysis: &SchemaAnalysis,
3832 ) -> Result<TokenStream> {
3833 use crate::streaming::HttpMethod;
3834
3835 let trait_name = format_ident!(
3836 "{}StreamingClient",
3837 self.to_rust_type_name(&endpoint.operation_id)
3838 );
3839 let method_name =
3840 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3841 let event_type =
3842 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3843
3844 let mut header_setup = Vec::new();
3846 for (name, value) in &endpoint.required_headers {
3847 header_setup.push(quote! {
3848 headers.insert(#name, HeaderValue::from_static(#value));
3849 });
3850 }
3851
3852 if let Some(auth_header) = &endpoint.auth_header {
3855 match auth_header {
3856 crate::streaming::AuthHeader::Bearer(header_name) => {
3857 header_setup.push(quote! {
3858 if let Some(ref api_key) = self.api_key {
3859 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3860 }
3861 });
3862 }
3863 crate::streaming::AuthHeader::ApiKey(header_name) => {
3864 header_setup.push(quote! {
3865 if let Some(ref api_key) = self.api_key {
3866 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3867 }
3868 });
3869 }
3870 }
3871 } else {
3872 header_setup.push(quote! {
3874 if let Some(ref api_key) = self.api_key {
3875 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3876 }
3877 });
3878 }
3879
3880 header_setup.push(quote! {
3882 for (name, value) in &self.custom_headers {
3883 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3884 headers.insert(header_name, header_value);
3885 }
3886 }
3887 });
3888
3889 if !endpoint.optional_headers.is_empty() {
3891 header_setup.push(quote! {
3892 for (key, value) in &self.optional_headers {
3893 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3894 headers.insert(header_name, header_value);
3895 }
3896 }
3897 });
3898 }
3899
3900 match endpoint.http_method {
3902 HttpMethod::Get => self.generate_get_streaming_impl(
3903 endpoint,
3904 client_name,
3905 &trait_name,
3906 &method_name,
3907 &event_type,
3908 &header_setup,
3909 ),
3910 HttpMethod::Post => self.generate_post_streaming_impl(
3911 endpoint,
3912 client_name,
3913 &trait_name,
3914 &method_name,
3915 &event_type,
3916 &header_setup,
3917 analysis,
3918 ),
3919 }
3920 }
3921
3922 fn generate_get_streaming_impl(
3924 &self,
3925 endpoint: &crate::streaming::StreamingEndpoint,
3926 client_name: &proc_macro2::Ident,
3927 trait_name: &proc_macro2::Ident,
3928 method_name: &proc_macro2::Ident,
3929 event_type: &proc_macro2::Ident,
3930 header_setup: &[TokenStream],
3931 ) -> Result<TokenStream> {
3932 let path = &endpoint.path;
3933
3934 let mut param_defs = Vec::new();
3936 let mut query_params = Vec::new();
3937
3938 for qp in &endpoint.query_parameters {
3939 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3940 let param_name_str = &qp.name;
3941
3942 if qp.required {
3943 param_defs.push(quote! { #param_name: &str });
3944 query_params.push(quote! {
3945 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3946 });
3947 } else {
3948 param_defs.push(quote! { #param_name: Option<&str> });
3949 query_params.push(quote! {
3950 if let Some(v) = #param_name {
3951 url.query_pairs_mut().append_pair(#param_name_str, v);
3952 }
3953 });
3954 }
3955 }
3956
3957 let url_construction = quote! {
3959 let base_url = url::Url::parse(&self.base_url)
3960 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3961 let path_to_join = #path.trim_start_matches('/');
3962 let mut url = base_url.join(path_to_join)
3963 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3964 #(#query_params)*
3965 };
3966
3967 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3968
3969 Ok(quote! {
3970 #[async_trait]
3971 impl #trait_name for #client_name {
3972 type Error = StreamingError;
3973
3974 #instrument_skip
3975 async fn #method_name(
3976 &self,
3977 #(#param_defs),*
3978 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3979 debug!("Starting streaming GET request");
3980
3981 let mut headers = HeaderMap::new();
3982 #(#header_setup)*
3983
3984 #url_construction
3985 let url_str = url.to_string();
3986 debug!("Making streaming GET request to: {}", url_str);
3987
3988 let request_builder = self.http_client
3989 .get(url_str)
3990 .headers(headers);
3991
3992 debug!("Creating SSE stream from request");
3993 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3994 info!("SSE stream created successfully");
3995 Ok(Box::pin(stream))
3996 }
3997 }
3998 })
3999 }
4000
4001 #[allow(clippy::too_many_arguments)]
4003 fn generate_post_streaming_impl(
4004 &self,
4005 endpoint: &crate::streaming::StreamingEndpoint,
4006 client_name: &proc_macro2::Ident,
4007 trait_name: &proc_macro2::Ident,
4008 method_name: &proc_macro2::Ident,
4009 event_type: &proc_macro2::Ident,
4010 header_setup: &[TokenStream],
4011 analysis: &SchemaAnalysis,
4012 ) -> Result<TokenStream> {
4013 let path = &endpoint.path;
4014
4015 let request_type = self
4017 .find_request_type_for_operation(&endpoint.operation_id, analysis)
4018 .unwrap_or_else(|| "serde_json::Value".to_string());
4019 let request_type_ident = if request_type.contains("::") {
4020 let parts: Vec<&str> = request_type.split("::").collect();
4021 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
4022 quote! { #(#path_parts)::* }
4023 } else {
4024 let ident = format_ident!("{}", request_type);
4025 quote! { #ident }
4026 };
4027
4028 let url_construction = quote! {
4030 let base_url = url::Url::parse(&self.base_url)
4031 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
4032 let path_to_join = #path.trim_start_matches('/');
4033 let url = base_url.join(path_to_join)
4034 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
4035 .to_string();
4036 };
4037
4038 let stream_param = &endpoint.stream_parameter;
4040 let stream_setup = if stream_param.is_empty() {
4041 quote! {
4042 let streaming_request = request;
4043 }
4044 } else {
4045 quote! {
4046 let mut streaming_request = request;
4048 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
4049 if let Some(obj) = request_value.as_object_mut() {
4050 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
4051 }
4052 streaming_request = serde_json::from_value(request_value)?;
4053 }
4054 }
4055 };
4056
4057 Ok(quote! {
4058 #[async_trait]
4059 impl #trait_name for #client_name {
4060 type Error = StreamingError;
4061
4062 #[instrument(skip(self, request), name = "streaming_post_request")]
4063 async fn #method_name(
4064 &self,
4065 request: #request_type_ident,
4066 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
4067 debug!("Starting streaming POST request");
4068
4069 #stream_setup
4070
4071 let mut headers = HeaderMap::new();
4072 #(#header_setup)*
4073
4074 #url_construction
4075 debug!("Making streaming POST request to: {}", url);
4076
4077 let request_builder = self.http_client
4078 .post(&url)
4079 .headers(headers)
4080 .json(&streaming_request);
4081
4082 debug!("Creating SSE stream from request");
4083 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
4084 info!("SSE stream created successfully");
4085 Ok(Box::pin(stream))
4086 }
4087 }
4088 })
4089 }
4090
4091 fn generate_sse_parser_utilities(
4093 &self,
4094 _streaming_config: &crate::streaming::StreamingConfig,
4095 ) -> Result<TokenStream> {
4096 Ok(quote! {
4097 pub async fn parse_sse_stream<T>(
4099 request_builder: reqwest::RequestBuilder
4100 ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
4101 where
4102 T: serde::de::DeserializeOwned + Send + 'static,
4103 {
4104 let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
4105 StreamingError::Connection(format!("Failed to create event source: {}", e))
4106 })?;
4107
4108 let stream = event_source.filter_map(|event_result| async move {
4109 match event_result {
4110 Ok(reqwest_eventsource::Event::Open) => {
4111 debug!("SSE connection opened");
4112 None
4113 }
4114 Ok(reqwest_eventsource::Event::Message(message)) => {
4115 if message.event == "ping" {
4117 debug!("Received SSE ping event, skipping");
4118 return None;
4119 }
4120
4121 if message.data.trim().is_empty() {
4123 debug!("Empty SSE data, skipping");
4124 return None;
4125 }
4126
4127 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
4129 if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
4130 if event_type == "ping" {
4131 debug!("Received ping event in JSON data, skipping");
4132 return None;
4133 }
4134 }
4135
4136 match serde_json::from_value::<T>(json_value) {
4138 Ok(parsed_event) => {
4139 Some(Ok(parsed_event))
4140 }
4141 Err(e) => {
4142 if message.data.contains("ping") || message.event.contains("ping") {
4143 debug!("Ignoring ping-related event: {}", message.data);
4144 None
4145 } else {
4146 Some(Err(StreamingError::Parsing(
4147 format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
4148 )))
4149 }
4150 }
4151 }
4152 } else {
4153 Some(Err(StreamingError::Parsing(
4155 format!("SSE event is not valid JSON: {}", message.data)
4156 )))
4157 }
4158 }
4159 Err(e) => {
4160 match e {
4162 reqwest_eventsource::Error::StreamEnded => {
4163 debug!("SSE stream completed normally");
4164 None }
4166 reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
4167 let status_code = status.as_u16();
4169
4170 let error_body = match response.text().await {
4172 Ok(body) => body,
4173 Err(_) => "Failed to read error response body".to_string()
4174 };
4175
4176 error!("SSE connection error - HTTP {}: {}", status_code, error_body);
4177
4178 let detailed_error = format!(
4179 "HTTP {} error: {}",
4180 status_code,
4181 error_body
4182 );
4183
4184 Some(Err(StreamingError::Connection(detailed_error)))
4185 }
4186 _ => {
4187 let error_str = e.to_string();
4188 if error_str.contains("stream closed") {
4189 debug!("SSE stream closed");
4190 None
4191 } else {
4192 error!("SSE connection error: {}", e);
4193 Some(Err(StreamingError::Connection(error_str)))
4194 }
4195 }
4196 }
4197 }
4198 }
4199 });
4200
4201 Ok(stream)
4202 }
4203 })
4204 }
4205
4206 fn generate_reconnection_utilities(
4208 &self,
4209 reconnect_config: &crate::streaming::ReconnectionConfig,
4210 ) -> Result<TokenStream> {
4211 let max_retries = reconnect_config.max_retries;
4212 let initial_delay = reconnect_config.initial_delay_ms;
4213 let max_delay = reconnect_config.max_delay_ms;
4214 let backoff_multiplier = reconnect_config.backoff_multiplier;
4215
4216 Ok(quote! {
4217 #[derive(Debug, Clone)]
4219 pub struct ReconnectionManager {
4220 max_retries: u32,
4221 initial_delay_ms: u64,
4222 max_delay_ms: u64,
4223 backoff_multiplier: f64,
4224 current_attempt: u32,
4225 }
4226
4227 impl ReconnectionManager {
4228 pub fn new() -> Self {
4230 Self {
4231 max_retries: #max_retries,
4232 initial_delay_ms: #initial_delay,
4233 max_delay_ms: #max_delay,
4234 backoff_multiplier: #backoff_multiplier,
4235 current_attempt: 0,
4236 }
4237 }
4238
4239 pub fn should_retry(&self) -> bool {
4241 self.current_attempt < self.max_retries
4242 }
4243
4244 pub fn next_retry_delay(&mut self) -> Duration {
4246 if !self.should_retry() {
4247 return Duration::from_secs(0);
4248 }
4249
4250 let delay_ms = (self.initial_delay_ms as f64
4251 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
4252 let delay_ms = delay_ms.min(self.max_delay_ms);
4253
4254 self.current_attempt += 1;
4255 Duration::from_millis(delay_ms)
4256 }
4257
4258 pub fn reset(&mut self) {
4260 self.current_attempt = 0;
4261 }
4262
4263 pub fn current_attempt(&self) -> u32 {
4265 self.current_attempt
4266 }
4267 }
4268
4269 impl Default for ReconnectionManager {
4270 fn default() -> Self {
4271 Self::new()
4272 }
4273 }
4274 })
4275 }
4276}