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)]
89struct DiscriminatedVariantInfo {
90 discriminator_field: String,
92 discriminator_value: String,
94 is_parent_untagged: bool,
96}
97
98#[derive(Debug, Clone)]
99pub struct GeneratorConfig {
100 pub spec_path: PathBuf,
102 pub output_dir: PathBuf,
104 pub module_name: String,
111 pub enable_sse_client: bool,
113 pub enable_async_client: bool,
115 pub enable_specta: bool,
117 pub type_mappings: BTreeMap<String, String>,
119 pub streaming_config: Option<StreamingConfig>,
121 pub nullable_field_overrides: BTreeMap<String, bool>,
124 pub extensible_enum_overrides: BTreeMap<String, bool>,
131 pub schema_extensions: Vec<PathBuf>,
134 pub http_client_config: Option<crate::http_config::HttpClientConfig>,
136 pub retry_config: Option<crate::http_config::RetryConfig>,
138 pub tracing_enabled: bool,
140 pub auth_config: Option<crate::http_config::AuthConfig>,
142 pub enable_registry: bool,
144 pub registry_only: bool,
146 pub types: crate::type_mapping::TypeMappingConfig,
150 pub server: Option<crate::config::ServerSection>,
153}
154
155impl Default for GeneratorConfig {
156 fn default() -> Self {
157 Self {
158 spec_path: "openapi.json".into(),
159 output_dir: "src/gen".into(),
160 module_name: "api_types".to_string(),
161 enable_sse_client: true,
162 enable_async_client: true,
163 enable_specta: false,
164 type_mappings: default_type_mappings(),
165 streaming_config: None,
166 nullable_field_overrides: BTreeMap::new(),
167 extensible_enum_overrides: BTreeMap::new(),
168 schema_extensions: Vec::new(),
169 http_client_config: None,
170 retry_config: None,
171 tracing_enabled: true,
172 auth_config: None,
173 enable_registry: false,
174 registry_only: false,
175 types: crate::type_mapping::TypeMappingConfig::default(),
176 server: None,
177 }
178 }
179}
180
181pub fn default_type_mappings() -> BTreeMap<String, String> {
182 let mut mappings = BTreeMap::new();
183 mappings.insert("integer".to_string(), "i64".to_string());
184 mappings.insert("number".to_string(), "f64".to_string());
185 mappings.insert("string".to_string(), "String".to_string());
186 mappings.insert("boolean".to_string(), "bool".to_string());
187 mappings
188}
189
190#[derive(Debug, Clone)]
192pub struct GeneratedFile {
193 pub path: PathBuf,
195 pub content: String,
197}
198
199#[derive(Debug, Clone)]
201pub struct GenerationResult {
202 pub files: Vec<GeneratedFile>,
204 pub mod_file: GeneratedFile,
206 pub required_deps: Vec<crate::type_mapping::DepRequirement>,
214}
215
216pub struct CodeGenerator {
217 config: GeneratorConfig,
218}
219
220impl CodeGenerator {
221 pub fn new(config: GeneratorConfig) -> Self {
222 Self { config }
223 }
224
225 pub fn config(&self) -> &GeneratorConfig {
227 &self.config
228 }
229
230 pub fn generate_all(&self, analysis: &mut SchemaAnalysis) -> Result<GenerationResult> {
232 let mut files = Vec::new();
233
234 if !self.config.registry_only {
235 let types_content = self.generate_types(analysis)?;
237 files.push(GeneratedFile {
238 path: "types.rs".into(),
239 content: types_content,
240 });
241
242 if let Some(ref streaming_config) = self.config.streaming_config {
244 let streaming_content =
245 self.generate_streaming_client(streaming_config, analysis)?;
246 files.push(GeneratedFile {
247 path: "streaming.rs".into(),
248 content: streaming_content,
249 });
250 }
251
252 if self.config.enable_async_client {
254 let http_content = self.generate_http_client(analysis)?;
255 files.push(GeneratedFile {
256 path: "client.rs".into(),
257 content: http_content,
258 });
259 }
260 }
261
262 if self.config.enable_registry || self.config.registry_only {
264 let registry_content = self.generate_registry(analysis)?;
265 files.push(GeneratedFile {
266 path: "registry.rs".into(),
267 content: registry_content,
268 });
269 }
270
271 let mod_content = self.generate_mod_file(&files)?;
273 let mod_file = GeneratedFile {
274 path: "mod.rs".into(),
275 content: mod_content,
276 };
277
278 let required_deps =
282 crate::type_mapping::collect_dep_requirements(&analysis.used_type_features);
283
284 Ok(GenerationResult {
285 files,
286 mod_file,
287 required_deps,
288 })
289 }
290
291 pub fn generate(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
293 self.generate_types(analysis)
294 }
295
296 fn generate_types(&self, analysis: &mut SchemaAnalysis) -> Result<String> {
298 let mut type_definitions = TokenStream::new();
299
300 let mut discriminated_variant_info: BTreeMap<String, DiscriminatedVariantInfo> =
303 BTreeMap::new();
304
305 let mut sorted_schemas: Vec<_> = analysis.schemas.iter().collect();
307 sorted_schemas.sort_by_key(|(name, _)| name.as_str());
308
309 for (_parent_name, schema) in sorted_schemas {
310 if let crate::analysis::SchemaType::DiscriminatedUnion {
311 variants,
312 discriminator_field,
313 } = &schema.schema_type
314 {
315 let is_parent_untagged =
317 self.should_use_untagged_discriminated_union(schema, analysis);
318
319 for variant in variants {
320 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
323 if let crate::analysis::SchemaType::Object { properties, .. } =
324 &variant_schema.schema_type
325 {
326 if properties.contains_key(discriminator_field) {
327 discriminated_variant_info.insert(
328 variant.type_name.clone(),
329 DiscriminatedVariantInfo {
330 discriminator_field: discriminator_field.clone(),
331 discriminator_value: variant.discriminator_value.clone(),
332 is_parent_untagged,
333 },
334 );
335 }
336 }
337 }
338 }
339 }
340 }
341
342 let generation_order = analysis.dependencies.topological_sort()?;
344
345 let mut emitted_rust_names: std::collections::HashSet<String> =
353 std::collections::HashSet::new();
354 let mut processed = std::collections::HashSet::new();
355
356 for schema_name in generation_order {
358 if let Some(schema) = analysis.schemas.get(&schema_name) {
359 let rust_name = self.to_rust_type_name(&schema.name);
360 if !emitted_rust_names.insert(rust_name) {
361 processed.insert(schema_name);
362 continue;
363 }
364 let type_def =
365 self.generate_type_definition(schema, analysis, &discriminated_variant_info)?;
366 if !type_def.is_empty() {
367 type_definitions.extend(type_def);
368 }
369 processed.insert(schema_name);
370 }
371 }
372
373 let mut remaining_schemas: Vec<_> = analysis
375 .schemas
376 .iter()
377 .filter(|(name, _)| !processed.contains(*name))
378 .collect();
379 remaining_schemas.sort_by_key(|(name, _)| name.as_str());
380
381 for (_schema_name, schema) in remaining_schemas {
382 let rust_name = self.to_rust_type_name(&schema.name);
383 if !emitted_rust_names.insert(rust_name) {
384 continue;
385 }
386 let type_def =
387 self.generate_type_definition(schema, analysis, &discriminated_variant_info)?;
388 if !type_def.is_empty() {
389 type_definitions.extend(type_def);
390 }
391 }
392
393 let base64_helper = if analysis
398 .used_type_features
399 .contains(crate::type_mapping::TypeFeature::Base64)
400 {
401 quote! {
402 mod base64_serde {
407 use base64::{Engine as _, engine::general_purpose::STANDARD};
408 use serde::{Deserialize, Deserializer, Serializer};
409
410 pub fn serialize<S: Serializer>(
411 bytes: &Vec<u8>,
412 ser: S,
413 ) -> Result<S::Ok, S::Error> {
414 ser.serialize_str(&STANDARD.encode(bytes))
415 }
416
417 pub fn deserialize<'de, D: Deserializer<'de>>(
418 de: D,
419 ) -> Result<Vec<u8>, D::Error> {
420 let s = String::deserialize(de)?;
421 STANDARD
422 .decode(s.as_bytes())
423 .map_err(serde::de::Error::custom)
424 }
425
426 pub mod option {
432 use super::*;
433 use serde::{Deserialize, Deserializer, Serializer};
434
435 pub fn serialize<S: Serializer>(
436 opt: &Option<Vec<u8>>,
437 ser: S,
438 ) -> Result<S::Ok, S::Error> {
439 match opt {
440 Some(bytes) => super::serialize(bytes, ser),
441 None => ser.serialize_none(),
442 }
443 }
444
445 pub fn deserialize<'de, D: Deserializer<'de>>(
446 de: D,
447 ) -> Result<Option<Vec<u8>>, D::Error> {
448 let opt = Option::<String>::deserialize(de)?;
449 opt.map(|s| {
450 STANDARD
451 .decode(s.as_bytes())
452 .map_err(serde::de::Error::custom)
453 })
454 .transpose()
455 }
456 }
457 }
458 }
459 } else {
460 TokenStream::new()
461 };
462
463 let time_date_helper = if analysis
470 .used_type_features
471 .contains(crate::type_mapping::TypeFeature::TimeDate)
472 {
473 quote! {
474 time::serde::format_description!(
475 time_date_format,
476 Date,
477 "[year]-[month]-[day]"
478 );
479 }
480 } else {
481 TokenStream::new()
482 };
483
484 let time_time_helper = if analysis
489 .used_type_features
490 .contains(crate::type_mapping::TypeFeature::TimeTime)
491 {
492 quote! {
493 time::serde::format_description!(
494 version = 2,
495 time_time_format,
496 Time,
497 "[hour]:[minute]:[second][optional [.[subsecond]]]"
498 );
499 }
500 } else {
501 TokenStream::new()
502 };
503
504 let generated = quote! {
506 #![allow(clippy::large_enum_variant)]
512 #![allow(clippy::format_in_format_args)]
513 #![allow(clippy::let_unit_value)]
514 #![allow(unreachable_patterns)]
515
516 use serde::{Deserialize, Serialize};
517
518 #base64_helper
519
520 #time_date_helper
521
522 #time_time_helper
523
524 #type_definitions
525 };
526
527 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
529 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
530 })?;
531
532 let formatted = prettyplease::unparse(&syntax_tree);
533
534 Ok(formatted)
535 }
536
537 fn generate_streaming_client(
539 &self,
540 streaming_config: &StreamingConfig,
541 analysis: &SchemaAnalysis,
542 ) -> Result<String> {
543 let mut client_code = TokenStream::new();
544
545 let imports = quote! {
547 #![allow(clippy::format_in_format_args)]
552 #![allow(clippy::let_unit_value)]
553 #![allow(unused_mut)]
554
555 use super::types::*;
556 use async_trait::async_trait;
557 use futures_util::{Stream, StreamExt};
558 use std::pin::Pin;
559 use std::time::Duration;
560 use reqwest::header::{HeaderMap, HeaderValue};
561 use tracing::{debug, error, info, warn, instrument};
562 };
563 client_code.extend(imports);
564
565 if streaming_config.generate_client {
567 let error_types = self.generate_streaming_error_types()?;
568 client_code.extend(error_types);
569 }
570
571 for endpoint in &streaming_config.endpoints {
573 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
574 client_code.extend(trait_code);
575 }
576
577 if streaming_config.generate_client {
579 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
580 client_code.extend(client_impl);
581 }
582
583 if streaming_config.event_parser_helpers {
585 let parser_code = self.generate_sse_parser_utilities(streaming_config)?;
586 client_code.extend(parser_code);
587 }
588
589 if let Some(reconnect_config) = &streaming_config.reconnection_config {
591 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
592 client_code.extend(reconnect_code);
593 }
594
595 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
596 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
597 })?;
598
599 Ok(prettyplease::unparse(&syntax_tree))
600 }
601
602 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
604 let error_types = self.generate_http_error_types();
605 let client_struct = self.generate_http_client_struct();
606 let operation_methods = self.generate_operation_methods(analysis);
607
608 let generated = quote! {
609 #![allow(clippy::format_in_format_args)]
614 #![allow(clippy::let_unit_value)]
615
616 use super::types::*;
617
618 #error_types
619
620 #client_struct
621
622 #operation_methods
623 };
624
625 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
626 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
627 })?;
628
629 Ok(prettyplease::unparse(&syntax_tree))
630 }
631
632 fn generate_http_error_types(&self) -> TokenStream {
634 quote! {
635 use thiserror::Error;
636
637 #[derive(Error, Debug)]
645 pub enum HttpError {
646 #[error("Network error: {0}")]
648 Network(#[from] reqwest::Error),
649
650 #[error("Middleware error: {0}")]
652 Middleware(#[from] reqwest_middleware::Error),
653
654 #[error("Failed to serialize request: {0}")]
656 Serialization(String),
657
658 #[error("Authentication error: {0}")]
660 Auth(String),
661
662 #[error("Request timeout")]
664 Timeout,
665
666 #[error("Configuration error: {0}")]
668 Config(String),
669
670 #[error("{0}")]
672 Other(String),
673 }
674
675 impl HttpError {
676 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
678 Self::Serialization(error.to_string())
679 }
680
681 pub fn is_retryable(&self) -> bool {
683 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
684 }
685 }
686
687 #[derive(Debug, Clone)]
697 pub struct ApiError<E> {
698 pub status: u16,
699 pub headers: reqwest::header::HeaderMap,
700 pub body: String,
701 pub typed: Option<E>,
702 pub parse_error: Option<String>,
703 }
704
705 impl<E> ApiError<E> {
706 pub fn is_client_error(&self) -> bool {
707 (400..500).contains(&self.status)
708 }
709
710 pub fn is_server_error(&self) -> bool {
711 (500..600).contains(&self.status)
712 }
713
714 pub fn is_retryable(&self) -> bool {
717 matches!(self.status, 429 | 500 | 502 | 503 | 504)
718 }
719 }
720
721 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723 write!(f, "API error {}: {}", self.status, self.body)
724 }
725 }
726
727 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
728
729 #[derive(Debug, Error)]
737 pub enum ApiOpError<E: std::fmt::Debug> {
738 #[error(transparent)]
739 Transport(#[from] HttpError),
740
741 #[error(transparent)]
742 Api(ApiError<E>),
743 }
744
745 impl<E: std::fmt::Debug> ApiOpError<E> {
746 pub fn api(&self) -> Option<&ApiError<E>> {
748 match self {
749 Self::Api(e) => Some(e),
750 Self::Transport(_) => None,
751 }
752 }
753
754 pub fn is_api_error(&self) -> bool {
757 matches!(self, Self::Api(_))
758 }
759 }
760
761 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
764 fn from(e: reqwest::Error) -> Self {
765 Self::Transport(HttpError::Network(e))
766 }
767 }
768
769 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
770 fn from(e: reqwest_middleware::Error) -> Self {
771 Self::Transport(HttpError::Middleware(e))
772 }
773 }
774
775 pub type HttpResult<T> = Result<T, HttpError>;
779 }
780 }
781
782 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
784 let mut module_declarations = Vec::new();
785 let mut pub_uses = Vec::new();
786
787 for file in files {
788 if let Some(module_name) = file.path.file_stem().and_then(|s| s.to_str()) {
789 if module_name != "mod" {
790 module_declarations.push(format!("pub mod {module_name};"));
791 pub_uses.push(format!("pub use {module_name}::*;"));
792 }
793 }
794 }
795
796 let mount_hint = format!(
803 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
804 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
805 name = self.config.module_name,
806 );
807
808 let content = format!(
809 r#"//! Generated API modules
810//!
811//! This module exports all generated API types and clients.
812//! Do not edit manually - regenerate using the appropriate script.
813//!
814{mount_hint}
815#![allow(unused_imports)]
816
817{decls}
818
819{uses}
820"#,
821 mount_hint = mount_hint,
822 decls = module_declarations.join("\n"),
823 uses = pub_uses.join("\n"),
824 );
825
826 Ok(content)
827 }
828
829 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
831 use std::fs;
832
833 fs::create_dir_all(&self.config.output_dir)?;
835
836 for file in &result.files {
838 let file_path = self.config.output_dir.join(&file.path);
839 fs::write(&file_path, &file.content)?;
840 }
841
842 let mod_path = self.config.output_dir.join(&result.mod_file.path);
844 fs::write(&mod_path, &result.mod_file.content)?;
845
846 if let Some(toml) = crate::type_mapping::render_required_deps_toml(&result.required_deps) {
852 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
853 fs::write(&deps_path, toml)?;
854 }
855
856 Ok(())
857 }
858
859 fn generate_type_definition(
860 &self,
861 schema: &crate::analysis::AnalyzedSchema,
862 analysis: &crate::analysis::SchemaAnalysis,
863 discriminated_variant_info: &BTreeMap<String, DiscriminatedVariantInfo>,
864 ) -> Result<TokenStream> {
865 use crate::analysis::SchemaType;
866
867 match &schema.schema_type {
868 SchemaType::Primitive { rust_type, .. } => {
869 self.generate_type_alias(schema, rust_type)
871 }
872 SchemaType::StringEnum { values } => {
873 let ext = analysis.enum_extensions.get(&schema.name);
874 let rust_name = self.to_rust_type_name(&schema.name);
881 let force_extensible = self
882 .config
883 .extensible_enum_overrides
884 .get(&schema.name)
885 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
886 .copied()
887 .unwrap_or(false);
888 if force_extensible {
889 self.generate_extensible_enum(schema, values, ext)
890 } else {
891 self.generate_string_enum(schema, values, ext)
892 }
893 }
894 SchemaType::ExtensibleEnum { known_values } => {
895 let ext = analysis.enum_extensions.get(&schema.name);
896 self.generate_extensible_enum(schema, known_values, ext)
897 }
898 SchemaType::Object {
899 properties,
900 required,
901 additional_properties,
902 } => self.generate_struct(
903 schema,
904 properties,
905 required,
906 additional_properties,
907 analysis,
908 discriminated_variant_info.get(&schema.name),
909 ),
910 SchemaType::DiscriminatedUnion {
911 discriminator_field,
912 variants,
913 } => {
914 if self.should_use_untagged_discriminated_union(schema, analysis) {
916 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
918 .iter()
919 .map(|v| crate::analysis::SchemaRef {
920 target: v.type_name.clone(),
921 nullable: false,
922 })
923 .collect();
924 self.generate_union_enum(schema, &schema_refs, analysis)
925 } else {
926 self.generate_discriminated_enum(
927 schema,
928 discriminator_field,
929 variants,
930 analysis,
931 )
932 }
933 }
934 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
935 SchemaType::Reference { target } => {
936 if schema.name != *target {
939 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
941 let target_type = format_ident!("{}", self.to_rust_type_name(target));
942
943 let doc_comment = if let Some(desc) = &schema.description {
944 quote! { #[doc = #desc] }
945 } else {
946 TokenStream::new()
947 };
948
949 Ok(quote! {
950 #doc_comment
951 pub type #alias_name = #target_type;
952 })
953 } else {
954 Ok(TokenStream::new())
956 }
957 }
958 SchemaType::Array { item_type } => {
959 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
967
968 if let SchemaType::Reference { target } = item_type.as_ref() {
970 if let Some(info) = discriminated_variant_info.get(target) {
971 if !info.is_parent_untagged {
972 let wrapper_name =
974 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
975 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
976 let disc_field = &info.discriminator_field;
977 let disc_value = &info.discriminator_value;
978
979 let doc_comment = if let Some(desc) = &schema.description {
980 quote! { #[doc = #desc] }
981 } else {
982 TokenStream::new()
983 };
984
985 return Ok(quote! {
986 #[derive(Debug, Clone, Deserialize, Serialize)]
990 #[serde(tag = #disc_field)]
991 pub enum #wrapper_name {
992 #[serde(rename = #disc_value)]
993 #variant_type(#variant_type),
994 }
995 #doc_comment
996 pub type #array_name = Vec<#wrapper_name>;
997 });
998 }
999 }
1000 }
1001
1002 let inner_type = self.generate_array_item_type(item_type, analysis);
1003
1004 let doc_comment = if let Some(desc) = &schema.description {
1005 quote! { #[doc = #desc] }
1006 } else {
1007 TokenStream::new()
1008 };
1009
1010 Ok(quote! {
1011 #doc_comment
1012 pub type #array_name = Vec<#inner_type>;
1013 })
1014 }
1015 SchemaType::Composition { schemas } => {
1016 self.generate_composition_struct(schema, schemas)
1017 }
1018 }
1019 }
1020
1021 fn generate_type_alias(
1022 &self,
1023 schema: &crate::analysis::AnalyzedSchema,
1024 rust_type: &str,
1025 ) -> Result<TokenStream> {
1026 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1027 let base_type = parse_rust_type(rust_type)?;
1031
1032 let doc_comment = if let Some(desc) = &schema.description {
1033 let sanitized_desc = self.sanitize_doc_comment(desc);
1034 quote! { #[doc = #sanitized_desc] }
1035 } else {
1036 TokenStream::new()
1037 };
1038
1039 Ok(quote! {
1040 #doc_comment
1041 pub type #type_name = #base_type;
1042 })
1043 }
1044
1045 fn generate_extensible_enum(
1046 &self,
1047 schema: &crate::analysis::AnalyzedSchema,
1048 known_values: &[String],
1049 ext: Option<&crate::analysis::EnumExtensions>,
1050 ) -> Result<TokenStream> {
1051 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1052
1053 let doc_comment = if let Some(desc) = &schema.description {
1054 quote! { #[doc = #desc] }
1055 } else {
1056 TokenStream::new()
1057 };
1058
1059 let varnames_override: Option<&Vec<String>> = ext
1063 .filter(|_| self.config.types.x_enum_varnames_enabled())
1064 .map(|e| &e.varnames)
1065 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1066 let descriptions_override: Option<&Vec<String>> = ext
1067 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1068 .map(|e| &e.descriptions)
1069 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1070
1071 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1072 let name = match varnames_override {
1073 Some(v) => v[index].clone(),
1074 None => self.to_rust_enum_variant(value),
1075 };
1076 format_ident!("{}", name)
1077 };
1078
1079 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1084 let variant_ident = variant_ident_for(i, value);
1085 let doc = descriptions_override
1086 .map(|d| {
1087 let s = self.sanitize_doc_comment(&d[i]);
1088 quote! { #[doc = #s] }
1089 })
1090 .unwrap_or_default();
1091 quote! {
1092 #doc
1093 #variant_ident,
1094 }
1095 });
1096
1097 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1098 let variant_ident = variant_ident_for(i, value);
1099 quote! {
1100 #value => Ok(#enum_name::#variant_ident),
1101 }
1102 });
1103
1104 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1105 let variant_ident = variant_ident_for(i, value);
1106 quote! {
1107 #enum_name::#variant_ident => #value,
1108 }
1109 });
1110
1111 let derives = if self.config.enable_specta {
1112 quote! {
1113 #[derive(Debug, Clone, PartialEq, Eq)]
1114 #[cfg_attr(feature = "specta", derive(specta::Type))]
1115 }
1116 } else {
1117 quote! {
1118 #[derive(Debug, Clone, PartialEq, Eq)]
1119 }
1120 };
1121
1122 Ok(quote! {
1123 #doc_comment
1124 #derives
1125 pub enum #enum_name {
1126 #(#known_variants)*
1127 Custom(String),
1129 }
1130
1131 impl<'de> serde::Deserialize<'de> for #enum_name {
1132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1133 where
1134 D: serde::Deserializer<'de>,
1135 {
1136 let value = String::deserialize(deserializer)?;
1137 match value.as_str() {
1138 #(#match_arms_de)*
1139 _ => Ok(#enum_name::Custom(value)),
1140 }
1141 }
1142 }
1143
1144 impl serde::Serialize for #enum_name {
1145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1146 where
1147 S: serde::Serializer,
1148 {
1149 let value = match self {
1150 #(#match_arms_ser)*
1151 #enum_name::Custom(s) => s.as_str(),
1152 };
1153 serializer.serialize_str(value)
1154 }
1155 }
1156 })
1157 }
1158
1159 fn generate_string_enum(
1160 &self,
1161 schema: &crate::analysis::AnalyzedSchema,
1162 values: &[String],
1163 ext: Option<&crate::analysis::EnumExtensions>,
1164 ) -> Result<TokenStream> {
1165 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1166
1167 let default_value = schema
1174 .default
1175 .as_ref()
1176 .and_then(|v| v.as_str())
1177 .map(|s| s.to_string());
1178 let has_default_match = match &default_value {
1179 Some(d) => values.iter().any(|v| v == d),
1180 None => !values.is_empty(),
1181 };
1182
1183 let varnames_override: Option<&Vec<String>> = ext
1187 .filter(|_| self.config.types.x_enum_varnames_enabled())
1188 .map(|e| &e.varnames)
1189 .filter(|v| !v.is_empty() && v.len() == values.len());
1190 let descriptions_override: Option<&Vec<String>> = ext
1191 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1192 .map(|e| &e.descriptions)
1193 .filter(|v| !v.is_empty() && v.len() == values.len());
1194
1195 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1202 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1203 .iter()
1204 .enumerate()
1205 .map(|(i, value)| {
1206 let base = match varnames_override {
1207 Some(v) => v[i].clone(),
1208 None => self.to_rust_enum_variant(value),
1209 };
1210 let mut variant_name = base.clone();
1211 let mut suffix = 2;
1212 while !used.insert(variant_name.clone()) {
1213 variant_name = format!("{base}_{suffix}");
1214 suffix += 1;
1215 }
1216 let variant_ident = format_ident!("{}", variant_name);
1217 let is_default = if let Some(ref default) = default_value {
1218 value == default
1219 } else {
1220 i == 0
1221 };
1222 let description = descriptions_override.map(|d| d[i].clone());
1223 (variant_ident, value, is_default, description)
1224 })
1225 .collect();
1226
1227 let variants =
1228 variant_pairs
1229 .iter()
1230 .map(|(variant_ident, value, is_default, description)| {
1231 let doc = description
1232 .as_ref()
1233 .map(|d| {
1234 let s = self.sanitize_doc_comment(d);
1235 quote! { #[doc = #s] }
1236 })
1237 .unwrap_or_default();
1238 if *is_default {
1239 quote! {
1240 #doc
1241 #[default]
1242 #[serde(rename = #value)]
1243 #variant_ident,
1244 }
1245 } else {
1246 quote! {
1247 #doc
1248 #[serde(rename = #value)]
1249 #variant_ident,
1250 }
1251 }
1252 });
1253
1254 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1258 quote! { Self::#variant_ident => #value, }
1259 });
1260
1261 let doc_comment = if let Some(desc) = &schema.description {
1262 quote! { #[doc = #desc] }
1263 } else {
1264 TokenStream::new()
1265 };
1266
1267 let derives = match (self.config.enable_specta, has_default_match) {
1270 (true, true) => quote! {
1271 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1272 #[cfg_attr(feature = "specta", derive(specta::Type))]
1273 },
1274 (true, false) => quote! {
1275 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1276 #[cfg_attr(feature = "specta", derive(specta::Type))]
1277 },
1278 (false, true) => quote! {
1279 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1280 },
1281 (false, false) => quote! {
1282 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1283 },
1284 };
1285
1286 Ok(quote! {
1287 #doc_comment
1288 #derives
1289 pub enum #enum_name {
1290 #(#variants)*
1291 }
1292
1293 impl #enum_name {
1294 pub fn as_str(&self) -> &'static str {
1295 match self {
1296 #(#as_str_arms)*
1297 }
1298 }
1299 }
1300
1301 impl ::std::fmt::Display for #enum_name {
1302 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1303 f.write_str(self.as_str())
1304 }
1305 }
1306
1307 impl AsRef<str> for #enum_name {
1308 fn as_ref(&self) -> &str {
1309 self.as_str()
1310 }
1311 }
1312 })
1313 }
1314
1315 fn generate_struct(
1316 &self,
1317 schema: &crate::analysis::AnalyzedSchema,
1318 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1319 required: &std::collections::HashSet<String>,
1320 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1321 analysis: &crate::analysis::SchemaAnalysis,
1322 discriminator_info: Option<&DiscriminatedVariantInfo>,
1323 ) -> Result<TokenStream> {
1324 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1325
1326 let mut sorted_properties: Vec<_> = properties.iter().collect();
1328 sorted_properties.sort_by_key(|(name, _)| name.as_str());
1329
1330 let mut used_field_idents: std::collections::HashSet<String> =
1338 std::collections::HashSet::new();
1339
1340 let mut fields: Vec<TokenStream> = sorted_properties
1341 .into_iter()
1342 .filter(|(field_name, _)| {
1343 if let Some(info) = discriminator_info {
1347 if !info.is_parent_untagged
1348 && field_name.as_str() == info.discriminator_field.as_str()
1349 {
1350 false } else {
1352 true }
1354 } else {
1355 true }
1357 })
1358 .map(|(field_name, prop)| {
1359 let raw = self.to_rust_field_name(field_name);
1360 let mut chosen = raw.clone();
1361 let mut suffix = 2;
1362 while !used_field_idents.insert(chosen.clone()) {
1363 chosen = format!("{raw}_{suffix}");
1364 suffix += 1;
1365 }
1366 let field_ident = Self::to_field_ident(&chosen);
1367 let is_required = required.contains(field_name);
1368 let field_type =
1369 self.generate_field_type(&schema.name, field_name, prop, is_required, analysis);
1370
1371 let serde_attrs =
1372 self.generate_serde_field_attrs(field_name, prop, is_required, analysis);
1373 let specta_attrs = self.generate_specta_field_attrs(field_name);
1374
1375 let doc_comment = if let Some(desc) = &prop.description {
1376 let sanitized_desc = self.sanitize_doc_comment(desc);
1377 quote! { #[doc = #sanitized_desc] }
1378 } else {
1379 TokenStream::new()
1380 };
1381 let constraint_doc = self.generate_constraint_doc(&prop.constraints);
1382
1383 quote! {
1384 #doc_comment
1385 #constraint_doc
1386 #serde_attrs
1387 #specta_attrs
1388 pub #field_ident: #field_type,
1389 }
1390 })
1391 .collect();
1392
1393 match additional_properties {
1399 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1400 crate::analysis::ObjectAdditionalProperties::Untyped => {
1401 fields.push(quote! {
1402 #[serde(flatten)]
1404 pub additional_properties:
1405 std::collections::BTreeMap<String, serde_json::Value>,
1406 });
1407 }
1408 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1409 let value_tokens = self.generate_array_item_type(value_type, analysis);
1410 fields.push(quote! {
1411 #[serde(flatten)]
1414 pub additional_properties:
1415 std::collections::BTreeMap<String, #value_tokens>,
1416 });
1417 }
1418 }
1419
1420 let doc_comment = if let Some(desc) = &schema.description {
1421 quote! { #[doc = #desc] }
1422 } else {
1423 TokenStream::new()
1424 };
1425
1426 let derives = if self.config.enable_specta {
1430 quote! {
1431 #[derive(Debug, Clone, Deserialize, Serialize)]
1432 #[cfg_attr(feature = "specta", derive(specta::Type))]
1433 }
1434 } else {
1435 quote! {
1436 #[derive(Debug, Clone, Deserialize, Serialize)]
1437 }
1438 };
1439
1440 Ok(quote! {
1441 #doc_comment
1442 #derives
1443 pub struct #struct_name {
1444 #(#fields)*
1445 }
1446 })
1447 }
1448
1449 fn generate_discriminated_enum(
1450 &self,
1451 schema: &crate::analysis::AnalyzedSchema,
1452 discriminator_field: &str,
1453 variants: &[crate::analysis::UnionVariant],
1454 analysis: &crate::analysis::SchemaAnalysis,
1455 ) -> Result<TokenStream> {
1456 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1457
1458 let has_nested_discriminated_union = variants.iter().any(|variant| {
1460 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
1461 matches!(
1462 variant_schema.schema_type,
1463 crate::analysis::SchemaType::DiscriminatedUnion { .. }
1464 )
1465 } else {
1466 false
1467 }
1468 });
1469
1470 if has_nested_discriminated_union {
1472 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1474 .iter()
1475 .map(|v| crate::analysis::SchemaRef {
1476 target: v.type_name.clone(),
1477 nullable: false,
1478 })
1479 .collect();
1480 return self.generate_union_enum(schema, &schema_refs, analysis);
1481 }
1482
1483 let enclosing = self.to_rust_type_name(&schema.name);
1484 let enum_variants = variants.iter().map(|variant| {
1485 let variant_name = format_ident!("{}", variant.rust_name);
1486 let variant_value = &variant.discriminator_value;
1487
1488 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
1489 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
1493 || analysis
1494 .dependencies
1495 .recursive_schemas
1496 .contains(&variant.type_name)
1497 {
1498 quote! { Box<#variant_type> }
1499 } else {
1500 quote! { #variant_type }
1501 };
1502 quote! {
1503 #[serde(rename = #variant_value)]
1504 #variant_name(#payload),
1505 }
1506 });
1507
1508 let doc_comment = if let Some(desc) = &schema.description {
1509 quote! { #[doc = #desc] }
1510 } else {
1511 TokenStream::new()
1512 };
1513
1514 let derives = if self.config.enable_specta {
1516 quote! {
1517 #[derive(Debug, Clone, Deserialize, Serialize)]
1518 #[cfg_attr(feature = "specta", derive(specta::Type))]
1519 #[serde(tag = #discriminator_field)]
1520 }
1521 } else {
1522 quote! {
1523 #[derive(Debug, Clone, Deserialize, Serialize)]
1524 #[serde(tag = #discriminator_field)]
1525 }
1526 };
1527
1528 Ok(quote! {
1529 #doc_comment
1530 #derives
1531 pub enum #enum_name {
1532 #(#enum_variants)*
1533 }
1534 })
1535 }
1536
1537 fn should_use_untagged_discriminated_union(
1539 &self,
1540 schema: &crate::analysis::AnalyzedSchema,
1541 analysis: &crate::analysis::SchemaAnalysis,
1542 ) -> bool {
1543 for other_schema in analysis.schemas.values() {
1548 if let crate::analysis::SchemaType::DiscriminatedUnion {
1549 variants,
1550 discriminator_field: _,
1551 } = &other_schema.schema_type
1552 {
1553 for variant in variants {
1554 if variant.type_name == schema.name {
1555 if let crate::analysis::SchemaType::DiscriminatedUnion {
1560 discriminator_field: current_discriminator,
1561 variants: current_variants,
1562 ..
1563 } = &schema.schema_type
1564 {
1565 for current_variant in current_variants {
1567 if let Some(variant_schema) =
1568 analysis.schemas.get(¤t_variant.type_name)
1569 {
1570 if let crate::analysis::SchemaType::Object {
1571 properties, ..
1572 } = &variant_schema.schema_type
1573 {
1574 if properties.contains_key(current_discriminator) {
1575 return false;
1578 }
1579 }
1580 }
1581 }
1582 }
1583
1584 return true;
1586 }
1587 }
1588 }
1589 }
1590 false
1591 }
1592
1593 fn generate_union_enum(
1594 &self,
1595 schema: &crate::analysis::AnalyzedSchema,
1596 variants: &[crate::analysis::SchemaRef],
1597 analysis: &crate::analysis::SchemaAnalysis,
1598 ) -> Result<TokenStream> {
1599 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1600
1601 let mut used_variant_names = std::collections::HashSet::new();
1603 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
1604 let base_variant_name = self.type_name_to_variant_name(&variant.target);
1606 let variant_name = self.ensure_unique_variant_name_generator(
1607 base_variant_name,
1608 &mut used_variant_names,
1609 i,
1610 );
1611 let variant_name_ident = format_ident!("{}", variant_name);
1612
1613 let variant_type_tokens = if matches!(
1615 variant.target.as_str(),
1616 "bool"
1617 | "i8"
1618 | "i16"
1619 | "i32"
1620 | "i64"
1621 | "i128"
1622 | "u8"
1623 | "u16"
1624 | "u32"
1625 | "u64"
1626 | "u128"
1627 | "f32"
1628 | "f64"
1629 | "String"
1630 ) {
1631 let type_ident = format_ident!("{}", variant.target);
1632 quote! { #type_ident }
1633 } else if variant.target == "serde_json::Value" {
1634 quote! { serde_json::Value }
1637 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
1638 let inner = &variant.target[4..variant.target.len() - 1];
1640
1641 if inner.starts_with("Vec<") && inner.ends_with(">") {
1643 let inner_inner = &inner[4..inner.len() - 1];
1644 if inner_inner == "serde_json::Value" {
1645 quote! { Vec<Vec<serde_json::Value>> }
1646 } else {
1647 let inner_inner_type = if matches!(
1648 inner_inner,
1649 "bool"
1650 | "i8"
1651 | "i16"
1652 | "i32"
1653 | "i64"
1654 | "i128"
1655 | "u8"
1656 | "u16"
1657 | "u32"
1658 | "u64"
1659 | "u128"
1660 | "f32"
1661 | "f64"
1662 | "String"
1663 ) {
1664 format_ident!("{}", inner_inner)
1665 } else {
1666 format_ident!("{}", self.to_rust_type_name(inner_inner))
1667 };
1668 quote! { Vec<Vec<#inner_inner_type>> }
1669 }
1670 } else if inner == "serde_json::Value" {
1671 quote! { Vec<serde_json::Value> }
1672 } else {
1673 let inner_type = if matches!(
1674 inner,
1675 "bool"
1676 | "i8"
1677 | "i16"
1678 | "i32"
1679 | "i64"
1680 | "i128"
1681 | "u8"
1682 | "u16"
1683 | "u32"
1684 | "u64"
1685 | "u128"
1686 | "f32"
1687 | "f64"
1688 | "String"
1689 ) {
1690 format_ident!("{}", inner)
1691 } else {
1692 format_ident!("{}", self.to_rust_type_name(inner))
1693 };
1694 quote! { Vec<#inner_type> }
1695 }
1696 } else if variant.target.contains("::") || variant.target.contains('<') {
1697 parse_rust_type(&variant.target).unwrap_or_else(|_| {
1702 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
1703 quote! { #fallback }
1704 })
1705 } else {
1706 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
1707 quote! { #type_ident }
1708 };
1709
1710 let target_rust_name = self.to_rust_type_name(&variant.target);
1714 let enclosing_name = self.to_rust_type_name(&schema.name);
1715 let is_self_ref = target_rust_name == enclosing_name;
1716 let is_recursive_target = analysis
1720 .dependencies
1721 .recursive_schemas
1722 .contains(&variant.target);
1723 let variant_type_tokens = if is_self_ref || is_recursive_target {
1724 quote! { Box<#variant_type_tokens> }
1725 } else {
1726 variant_type_tokens
1727 };
1728
1729 quote! {
1730 #variant_name_ident(#variant_type_tokens),
1731 }
1732 });
1733
1734 let doc_comment = if let Some(desc) = &schema.description {
1735 quote! { #[doc = #desc] }
1736 } else {
1737 TokenStream::new()
1738 };
1739
1740 let derives = if self.config.enable_specta {
1742 quote! {
1743 #[derive(Debug, Clone, Deserialize, Serialize)]
1744 #[cfg_attr(feature = "specta", derive(specta::Type))]
1745 #[serde(untagged)]
1746 }
1747 } else {
1748 quote! {
1749 #[derive(Debug, Clone, Deserialize, Serialize)]
1750 #[serde(untagged)]
1751 }
1752 };
1753
1754 Ok(quote! {
1755 #doc_comment
1756 #derives
1757 pub enum #enum_name {
1758 #(#enum_variants)*
1759 }
1760 })
1761 }
1762
1763 fn target_aliases_back_to(
1768 &self,
1769 target: &str,
1770 enclosing_rust_name: &str,
1771 analysis: &crate::analysis::SchemaAnalysis,
1772 ) -> bool {
1773 let mut current = target.to_string();
1774 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
1775 for _ in 0..16 {
1776 if !visited.insert(current.clone()) {
1777 return true;
1778 }
1779 let Some(schema) = analysis.schemas.get(¤t) else {
1780 return false;
1781 };
1782 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
1783 if self.to_rust_type_name(next) == enclosing_rust_name {
1784 return true;
1785 }
1786 current = next.clone();
1787 continue;
1788 }
1789 return false;
1790 }
1791 false
1792 }
1793
1794 fn generate_field_type(
1795 &self,
1796 schema_name: &str,
1797 field_name: &str,
1798 prop: &crate::analysis::PropertyInfo,
1799 is_required: bool,
1800 analysis: &crate::analysis::SchemaAnalysis,
1801 ) -> TokenStream {
1802 use crate::analysis::SchemaType;
1803
1804 let base_type = match &prop.schema_type {
1805 SchemaType::Primitive { rust_type, .. } => {
1806 parse_rust_type(rust_type).unwrap_or_else(|_| {
1809 eprintln!(
1814 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
1815 falling back to String"
1816 );
1817 quote! { String }
1818 })
1819 }
1820 SchemaType::Reference { target } => {
1821 let target_rust_name = self.to_rust_type_name(target);
1822 let target_type = format_ident!("{}", target_rust_name);
1823 let enclosing_rust_name = self.to_rust_type_name(schema_name);
1836 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
1837 let is_alias_chain_self =
1838 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
1839 if analysis.dependencies.recursive_schemas.contains(target)
1840 || is_self_via_rust_name
1841 || is_alias_chain_self
1842 {
1843 quote! { Box<#target_type> }
1844 } else {
1845 quote! { #target_type }
1846 }
1847 }
1848 SchemaType::Array { item_type } => {
1849 let inner_type = self.generate_array_item_type(item_type, analysis);
1850 quote! { Vec<#inner_type> }
1851 }
1852 _ => {
1853 quote! { serde_json::Value }
1855 }
1856 };
1857
1858 let override_key = format!("{schema_name}.{field_name}");
1860 let is_nullable_override = self
1861 .config
1862 .nullable_field_overrides
1863 .get(&override_key)
1864 .copied()
1865 .unwrap_or(false);
1866
1867 if is_required && !prop.nullable && !is_nullable_override {
1868 if prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis) {
1871 quote! { Option<#base_type> }
1872 } else {
1873 base_type
1874 }
1875 } else {
1876 quote! { Option<#base_type> }
1877 }
1878 }
1879
1880 fn generate_serde_field_attrs(
1881 &self,
1882 field_name: &str,
1883 prop: &crate::analysis::PropertyInfo,
1884 is_required: bool,
1885 analysis: &crate::analysis::SchemaAnalysis,
1886 ) -> TokenStream {
1887 let mut attrs = Vec::new();
1888
1889 let rust_field_name = self.to_rust_field_name(field_name);
1892 let comparison_name = rust_field_name
1893 .strip_prefix("r#")
1894 .unwrap_or(&rust_field_name);
1895 if comparison_name != field_name {
1896 attrs.push(quote! { rename = #field_name });
1897 }
1898
1899 if !is_required || prop.nullable {
1901 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
1902 }
1903
1904 if prop.default.is_some()
1908 && (is_required && !prop.nullable)
1909 && !self.type_lacks_default(&prop.schema_type, analysis)
1910 {
1911 attrs.push(quote! { default });
1912 }
1913
1914 if let crate::analysis::SchemaType::Primitive {
1922 serde_with: Some(codec),
1923 ..
1924 } = &prop.schema_type
1925 {
1926 let is_option_wrapped = !is_required || prop.nullable;
1930 let codec_path = if is_option_wrapped {
1931 format!("{codec}::option")
1932 } else {
1933 codec.clone()
1934 };
1935 attrs.push(quote! { with = #codec_path });
1936 if is_option_wrapped {
1941 attrs.push(quote! { default });
1942 }
1943 }
1944
1945 if attrs.is_empty() {
1946 TokenStream::new()
1947 } else {
1948 quote! { #[serde(#(#attrs),*)] }
1949 }
1950 }
1951
1952 fn type_lacks_default(
1956 &self,
1957 schema_type: &crate::analysis::SchemaType,
1958 analysis: &crate::analysis::SchemaAnalysis,
1959 ) -> bool {
1960 use crate::analysis::SchemaType;
1961 match schema_type {
1962 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
1963 SchemaType::Primitive { rust_type, .. } => matches!(
1967 rust_type.as_str(),
1968 "chrono::DateTime<chrono::Utc>"
1969 | "chrono::NaiveDate"
1970 | "chrono::NaiveTime"
1971 | "chrono::Duration"
1972 | "url::Url"
1973 | "time::OffsetDateTime"
1974 | "time::Date"
1975 | "time::Time"
1976 | "iso8601::Duration"
1977 | "email_address::EmailAddress"
1978 ),
1979 SchemaType::Reference { target } => {
1980 if let Some(schema) = analysis.schemas.get(target) {
1981 self.type_lacks_default(&schema.schema_type, analysis)
1982 } else {
1983 false
1984 }
1985 }
1986 _ => false,
1987 }
1988 }
1989
1990 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
1991 if !self.config.enable_specta {
1992 return TokenStream::new();
1993 }
1994
1995 let camel_case_name = self.to_camel_case(field_name);
1997
1998 if camel_case_name != field_name {
2000 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
2001 } else {
2002 TokenStream::new()
2003 }
2004 }
2005
2006 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
2007 let neg_prefix =
2011 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
2012 "Neg"
2013 } else {
2014 ""
2015 };
2016
2017 let mut result = String::new();
2019 let mut next_upper = true;
2020 let mut prev_was_upper = false;
2021
2022 for (i, c) in s.chars().enumerate() {
2023 match c {
2024 'a'..='z' => {
2025 if next_upper {
2026 result.push(c.to_ascii_uppercase());
2027 next_upper = false;
2028 } else {
2029 result.push(c);
2030 }
2031 prev_was_upper = false;
2032 }
2033 'A'..='Z' => {
2034 if next_upper || (!prev_was_upper && i > 0) {
2035 result.push(c);
2037 next_upper = false;
2038 } else {
2039 result.push(c.to_ascii_lowercase());
2041 }
2042 prev_was_upper = true;
2043 }
2044 '0'..='9' => {
2045 result.push(c);
2046 next_upper = false;
2047 prev_was_upper = false;
2048 }
2049 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
2050 next_upper = true;
2052 prev_was_upper = false;
2053 }
2054 _ => {
2055 next_upper = true;
2057 prev_was_upper = false;
2058 }
2059 }
2060 }
2061
2062 if result.is_empty() {
2064 result = "Value".to_string();
2065 }
2066
2067 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2069 result = format!("Variant{neg_prefix}{result}");
2070 } else if !neg_prefix.is_empty() {
2071 result = format!("{neg_prefix}{result}");
2074 }
2075
2076 match result.as_str() {
2078 "Null" => "NullValue".to_string(),
2079 "True" => "TrueValue".to_string(),
2080 "False" => "FalseValue".to_string(),
2081 "Type" => "Type_".to_string(),
2082 "Match" => "Match_".to_string(),
2083 "Fn" => "Fn_".to_string(),
2084 "Impl" => "Impl_".to_string(),
2085 "Trait" => "Trait_".to_string(),
2086 "Struct" => "Struct_".to_string(),
2087 "Enum" => "Enum_".to_string(),
2088 "Mod" => "Mod_".to_string(),
2089 "Use" => "Use_".to_string(),
2090 "Pub" => "Pub_".to_string(),
2091 "Const" => "Const_".to_string(),
2092 "Static" => "Static_".to_string(),
2093 "Let" => "Let_".to_string(),
2094 "Mut" => "Mut_".to_string(),
2095 "Ref" => "Ref_".to_string(),
2096 "Move" => "Move_".to_string(),
2097 "Return" => "Return_".to_string(),
2098 "If" => "If_".to_string(),
2099 "Else" => "Else_".to_string(),
2100 "While" => "While_".to_string(),
2101 "For" => "For_".to_string(),
2102 "Loop" => "Loop_".to_string(),
2103 "Break" => "Break_".to_string(),
2104 "Continue" => "Continue_".to_string(),
2105 "Self" => "Self_".to_string(),
2106 "Super" => "Super_".to_string(),
2107 "Crate" => "Crate_".to_string(),
2108 "Async" => "Async_".to_string(),
2109 "Await" => "Await_".to_string(),
2110 _ => result,
2111 }
2112 }
2113
2114 #[allow(dead_code)]
2115 fn to_rust_identifier(&self, s: &str) -> String {
2116 let mut result = s
2118 .chars()
2119 .map(|c| match c {
2120 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2121 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2122 _ => '_',
2123 })
2124 .collect::<String>();
2125
2126 result = result.trim_matches('_').to_string();
2128
2129 if result.is_empty() {
2131 result = "value".to_string();
2132 }
2133
2134 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2136 result = format!("variant_{result}");
2137 }
2138
2139 match result.as_str() {
2141 "null" => "null_value".to_string(),
2142 "true" => "true_value".to_string(),
2143 "false" => "false_value".to_string(),
2144 "type" => "type_".to_string(),
2145 "match" => "match_".to_string(),
2146 "fn" => "fn_".to_string(),
2147 "impl" => "impl_".to_string(),
2148 "trait" => "trait_".to_string(),
2149 "struct" => "struct_".to_string(),
2150 "enum" => "enum_".to_string(),
2151 "mod" => "mod_".to_string(),
2152 "use" => "use_".to_string(),
2153 "pub" => "pub_".to_string(),
2154 "const" => "const_".to_string(),
2155 "static" => "static_".to_string(),
2156 "let" => "let_".to_string(),
2157 "mut" => "mut_".to_string(),
2158 "ref" => "ref_".to_string(),
2159 "move" => "move_".to_string(),
2160 "return" => "return_".to_string(),
2161 "if" => "if_".to_string(),
2162 "else" => "else_".to_string(),
2163 "while" => "while_".to_string(),
2164 "for" => "for_".to_string(),
2165 "loop" => "loop_".to_string(),
2166 "break" => "break_".to_string(),
2167 "continue" => "continue_".to_string(),
2168 "self" => "self_".to_string(),
2169 "super" => "super_".to_string(),
2170 "crate" => "crate_".to_string(),
2171 "async" => "async_".to_string(),
2172 "await" => "await_".to_string(),
2173 "override" => "override_".to_string(),
2175 "box" => "box_".to_string(),
2176 "dyn" => "dyn_".to_string(),
2177 "where" => "where_".to_string(),
2178 "in" => "in_".to_string(),
2179 "abstract" => "abstract_".to_string(),
2181 "become" => "become_".to_string(),
2182 "do" => "do_".to_string(),
2183 "final" => "final_".to_string(),
2184 "macro" => "macro_".to_string(),
2185 "priv" => "priv_".to_string(),
2186 "try" => "try_".to_string(),
2187 "typeof" => "typeof_".to_string(),
2188 "unsized" => "unsized_".to_string(),
2189 "virtual" => "virtual_".to_string(),
2190 "yield" => "yield_".to_string(),
2191 _ => result,
2192 }
2193 }
2194
2195 fn generate_constraint_doc(
2203 &self,
2204 constraints: &crate::analysis::PropertyConstraints,
2205 ) -> TokenStream {
2206 use crate::type_mapping::ConstraintMode;
2207
2208 if constraints.is_empty() {
2209 return TokenStream::new();
2210 }
2211 match self.config.types.constraint_mode() {
2212 ConstraintMode::Off => TokenStream::new(),
2213 ConstraintMode::Doc => {
2214 let formatted = format_constraints_doc(constraints);
2215 quote! { #[doc = #formatted] }
2216 }
2217 }
2218 }
2219
2220 fn sanitize_doc_comment(&self, desc: &str) -> String {
2221 let mut result = desc.to_string();
2223
2224 if result.contains('\n')
2232 && (result.contains('{')
2233 || result.contains("```")
2234 || result.contains("Human:")
2235 || result.contains("Assistant:")
2236 || result
2237 .lines()
2238 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2239 {
2240 if result.contains("```") {
2242 result = result.replace("```", "```ignore");
2243 } else {
2244 if result.lines().any(|line| {
2246 let trimmed = line.trim();
2247 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2248 }) {
2249 result = format!("```ignore\n{result}\n```");
2250 }
2251 }
2252 }
2253
2254 result
2255 }
2256
2257 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2258 let mut result = String::new();
2260 let mut next_upper = true;
2261 let mut prev_was_lower = false;
2262
2263 for c in s.chars() {
2264 match c {
2265 'a'..='z' => {
2266 if next_upper {
2267 result.push(c.to_ascii_uppercase());
2268 next_upper = false;
2269 } else {
2270 result.push(c);
2271 }
2272 prev_was_lower = true;
2273 }
2274 'A'..='Z' => {
2275 result.push(c);
2276 next_upper = false;
2277 prev_was_lower = false;
2278 }
2279 '0'..='9' => {
2280 if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2283 }
2285 result.push(c);
2286 next_upper = false;
2287 prev_was_lower = false;
2288 }
2289 '_' | '-' | '.' | ' ' => {
2290 next_upper = true;
2292 prev_was_lower = false;
2293 }
2294 _ => {
2295 next_upper = true;
2297 prev_was_lower = false;
2298 }
2299 }
2300 }
2301
2302 if result.is_empty() {
2304 result = "Type".to_string();
2305 }
2306
2307 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2309 result = format!("Type{result}");
2310 }
2311
2312 if matches!(
2319 result.as_str(),
2320 "Result"
2321 | "Option"
2322 | "Box"
2323 | "Vec"
2324 | "String"
2325 | "Some"
2326 | "None"
2327 | "Ok"
2328 | "Err"
2329 | "Default"
2330 | "Clone"
2331 | "Debug"
2332 | "Send"
2333 | "Sync"
2334 | "Sized"
2335 | "Iterator"
2336 | "From"
2337 | "Into"
2338 | "TryFrom"
2339 | "TryInto"
2340 | "AsRef"
2341 | "AsMut"
2342 ) {
2343 result.push_str("Type");
2344 }
2345
2346 result
2347 }
2348
2349 fn to_rust_field_name(&self, s: &str) -> String {
2350 let leading_marker = match s.chars().next() {
2354 Some('-') if s.len() > 1 => "neg_",
2355 Some('+') if s.len() > 1 => "pos_",
2356 _ => "",
2357 };
2358
2359 let mut result = String::new();
2361 let mut prev_was_upper = false;
2362 let mut prev_was_underscore = false;
2363
2364 for (i, c) in s.chars().enumerate() {
2365 match c {
2366 'A'..='Z' => {
2367 if i > 0 && !prev_was_upper && !prev_was_underscore {
2369 result.push('_');
2370 }
2371 result.push(c.to_ascii_lowercase());
2372 prev_was_upper = true;
2373 prev_was_underscore = false;
2374 }
2375 'a'..='z' | '0'..='9' => {
2376 result.push(c);
2377 prev_was_upper = false;
2378 prev_was_underscore = false;
2379 }
2380 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
2381 if !prev_was_underscore && !result.is_empty() {
2382 result.push('_');
2383 prev_was_underscore = true;
2384 }
2385 prev_was_upper = false;
2386 }
2387 _ => {
2388 if !prev_was_underscore && !result.is_empty() {
2390 result.push('_');
2391 }
2392 prev_was_upper = false;
2393 prev_was_underscore = true;
2394 }
2395 }
2396 }
2397
2398 let mut result = result.trim_matches('_').to_string();
2400 if result.is_empty() {
2401 return "field".to_string();
2402 }
2403
2404 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2406 result = format!("field_{leading_marker}{result}");
2407 } else if !leading_marker.is_empty() {
2408 result = format!("{leading_marker}{result}");
2409 }
2410
2411 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
2415 return format!("{result}_field");
2416 }
2417 if Self::is_rust_keyword(&result) {
2419 format!("r#{result}")
2420 } else {
2421 result
2422 }
2423 }
2424
2425 pub fn is_rust_keyword(s: &str) -> bool {
2427 matches!(
2428 s,
2429 "type"
2430 | "match"
2431 | "fn"
2432 | "struct"
2433 | "enum"
2434 | "impl"
2435 | "trait"
2436 | "mod"
2437 | "use"
2438 | "pub"
2439 | "const"
2440 | "static"
2441 | "let"
2442 | "mut"
2443 | "ref"
2444 | "move"
2445 | "return"
2446 | "if"
2447 | "else"
2448 | "while"
2449 | "for"
2450 | "loop"
2451 | "break"
2452 | "continue"
2453 | "self"
2454 | "super"
2455 | "crate"
2456 | "async"
2457 | "await"
2458 | "override"
2459 | "box"
2460 | "dyn"
2461 | "where"
2462 | "in"
2463 | "abstract"
2464 | "become"
2465 | "do"
2466 | "final"
2467 | "macro"
2468 | "priv"
2469 | "try"
2470 | "typeof"
2471 | "unsized"
2472 | "virtual"
2473 | "yield"
2474 | "gen"
2476 )
2477 }
2478
2479 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
2481 if let Some(raw) = name.strip_prefix("r#") {
2482 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
2483 } else {
2484 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
2485 }
2486 }
2487
2488 fn to_camel_case(&self, s: &str) -> String {
2489 let mut result = String::new();
2491 let mut capitalize_next = false;
2492
2493 for (i, c) in s.chars().enumerate() {
2494 match c {
2495 '_' | '-' | '.' | ' ' => {
2496 capitalize_next = true;
2498 }
2499 'A'..='Z' => {
2500 if i == 0 {
2501 result.push(c.to_ascii_lowercase());
2503 } else if capitalize_next {
2504 result.push(c);
2505 capitalize_next = false;
2506 } else {
2507 result.push(c.to_ascii_lowercase());
2508 }
2509 }
2510 'a'..='z' | '0'..='9' => {
2511 if capitalize_next {
2512 result.push(c.to_ascii_uppercase());
2513 capitalize_next = false;
2514 } else {
2515 result.push(c);
2516 }
2517 }
2518 _ => {
2519 capitalize_next = true;
2521 }
2522 }
2523 }
2524
2525 if result.is_empty() {
2526 return "field".to_string();
2527 }
2528
2529 result
2530 }
2531
2532 fn generate_composition_struct(
2533 &self,
2534 schema: &crate::analysis::AnalyzedSchema,
2535 schemas: &[crate::analysis::SchemaRef],
2536 ) -> Result<TokenStream> {
2537 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2538
2539 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
2545 let field_name = format_ident!("part_{}", i);
2546 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
2547
2548 quote! {
2549 #[serde(flatten)]
2550 pub #field_name: #field_type,
2551 }
2552 });
2553
2554 let doc_comment = if let Some(desc) = &schema.description {
2555 quote! { #[doc = #desc] }
2556 } else {
2557 TokenStream::new()
2558 };
2559
2560 let derives = if self.config.enable_specta {
2562 quote! {
2563 #[derive(Debug, Clone, Deserialize, Serialize)]
2564 #[cfg_attr(feature = "specta", derive(specta::Type))]
2565 }
2566 } else {
2567 quote! {
2568 #[derive(Debug, Clone, Deserialize, Serialize)]
2569 }
2570 };
2571
2572 Ok(quote! {
2573 #doc_comment
2574 #derives
2575 pub struct #struct_name {
2576 #(#fields)*
2577 }
2578 })
2579 }
2580
2581 #[allow(dead_code)]
2582 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
2583 let mut missing = std::collections::HashSet::new();
2584 let defined_types: std::collections::HashSet<String> =
2585 analysis.schemas.keys().cloned().collect();
2586
2587 for schema in analysis.schemas.values() {
2589 match &schema.schema_type {
2590 crate::analysis::SchemaType::Union { variants } => {
2591 for variant in variants {
2592 if !defined_types.contains(&variant.target) {
2593 missing.insert(variant.target.clone());
2594 }
2595 }
2596 }
2597 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
2598 for variant in variants {
2599 if !defined_types.contains(&variant.type_name) {
2600 missing.insert(variant.type_name.clone());
2601 }
2602 }
2603 }
2604 crate::analysis::SchemaType::Object { properties, .. } => {
2605 let mut sorted_props: Vec<_> = properties.iter().collect();
2607 sorted_props.sort_by_key(|(name, _)| name.as_str());
2608 for (_, prop) in sorted_props {
2609 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
2610 {
2611 if !defined_types.contains(target) {
2612 missing.insert(target.clone());
2613 }
2614 }
2615 }
2616 }
2617 crate::analysis::SchemaType::Reference { target }
2618 if !defined_types.contains(target) =>
2619 {
2620 missing.insert(target.clone());
2621 }
2622 _ => {}
2623 }
2624 }
2625
2626 missing
2627 }
2628
2629 #[allow(clippy::only_used_in_recursion)]
2630 fn generate_array_item_type(
2631 &self,
2632 item_type: &crate::analysis::SchemaType,
2633 analysis: &crate::analysis::SchemaAnalysis,
2634 ) -> TokenStream {
2635 use crate::analysis::SchemaType;
2636
2637 match item_type {
2638 SchemaType::Primitive { rust_type, .. } => {
2639 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
2644 quote! { #parsed }
2645 } else if rust_type.contains("::") {
2646 let parts: Vec<_> = rust_type
2647 .split("::")
2648 .map(|p| format_ident!("{}", p))
2649 .collect();
2650 quote! { #(#parts)::* }
2651 } else {
2652 let type_ident = format_ident!("{}", rust_type);
2653 quote! { #type_ident }
2654 }
2655 }
2656 SchemaType::Reference { target } => {
2657 let target_type = format_ident!("{}", self.to_rust_type_name(target));
2658 if analysis.dependencies.recursive_schemas.contains(target) {
2660 quote! { Box<#target_type> }
2661 } else {
2662 quote! { #target_type }
2663 }
2664 }
2665 SchemaType::Array { item_type } => {
2666 let inner_type = self.generate_array_item_type(item_type, analysis);
2668 quote! { Vec<#inner_type> }
2669 }
2670 _ => {
2671 quote! { serde_json::Value }
2673 }
2674 }
2675 }
2676
2677 fn type_name_to_variant_name(&self, type_name: &str) -> String {
2679 match type_name {
2681 "bool" => return "Boolean".to_string(),
2682 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
2683 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
2684 "f32" | "f64" => return "Number".to_string(),
2685 "String" => return "String".to_string(),
2686 "serde_json::Value" => return "Value".to_string(),
2687 "bytes::Bytes" => return "Binary".to_string(),
2691 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
2692 "chrono::NaiveDate" => return "Date".to_string(),
2693 "chrono::NaiveTime" => return "Time".to_string(),
2694 "uuid::Uuid" => return "Uuid".to_string(),
2695 "url::Url" => return "Url".to_string(),
2696 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
2697 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
2698 _ => {}
2699 }
2700
2701 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
2703 let inner = &type_name[4..type_name.len() - 1];
2704 if inner.starts_with("Vec<") && inner.ends_with(">") {
2706 let inner_inner = &inner[4..inner.len() - 1];
2707 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
2708 }
2709 return format!("{}Array", self.type_name_to_variant_name(inner));
2710 }
2711
2712 let clean_name = type_name
2718 .trim_end_matches("Type")
2719 .trim_end_matches("Schema")
2720 .trim_end_matches("Item");
2721
2722 self.to_rust_type_name(clean_name)
2724 }
2725
2726 fn ensure_unique_variant_name_generator(
2728 &self,
2729 base_name: String,
2730 used_names: &mut std::collections::HashSet<String>,
2731 fallback_index: usize,
2732 ) -> String {
2733 if used_names.insert(base_name.clone()) {
2734 return base_name;
2735 }
2736
2737 for i in 2..100 {
2739 let numbered_name = format!("{base_name}{i}");
2740 if used_names.insert(numbered_name.clone()) {
2741 return numbered_name;
2742 }
2743 }
2744
2745 let fallback = format!("Variant{fallback_index}");
2747 used_names.insert(fallback.clone());
2748 fallback
2749 }
2750
2751 fn find_request_type_for_operation(
2753 &self,
2754 operation_id: &str,
2755 analysis: &SchemaAnalysis,
2756 ) -> Option<String> {
2757 analysis.operations.get(operation_id).and_then(|op| {
2759 op.request_body
2760 .as_ref()
2761 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
2762 })
2763 }
2764
2765 fn resolve_streaming_event_type(
2767 &self,
2768 endpoint: &crate::streaming::StreamingEndpoint,
2769 analysis: &SchemaAnalysis,
2770 ) -> Result<String> {
2771 match &endpoint.event_flow {
2772 crate::streaming::EventFlow::Simple => {
2773 if analysis.schemas.contains_key(&endpoint.event_union_type) {
2776 Ok(endpoint.event_union_type.to_string())
2777 } else {
2778 Err(crate::error::GeneratorError::ValidationError(format!(
2779 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
2780 endpoint.event_union_type, endpoint.operation_id
2781 )))
2782 }
2783 }
2784 crate::streaming::EventFlow::StartDeltaStop { .. } => {
2785 if analysis.schemas.contains_key(&endpoint.event_union_type) {
2788 Ok(endpoint.event_union_type.to_string())
2789 } else {
2790 Err(crate::error::GeneratorError::ValidationError(format!(
2791 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
2792 endpoint.event_union_type, endpoint.operation_id
2793 )))
2794 }
2795 }
2796 }
2797 }
2798
2799 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
2801 Ok(quote! {
2802 #[derive(Debug, thiserror::Error)]
2804 pub enum StreamingError {
2805 #[error("Connection error: {0}")]
2806 Connection(String),
2807 #[error("HTTP error: {status}")]
2808 Http { status: u16 },
2809 #[error("SSE parsing error: {0}")]
2810 Parsing(String),
2811 #[error("Authentication error: {0}")]
2812 Authentication(String),
2813 #[error("Rate limit error: {0}")]
2814 RateLimit(String),
2815 #[error("API error: {0}")]
2816 Api(String),
2817 #[error("Timeout error: {0}")]
2818 Timeout(String),
2819 #[error("JSON serialization/deserialization error: {0}")]
2820 Json(#[from] serde_json::Error),
2821 #[error("Request error: {0}")]
2822 Request(reqwest::Error),
2823 }
2824
2825 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
2826 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
2827 StreamingError::Api(format!("Invalid header value: {}", err))
2828 }
2829 }
2830
2831 impl From<reqwest::Error> for StreamingError {
2832 fn from(err: reqwest::Error) -> Self {
2833 if err.is_timeout() {
2834 StreamingError::Timeout(err.to_string())
2835 } else if err.is_status() {
2836 if let Some(status) = err.status() {
2837 StreamingError::Http { status: status.as_u16() }
2838 } else {
2839 StreamingError::Connection(err.to_string())
2840 }
2841 } else {
2842 StreamingError::Request(err)
2843 }
2844 }
2845 }
2846 })
2847 }
2848
2849 fn generate_endpoint_trait(
2851 &self,
2852 endpoint: &crate::streaming::StreamingEndpoint,
2853 analysis: &SchemaAnalysis,
2854 ) -> Result<TokenStream> {
2855 use crate::streaming::HttpMethod;
2856
2857 let trait_name = format_ident!(
2858 "{}StreamingClient",
2859 self.to_rust_type_name(&endpoint.operation_id)
2860 );
2861 let method_name =
2862 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
2863 let event_type =
2864 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
2865
2866 let method_signature = match endpoint.http_method {
2868 HttpMethod::Get => {
2869 let mut param_defs = Vec::new();
2871 for qp in &endpoint.query_parameters {
2872 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
2873 if qp.required {
2874 param_defs.push(quote! { #param_name: &str });
2875 } else {
2876 param_defs.push(quote! { #param_name: Option<&str> });
2877 }
2878 }
2879 quote! {
2880 async fn #method_name(
2881 &self,
2882 #(#param_defs),*
2883 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2884 }
2885 }
2886 HttpMethod::Post => {
2887 let request_type = self
2889 .find_request_type_for_operation(&endpoint.operation_id, analysis)
2890 .unwrap_or_else(|| "serde_json::Value".to_string());
2891 let request_type_ident = if request_type.contains("::") {
2892 let parts: Vec<&str> = request_type.split("::").collect();
2893 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
2894 quote! { #(#path_parts)::* }
2895 } else {
2896 let ident = format_ident!("{}", request_type);
2897 quote! { #ident }
2898 };
2899 quote! {
2900 async fn #method_name(
2901 &self,
2902 request: #request_type_ident,
2903 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2904 }
2905 }
2906 };
2907
2908 Ok(quote! {
2909 #[async_trait]
2911 pub trait #trait_name {
2912 type Error: std::error::Error + Send + Sync + 'static;
2913
2914 #method_signature
2916 }
2917 })
2918 }
2919
2920 fn generate_streaming_client_impl(
2922 &self,
2923 streaming_config: &crate::streaming::StreamingConfig,
2924 analysis: &SchemaAnalysis,
2925 ) -> Result<TokenStream> {
2926 let client_name = format_ident!(
2927 "{}Client",
2928 self.to_rust_type_name(&streaming_config.client_module_name)
2929 );
2930
2931 let mut struct_fields = vec![
2934 quote! { base_url: String },
2935 quote! { api_key: Option<String> },
2936 quote! { http_client: reqwest::Client },
2937 quote! { custom_headers: std::collections::BTreeMap<String, String> },
2938 ];
2939
2940 let has_optional_headers = !streaming_config
2941 .endpoints
2942 .iter()
2943 .all(|e| e.optional_headers.is_empty());
2944
2945 if has_optional_headers {
2946 struct_fields
2947 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
2948 }
2949
2950 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
2953 streaming_config
2954 .endpoints
2955 .first()
2956 .and_then(|e| e.base_url.as_deref())
2957 .unwrap_or("https://api.example.com")
2958 } else {
2959 "https://api.example.com"
2960 };
2961
2962 let constructor_fields = if has_optional_headers {
2964 quote! {
2965 base_url: #default_base_url.to_string(),
2966 api_key: None,
2967 http_client: reqwest::Client::new(),
2968 custom_headers: std::collections::BTreeMap::new(),
2969 optional_headers: std::collections::BTreeMap::new(),
2970 }
2971 } else {
2972 quote! {
2973 base_url: #default_base_url.to_string(),
2974 api_key: None,
2975 http_client: reqwest::Client::new(),
2976 custom_headers: std::collections::BTreeMap::new(),
2977 }
2978 };
2979
2980 let optional_headers_method = if has_optional_headers {
2982 quote! {
2983 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
2985 self.optional_headers = headers;
2986 }
2987 }
2988 } else {
2989 TokenStream::new()
2990 };
2991
2992 let constructor = quote! {
2993 impl #client_name {
2994 pub fn new() -> Self {
2996 Self {
2997 #constructor_fields
2998 }
2999 }
3000
3001 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
3003 self.base_url = base_url.into();
3004 self
3005 }
3006
3007 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
3009 self.api_key = Some(api_key.into());
3010 self
3011 }
3012
3013 pub fn with_header(
3015 mut self,
3016 name: impl Into<String>,
3017 value: impl Into<String>,
3018 ) -> Self {
3019 self.custom_headers.insert(name.into(), value.into());
3020 self
3021 }
3022
3023 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
3025 self.http_client = client;
3026 self
3027 }
3028
3029 #optional_headers_method
3030 }
3031 };
3032
3033 let mut trait_impls = Vec::new();
3035 for endpoint in &streaming_config.endpoints {
3036 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
3037 trait_impls.push(trait_impl);
3038 }
3039
3040 let default_impl = quote! {
3042 impl Default for #client_name {
3043 fn default() -> Self {
3044 Self::new()
3045 }
3046 }
3047 };
3048
3049 Ok(quote! {
3050 #[derive(Debug, Clone)]
3052 pub struct #client_name {
3053 #(#struct_fields,)*
3054 }
3055
3056 #constructor
3057
3058 #default_impl
3059
3060 #(#trait_impls)*
3061 })
3062 }
3063
3064 fn generate_endpoint_trait_impl(
3066 &self,
3067 endpoint: &crate::streaming::StreamingEndpoint,
3068 client_name: &proc_macro2::Ident,
3069 analysis: &SchemaAnalysis,
3070 ) -> Result<TokenStream> {
3071 use crate::streaming::HttpMethod;
3072
3073 let trait_name = format_ident!(
3074 "{}StreamingClient",
3075 self.to_rust_type_name(&endpoint.operation_id)
3076 );
3077 let method_name =
3078 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3079 let event_type =
3080 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3081
3082 let mut header_setup = Vec::new();
3084 for (name, value) in &endpoint.required_headers {
3085 header_setup.push(quote! {
3086 headers.insert(#name, HeaderValue::from_static(#value));
3087 });
3088 }
3089
3090 if let Some(auth_header) = &endpoint.auth_header {
3093 match auth_header {
3094 crate::streaming::AuthHeader::Bearer(header_name) => {
3095 header_setup.push(quote! {
3096 if let Some(ref api_key) = self.api_key {
3097 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3098 }
3099 });
3100 }
3101 crate::streaming::AuthHeader::ApiKey(header_name) => {
3102 header_setup.push(quote! {
3103 if let Some(ref api_key) = self.api_key {
3104 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3105 }
3106 });
3107 }
3108 }
3109 } else {
3110 header_setup.push(quote! {
3112 if let Some(ref api_key) = self.api_key {
3113 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3114 }
3115 });
3116 }
3117
3118 header_setup.push(quote! {
3120 for (name, value) in &self.custom_headers {
3121 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3122 headers.insert(header_name, header_value);
3123 }
3124 }
3125 });
3126
3127 if !endpoint.optional_headers.is_empty() {
3129 header_setup.push(quote! {
3130 for (key, value) in &self.optional_headers {
3131 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3132 headers.insert(header_name, header_value);
3133 }
3134 }
3135 });
3136 }
3137
3138 match endpoint.http_method {
3140 HttpMethod::Get => self.generate_get_streaming_impl(
3141 endpoint,
3142 client_name,
3143 &trait_name,
3144 &method_name,
3145 &event_type,
3146 &header_setup,
3147 ),
3148 HttpMethod::Post => self.generate_post_streaming_impl(
3149 endpoint,
3150 client_name,
3151 &trait_name,
3152 &method_name,
3153 &event_type,
3154 &header_setup,
3155 analysis,
3156 ),
3157 }
3158 }
3159
3160 fn generate_get_streaming_impl(
3162 &self,
3163 endpoint: &crate::streaming::StreamingEndpoint,
3164 client_name: &proc_macro2::Ident,
3165 trait_name: &proc_macro2::Ident,
3166 method_name: &proc_macro2::Ident,
3167 event_type: &proc_macro2::Ident,
3168 header_setup: &[TokenStream],
3169 ) -> Result<TokenStream> {
3170 let path = &endpoint.path;
3171
3172 let mut param_defs = Vec::new();
3174 let mut query_params = Vec::new();
3175
3176 for qp in &endpoint.query_parameters {
3177 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3178 let param_name_str = &qp.name;
3179
3180 if qp.required {
3181 param_defs.push(quote! { #param_name: &str });
3182 query_params.push(quote! {
3183 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3184 });
3185 } else {
3186 param_defs.push(quote! { #param_name: Option<&str> });
3187 query_params.push(quote! {
3188 if let Some(v) = #param_name {
3189 url.query_pairs_mut().append_pair(#param_name_str, v);
3190 }
3191 });
3192 }
3193 }
3194
3195 let url_construction = quote! {
3197 let base_url = url::Url::parse(&self.base_url)
3198 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3199 let path_to_join = #path.trim_start_matches('/');
3200 let mut url = base_url.join(path_to_join)
3201 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3202 #(#query_params)*
3203 };
3204
3205 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3206
3207 Ok(quote! {
3208 #[async_trait]
3209 impl #trait_name for #client_name {
3210 type Error = StreamingError;
3211
3212 #instrument_skip
3213 async fn #method_name(
3214 &self,
3215 #(#param_defs),*
3216 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3217 debug!("Starting streaming GET request");
3218
3219 let mut headers = HeaderMap::new();
3220 #(#header_setup)*
3221
3222 #url_construction
3223 let url_str = url.to_string();
3224 debug!("Making streaming GET request to: {}", url_str);
3225
3226 let request_builder = self.http_client
3227 .get(url_str)
3228 .headers(headers);
3229
3230 debug!("Creating SSE stream from request");
3231 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3232 info!("SSE stream created successfully");
3233 Ok(Box::pin(stream))
3234 }
3235 }
3236 })
3237 }
3238
3239 #[allow(clippy::too_many_arguments)]
3241 fn generate_post_streaming_impl(
3242 &self,
3243 endpoint: &crate::streaming::StreamingEndpoint,
3244 client_name: &proc_macro2::Ident,
3245 trait_name: &proc_macro2::Ident,
3246 method_name: &proc_macro2::Ident,
3247 event_type: &proc_macro2::Ident,
3248 header_setup: &[TokenStream],
3249 analysis: &SchemaAnalysis,
3250 ) -> Result<TokenStream> {
3251 let path = &endpoint.path;
3252
3253 let request_type = self
3255 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3256 .unwrap_or_else(|| "serde_json::Value".to_string());
3257 let request_type_ident = if request_type.contains("::") {
3258 let parts: Vec<&str> = request_type.split("::").collect();
3259 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3260 quote! { #(#path_parts)::* }
3261 } else {
3262 let ident = format_ident!("{}", request_type);
3263 quote! { #ident }
3264 };
3265
3266 let url_construction = quote! {
3268 let base_url = url::Url::parse(&self.base_url)
3269 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3270 let path_to_join = #path.trim_start_matches('/');
3271 let url = base_url.join(path_to_join)
3272 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3273 .to_string();
3274 };
3275
3276 let stream_param = &endpoint.stream_parameter;
3278 let stream_setup = if stream_param.is_empty() {
3279 quote! {
3280 let streaming_request = request;
3281 }
3282 } else {
3283 quote! {
3284 let mut streaming_request = request;
3286 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3287 if let Some(obj) = request_value.as_object_mut() {
3288 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3289 }
3290 streaming_request = serde_json::from_value(request_value)?;
3291 }
3292 }
3293 };
3294
3295 Ok(quote! {
3296 #[async_trait]
3297 impl #trait_name for #client_name {
3298 type Error = StreamingError;
3299
3300 #[instrument(skip(self, request), name = "streaming_post_request")]
3301 async fn #method_name(
3302 &self,
3303 request: #request_type_ident,
3304 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3305 debug!("Starting streaming POST request");
3306
3307 #stream_setup
3308
3309 let mut headers = HeaderMap::new();
3310 #(#header_setup)*
3311
3312 #url_construction
3313 debug!("Making streaming POST request to: {}", url);
3314
3315 let request_builder = self.http_client
3316 .post(&url)
3317 .headers(headers)
3318 .json(&streaming_request);
3319
3320 debug!("Creating SSE stream from request");
3321 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3322 info!("SSE stream created successfully");
3323 Ok(Box::pin(stream))
3324 }
3325 }
3326 })
3327 }
3328
3329 fn generate_sse_parser_utilities(
3331 &self,
3332 _streaming_config: &crate::streaming::StreamingConfig,
3333 ) -> Result<TokenStream> {
3334 Ok(quote! {
3335 pub async fn parse_sse_stream<T>(
3337 request_builder: reqwest::RequestBuilder
3338 ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
3339 where
3340 T: serde::de::DeserializeOwned + Send + 'static,
3341 {
3342 let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
3343 StreamingError::Connection(format!("Failed to create event source: {}", e))
3344 })?;
3345
3346 let stream = event_source.filter_map(|event_result| async move {
3347 match event_result {
3348 Ok(reqwest_eventsource::Event::Open) => {
3349 debug!("SSE connection opened");
3350 None
3351 }
3352 Ok(reqwest_eventsource::Event::Message(message)) => {
3353 if message.event == "ping" {
3355 debug!("Received SSE ping event, skipping");
3356 return None;
3357 }
3358
3359 if message.data.trim().is_empty() {
3361 debug!("Empty SSE data, skipping");
3362 return None;
3363 }
3364
3365 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
3367 if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
3368 if event_type == "ping" {
3369 debug!("Received ping event in JSON data, skipping");
3370 return None;
3371 }
3372 }
3373
3374 match serde_json::from_value::<T>(json_value) {
3376 Ok(parsed_event) => {
3377 Some(Ok(parsed_event))
3378 }
3379 Err(e) => {
3380 if message.data.contains("ping") || message.event.contains("ping") {
3381 debug!("Ignoring ping-related event: {}", message.data);
3382 None
3383 } else {
3384 Some(Err(StreamingError::Parsing(
3385 format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
3386 )))
3387 }
3388 }
3389 }
3390 } else {
3391 Some(Err(StreamingError::Parsing(
3393 format!("SSE event is not valid JSON: {}", message.data)
3394 )))
3395 }
3396 }
3397 Err(e) => {
3398 match e {
3400 reqwest_eventsource::Error::StreamEnded => {
3401 debug!("SSE stream completed normally");
3402 None }
3404 reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
3405 let status_code = status.as_u16();
3407
3408 let error_body = match response.text().await {
3410 Ok(body) => body,
3411 Err(_) => "Failed to read error response body".to_string()
3412 };
3413
3414 error!("SSE connection error - HTTP {}: {}", status_code, error_body);
3415
3416 let detailed_error = format!(
3417 "HTTP {} error: {}",
3418 status_code,
3419 error_body
3420 );
3421
3422 Some(Err(StreamingError::Connection(detailed_error)))
3423 }
3424 _ => {
3425 let error_str = e.to_string();
3426 if error_str.contains("stream closed") {
3427 debug!("SSE stream closed");
3428 None
3429 } else {
3430 error!("SSE connection error: {}", e);
3431 Some(Err(StreamingError::Connection(error_str)))
3432 }
3433 }
3434 }
3435 }
3436 }
3437 });
3438
3439 Ok(stream)
3440 }
3441 })
3442 }
3443
3444 fn generate_reconnection_utilities(
3446 &self,
3447 reconnect_config: &crate::streaming::ReconnectionConfig,
3448 ) -> Result<TokenStream> {
3449 let max_retries = reconnect_config.max_retries;
3450 let initial_delay = reconnect_config.initial_delay_ms;
3451 let max_delay = reconnect_config.max_delay_ms;
3452 let backoff_multiplier = reconnect_config.backoff_multiplier;
3453
3454 Ok(quote! {
3455 #[derive(Debug, Clone)]
3457 pub struct ReconnectionManager {
3458 max_retries: u32,
3459 initial_delay_ms: u64,
3460 max_delay_ms: u64,
3461 backoff_multiplier: f64,
3462 current_attempt: u32,
3463 }
3464
3465 impl ReconnectionManager {
3466 pub fn new() -> Self {
3468 Self {
3469 max_retries: #max_retries,
3470 initial_delay_ms: #initial_delay,
3471 max_delay_ms: #max_delay,
3472 backoff_multiplier: #backoff_multiplier,
3473 current_attempt: 0,
3474 }
3475 }
3476
3477 pub fn should_retry(&self) -> bool {
3479 self.current_attempt < self.max_retries
3480 }
3481
3482 pub fn next_retry_delay(&mut self) -> Duration {
3484 if !self.should_retry() {
3485 return Duration::from_secs(0);
3486 }
3487
3488 let delay_ms = (self.initial_delay_ms as f64
3489 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
3490 let delay_ms = delay_ms.min(self.max_delay_ms);
3491
3492 self.current_attempt += 1;
3493 Duration::from_millis(delay_ms)
3494 }
3495
3496 pub fn reset(&mut self) {
3498 self.current_attempt = 0;
3499 }
3500
3501 pub fn current_attempt(&self) -> u32 {
3503 self.current_attempt
3504 }
3505 }
3506
3507 impl Default for ReconnectionManager {
3508 fn default() -> Self {
3509 Self::new()
3510 }
3511 }
3512 })
3513 }
3514}