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 generated = quote! {
465 #![allow(clippy::large_enum_variant)]
471 #![allow(clippy::format_in_format_args)]
472 #![allow(clippy::let_unit_value)]
473 #![allow(unreachable_patterns)]
474
475 use serde::{Deserialize, Serialize};
476
477 #base64_helper
478
479 #type_definitions
480 };
481
482 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
484 GeneratorError::CodeGenError(format!("Failed to parse generated code: {e}"))
485 })?;
486
487 let formatted = prettyplease::unparse(&syntax_tree);
488
489 Ok(formatted)
490 }
491
492 fn generate_streaming_client(
494 &self,
495 streaming_config: &StreamingConfig,
496 analysis: &SchemaAnalysis,
497 ) -> Result<String> {
498 let mut client_code = TokenStream::new();
499
500 let imports = quote! {
502 #![allow(clippy::format_in_format_args)]
507 #![allow(clippy::let_unit_value)]
508 #![allow(unused_mut)]
509
510 use super::types::*;
511 use async_trait::async_trait;
512 use futures_util::{Stream, StreamExt};
513 use std::pin::Pin;
514 use std::time::Duration;
515 use reqwest::header::{HeaderMap, HeaderValue};
516 use tracing::{debug, error, info, warn, instrument};
517 };
518 client_code.extend(imports);
519
520 if streaming_config.generate_client {
522 let error_types = self.generate_streaming_error_types()?;
523 client_code.extend(error_types);
524 }
525
526 for endpoint in &streaming_config.endpoints {
528 let trait_code = self.generate_endpoint_trait(endpoint, analysis)?;
529 client_code.extend(trait_code);
530 }
531
532 if streaming_config.generate_client {
534 let client_impl = self.generate_streaming_client_impl(streaming_config, analysis)?;
535 client_code.extend(client_impl);
536 }
537
538 if streaming_config.event_parser_helpers {
540 let parser_code = self.generate_sse_parser_utilities(streaming_config)?;
541 client_code.extend(parser_code);
542 }
543
544 if let Some(reconnect_config) = &streaming_config.reconnection_config {
546 let reconnect_code = self.generate_reconnection_utilities(reconnect_config)?;
547 client_code.extend(reconnect_code);
548 }
549
550 let syntax_tree = syn::parse2::<syn::File>(client_code).map_err(|e| {
551 GeneratorError::CodeGenError(format!("Failed to parse streaming client code: {e}"))
552 })?;
553
554 Ok(prettyplease::unparse(&syntax_tree))
555 }
556
557 pub fn generate_http_client(&self, analysis: &SchemaAnalysis) -> Result<String> {
559 let error_types = self.generate_http_error_types();
560 let client_struct = self.generate_http_client_struct();
561 let operation_methods = self.generate_operation_methods(analysis);
562
563 let generated = quote! {
564 #![allow(clippy::format_in_format_args)]
569 #![allow(clippy::let_unit_value)]
570
571 use super::types::*;
572
573 #error_types
574
575 #client_struct
576
577 #operation_methods
578 };
579
580 let syntax_tree = syn::parse2::<syn::File>(generated).map_err(|e| {
581 GeneratorError::CodeGenError(format!("Failed to parse HTTP client code: {e}"))
582 })?;
583
584 Ok(prettyplease::unparse(&syntax_tree))
585 }
586
587 fn generate_http_error_types(&self) -> TokenStream {
589 quote! {
590 use thiserror::Error;
591
592 #[derive(Error, Debug)]
600 pub enum HttpError {
601 #[error("Network error: {0}")]
603 Network(#[from] reqwest::Error),
604
605 #[error("Middleware error: {0}")]
607 Middleware(#[from] reqwest_middleware::Error),
608
609 #[error("Failed to serialize request: {0}")]
611 Serialization(String),
612
613 #[error("Authentication error: {0}")]
615 Auth(String),
616
617 #[error("Request timeout")]
619 Timeout,
620
621 #[error("Configuration error: {0}")]
623 Config(String),
624
625 #[error("{0}")]
627 Other(String),
628 }
629
630 impl HttpError {
631 pub fn serialization_error(error: impl std::fmt::Display) -> Self {
633 Self::Serialization(error.to_string())
634 }
635
636 pub fn is_retryable(&self) -> bool {
638 matches!(self, Self::Network(_) | Self::Middleware(_) | Self::Timeout)
639 }
640 }
641
642 #[derive(Debug, Clone)]
652 pub struct ApiError<E> {
653 pub status: u16,
654 pub headers: reqwest::header::HeaderMap,
655 pub body: String,
656 pub typed: Option<E>,
657 pub parse_error: Option<String>,
658 }
659
660 impl<E> ApiError<E> {
661 pub fn is_client_error(&self) -> bool {
662 (400..500).contains(&self.status)
663 }
664
665 pub fn is_server_error(&self) -> bool {
666 (500..600).contains(&self.status)
667 }
668
669 pub fn is_retryable(&self) -> bool {
672 matches!(self.status, 429 | 500 | 502 | 503 | 504)
673 }
674 }
675
676 impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
677 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 write!(f, "API error {}: {}", self.status, self.body)
679 }
680 }
681
682 impl<E: std::fmt::Debug> std::error::Error for ApiError<E> {}
683
684 #[derive(Debug, Error)]
692 pub enum ApiOpError<E: std::fmt::Debug> {
693 #[error(transparent)]
694 Transport(#[from] HttpError),
695
696 #[error(transparent)]
697 Api(ApiError<E>),
698 }
699
700 impl<E: std::fmt::Debug> ApiOpError<E> {
701 pub fn api(&self) -> Option<&ApiError<E>> {
703 match self {
704 Self::Api(e) => Some(e),
705 Self::Transport(_) => None,
706 }
707 }
708
709 pub fn is_api_error(&self) -> bool {
712 matches!(self, Self::Api(_))
713 }
714 }
715
716 impl<E: std::fmt::Debug> From<reqwest::Error> for ApiOpError<E> {
719 fn from(e: reqwest::Error) -> Self {
720 Self::Transport(HttpError::Network(e))
721 }
722 }
723
724 impl<E: std::fmt::Debug> From<reqwest_middleware::Error> for ApiOpError<E> {
725 fn from(e: reqwest_middleware::Error) -> Self {
726 Self::Transport(HttpError::Middleware(e))
727 }
728 }
729
730 pub type HttpResult<T> = Result<T, HttpError>;
734 }
735 }
736
737 fn generate_mod_file(&self, files: &[GeneratedFile]) -> Result<String> {
739 let mut module_declarations = Vec::new();
740 let mut pub_uses = Vec::new();
741
742 for file in files {
743 if let Some(module_name) = file.path.file_stem().and_then(|s| s.to_str()) {
744 if module_name != "mod" {
745 module_declarations.push(format!("pub mod {module_name};"));
746 pub_uses.push(format!("pub use {module_name}::*;"));
747 }
748 }
749 }
750
751 let mount_hint = format!(
758 "//! Configured `module_name` = `{name}`. Mount this tree under your\n\
759 //! preferred path, e.g. `pub mod {name};` in your crate root.\n",
760 name = self.config.module_name,
761 );
762
763 let content = format!(
764 r#"//! Generated API modules
765//!
766//! This module exports all generated API types and clients.
767//! Do not edit manually - regenerate using the appropriate script.
768//!
769{mount_hint}
770#![allow(unused_imports)]
771
772{decls}
773
774{uses}
775"#,
776 mount_hint = mount_hint,
777 decls = module_declarations.join("\n"),
778 uses = pub_uses.join("\n"),
779 );
780
781 Ok(content)
782 }
783
784 pub fn write_files(&self, result: &GenerationResult) -> Result<()> {
786 use std::fs;
787
788 fs::create_dir_all(&self.config.output_dir)?;
790
791 for file in &result.files {
793 let file_path = self.config.output_dir.join(&file.path);
794 fs::write(&file_path, &file.content)?;
795 }
796
797 let mod_path = self.config.output_dir.join(&result.mod_file.path);
799 fs::write(&mod_path, &result.mod_file.content)?;
800
801 if let Some(toml) = crate::type_mapping::render_required_deps_toml(&result.required_deps) {
807 let deps_path = self.config.output_dir.join("REQUIRED_DEPS.toml");
808 fs::write(&deps_path, toml)?;
809 }
810
811 Ok(())
812 }
813
814 fn generate_type_definition(
815 &self,
816 schema: &crate::analysis::AnalyzedSchema,
817 analysis: &crate::analysis::SchemaAnalysis,
818 discriminated_variant_info: &BTreeMap<String, DiscriminatedVariantInfo>,
819 ) -> Result<TokenStream> {
820 use crate::analysis::SchemaType;
821
822 match &schema.schema_type {
823 SchemaType::Primitive { rust_type, .. } => {
824 self.generate_type_alias(schema, rust_type)
826 }
827 SchemaType::StringEnum { values } => {
828 let ext = analysis.enum_extensions.get(&schema.name);
829 let rust_name = self.to_rust_type_name(&schema.name);
836 let force_extensible = self
837 .config
838 .extensible_enum_overrides
839 .get(&schema.name)
840 .or_else(|| self.config.extensible_enum_overrides.get(&rust_name))
841 .copied()
842 .unwrap_or(false);
843 if force_extensible {
844 self.generate_extensible_enum(schema, values, ext)
845 } else {
846 self.generate_string_enum(schema, values, ext)
847 }
848 }
849 SchemaType::ExtensibleEnum { known_values } => {
850 let ext = analysis.enum_extensions.get(&schema.name);
851 self.generate_extensible_enum(schema, known_values, ext)
852 }
853 SchemaType::Object {
854 properties,
855 required,
856 additional_properties,
857 } => self.generate_struct(
858 schema,
859 properties,
860 required,
861 additional_properties,
862 analysis,
863 discriminated_variant_info.get(&schema.name),
864 ),
865 SchemaType::DiscriminatedUnion {
866 discriminator_field,
867 variants,
868 } => {
869 if self.should_use_untagged_discriminated_union(schema, analysis) {
871 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
873 .iter()
874 .map(|v| crate::analysis::SchemaRef {
875 target: v.type_name.clone(),
876 nullable: false,
877 })
878 .collect();
879 self.generate_union_enum(schema, &schema_refs, analysis)
880 } else {
881 self.generate_discriminated_enum(
882 schema,
883 discriminator_field,
884 variants,
885 analysis,
886 )
887 }
888 }
889 SchemaType::Union { variants } => self.generate_union_enum(schema, variants, analysis),
890 SchemaType::Reference { target } => {
891 if schema.name != *target {
894 let alias_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
896 let target_type = format_ident!("{}", self.to_rust_type_name(target));
897
898 let doc_comment = if let Some(desc) = &schema.description {
899 quote! { #[doc = #desc] }
900 } else {
901 TokenStream::new()
902 };
903
904 Ok(quote! {
905 #doc_comment
906 pub type #alias_name = #target_type;
907 })
908 } else {
909 Ok(TokenStream::new())
911 }
912 }
913 SchemaType::Array { item_type } => {
914 let array_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
922
923 if let SchemaType::Reference { target } = item_type.as_ref() {
925 if let Some(info) = discriminated_variant_info.get(target) {
926 if !info.is_parent_untagged {
927 let wrapper_name =
929 format_ident!("{}Item", self.to_rust_type_name(&schema.name));
930 let variant_type = format_ident!("{}", self.to_rust_type_name(target));
931 let disc_field = &info.discriminator_field;
932 let disc_value = &info.discriminator_value;
933
934 let doc_comment = if let Some(desc) = &schema.description {
935 quote! { #[doc = #desc] }
936 } else {
937 TokenStream::new()
938 };
939
940 return Ok(quote! {
941 #[derive(Debug, Clone, Deserialize, Serialize)]
945 #[serde(tag = #disc_field)]
946 pub enum #wrapper_name {
947 #[serde(rename = #disc_value)]
948 #variant_type(#variant_type),
949 }
950 #doc_comment
951 pub type #array_name = Vec<#wrapper_name>;
952 });
953 }
954 }
955 }
956
957 let inner_type = self.generate_array_item_type(item_type, analysis);
958
959 let doc_comment = if let Some(desc) = &schema.description {
960 quote! { #[doc = #desc] }
961 } else {
962 TokenStream::new()
963 };
964
965 Ok(quote! {
966 #doc_comment
967 pub type #array_name = Vec<#inner_type>;
968 })
969 }
970 SchemaType::Composition { schemas } => {
971 self.generate_composition_struct(schema, schemas)
972 }
973 }
974 }
975
976 fn generate_type_alias(
977 &self,
978 schema: &crate::analysis::AnalyzedSchema,
979 rust_type: &str,
980 ) -> Result<TokenStream> {
981 let type_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
982 let base_type = parse_rust_type(rust_type)?;
986
987 let doc_comment = if let Some(desc) = &schema.description {
988 let sanitized_desc = self.sanitize_doc_comment(desc);
989 quote! { #[doc = #sanitized_desc] }
990 } else {
991 TokenStream::new()
992 };
993
994 Ok(quote! {
995 #doc_comment
996 pub type #type_name = #base_type;
997 })
998 }
999
1000 fn generate_extensible_enum(
1001 &self,
1002 schema: &crate::analysis::AnalyzedSchema,
1003 known_values: &[String],
1004 ext: Option<&crate::analysis::EnumExtensions>,
1005 ) -> Result<TokenStream> {
1006 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1007
1008 let doc_comment = if let Some(desc) = &schema.description {
1009 quote! { #[doc = #desc] }
1010 } else {
1011 TokenStream::new()
1012 };
1013
1014 let varnames_override: Option<&Vec<String>> = ext
1018 .filter(|_| self.config.types.x_enum_varnames_enabled())
1019 .map(|e| &e.varnames)
1020 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1021 let descriptions_override: Option<&Vec<String>> = ext
1022 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1023 .map(|e| &e.descriptions)
1024 .filter(|v| !v.is_empty() && v.len() == known_values.len());
1025
1026 let variant_ident_for = |index: usize, value: &str| -> proc_macro2::Ident {
1027 let name = match varnames_override {
1028 Some(v) => v[index].clone(),
1029 None => self.to_rust_enum_variant(value),
1030 };
1031 format_ident!("{}", name)
1032 };
1033
1034 let known_variants = known_values.iter().enumerate().map(|(i, value)| {
1039 let variant_ident = variant_ident_for(i, value);
1040 let doc = descriptions_override
1041 .map(|d| {
1042 let s = self.sanitize_doc_comment(&d[i]);
1043 quote! { #[doc = #s] }
1044 })
1045 .unwrap_or_default();
1046 quote! {
1047 #doc
1048 #variant_ident,
1049 }
1050 });
1051
1052 let match_arms_de = known_values.iter().enumerate().map(|(i, value)| {
1053 let variant_ident = variant_ident_for(i, value);
1054 quote! {
1055 #value => Ok(#enum_name::#variant_ident),
1056 }
1057 });
1058
1059 let match_arms_ser = known_values.iter().enumerate().map(|(i, value)| {
1060 let variant_ident = variant_ident_for(i, value);
1061 quote! {
1062 #enum_name::#variant_ident => #value,
1063 }
1064 });
1065
1066 let derives = if self.config.enable_specta {
1067 quote! {
1068 #[derive(Debug, Clone, PartialEq, Eq)]
1069 #[cfg_attr(feature = "specta", derive(specta::Type))]
1070 }
1071 } else {
1072 quote! {
1073 #[derive(Debug, Clone, PartialEq, Eq)]
1074 }
1075 };
1076
1077 Ok(quote! {
1078 #doc_comment
1079 #derives
1080 pub enum #enum_name {
1081 #(#known_variants)*
1082 Custom(String),
1084 }
1085
1086 impl<'de> serde::Deserialize<'de> for #enum_name {
1087 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1088 where
1089 D: serde::Deserializer<'de>,
1090 {
1091 let value = String::deserialize(deserializer)?;
1092 match value.as_str() {
1093 #(#match_arms_de)*
1094 _ => Ok(#enum_name::Custom(value)),
1095 }
1096 }
1097 }
1098
1099 impl serde::Serialize for #enum_name {
1100 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1101 where
1102 S: serde::Serializer,
1103 {
1104 let value = match self {
1105 #(#match_arms_ser)*
1106 #enum_name::Custom(s) => s.as_str(),
1107 };
1108 serializer.serialize_str(value)
1109 }
1110 }
1111 })
1112 }
1113
1114 fn generate_string_enum(
1115 &self,
1116 schema: &crate::analysis::AnalyzedSchema,
1117 values: &[String],
1118 ext: Option<&crate::analysis::EnumExtensions>,
1119 ) -> Result<TokenStream> {
1120 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1121
1122 let default_value = schema
1129 .default
1130 .as_ref()
1131 .and_then(|v| v.as_str())
1132 .map(|s| s.to_string());
1133 let has_default_match = match &default_value {
1134 Some(d) => values.iter().any(|v| v == d),
1135 None => !values.is_empty(),
1136 };
1137
1138 let varnames_override: Option<&Vec<String>> = ext
1142 .filter(|_| self.config.types.x_enum_varnames_enabled())
1143 .map(|e| &e.varnames)
1144 .filter(|v| !v.is_empty() && v.len() == values.len());
1145 let descriptions_override: Option<&Vec<String>> = ext
1146 .filter(|_| self.config.types.x_enum_descriptions_enabled())
1147 .map(|e| &e.descriptions)
1148 .filter(|v| !v.is_empty() && v.len() == values.len());
1149
1150 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
1157 let variant_pairs: Vec<(syn::Ident, &String, bool, Option<String>)> = values
1158 .iter()
1159 .enumerate()
1160 .map(|(i, value)| {
1161 let base = match varnames_override {
1162 Some(v) => v[i].clone(),
1163 None => self.to_rust_enum_variant(value),
1164 };
1165 let mut variant_name = base.clone();
1166 let mut suffix = 2;
1167 while !used.insert(variant_name.clone()) {
1168 variant_name = format!("{base}_{suffix}");
1169 suffix += 1;
1170 }
1171 let variant_ident = format_ident!("{}", variant_name);
1172 let is_default = if let Some(ref default) = default_value {
1173 value == default
1174 } else {
1175 i == 0
1176 };
1177 let description = descriptions_override.map(|d| d[i].clone());
1178 (variant_ident, value, is_default, description)
1179 })
1180 .collect();
1181
1182 let variants =
1183 variant_pairs
1184 .iter()
1185 .map(|(variant_ident, value, is_default, description)| {
1186 let doc = description
1187 .as_ref()
1188 .map(|d| {
1189 let s = self.sanitize_doc_comment(d);
1190 quote! { #[doc = #s] }
1191 })
1192 .unwrap_or_default();
1193 if *is_default {
1194 quote! {
1195 #doc
1196 #[default]
1197 #[serde(rename = #value)]
1198 #variant_ident,
1199 }
1200 } else {
1201 quote! {
1202 #doc
1203 #[serde(rename = #value)]
1204 #variant_ident,
1205 }
1206 }
1207 });
1208
1209 let as_str_arms = variant_pairs.iter().map(|(variant_ident, value, _, _)| {
1213 quote! { Self::#variant_ident => #value, }
1214 });
1215
1216 let doc_comment = if let Some(desc) = &schema.description {
1217 quote! { #[doc = #desc] }
1218 } else {
1219 TokenStream::new()
1220 };
1221
1222 let derives = match (self.config.enable_specta, has_default_match) {
1225 (true, true) => quote! {
1226 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1227 #[cfg_attr(feature = "specta", derive(specta::Type))]
1228 },
1229 (true, false) => quote! {
1230 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1231 #[cfg_attr(feature = "specta", derive(specta::Type))]
1232 },
1233 (false, true) => quote! {
1234 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
1235 },
1236 (false, false) => quote! {
1237 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
1238 },
1239 };
1240
1241 Ok(quote! {
1242 #doc_comment
1243 #derives
1244 pub enum #enum_name {
1245 #(#variants)*
1246 }
1247
1248 impl #enum_name {
1249 pub fn as_str(&self) -> &'static str {
1250 match self {
1251 #(#as_str_arms)*
1252 }
1253 }
1254 }
1255
1256 impl ::std::fmt::Display for #enum_name {
1257 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
1258 f.write_str(self.as_str())
1259 }
1260 }
1261
1262 impl AsRef<str> for #enum_name {
1263 fn as_ref(&self) -> &str {
1264 self.as_str()
1265 }
1266 }
1267 })
1268 }
1269
1270 fn generate_struct(
1271 &self,
1272 schema: &crate::analysis::AnalyzedSchema,
1273 properties: &BTreeMap<String, crate::analysis::PropertyInfo>,
1274 required: &std::collections::HashSet<String>,
1275 additional_properties: &crate::analysis::ObjectAdditionalProperties,
1276 analysis: &crate::analysis::SchemaAnalysis,
1277 discriminator_info: Option<&DiscriminatedVariantInfo>,
1278 ) -> Result<TokenStream> {
1279 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1280
1281 let mut sorted_properties: Vec<_> = properties.iter().collect();
1283 sorted_properties.sort_by_key(|(name, _)| name.as_str());
1284
1285 let mut used_field_idents: std::collections::HashSet<String> =
1293 std::collections::HashSet::new();
1294
1295 let mut fields: Vec<TokenStream> = sorted_properties
1296 .into_iter()
1297 .filter(|(field_name, _)| {
1298 if let Some(info) = discriminator_info {
1302 if !info.is_parent_untagged
1303 && field_name.as_str() == info.discriminator_field.as_str()
1304 {
1305 false } else {
1307 true }
1309 } else {
1310 true }
1312 })
1313 .map(|(field_name, prop)| {
1314 let raw = self.to_rust_field_name(field_name);
1315 let mut chosen = raw.clone();
1316 let mut suffix = 2;
1317 while !used_field_idents.insert(chosen.clone()) {
1318 chosen = format!("{raw}_{suffix}");
1319 suffix += 1;
1320 }
1321 let field_ident = Self::to_field_ident(&chosen);
1322 let is_required = required.contains(field_name);
1323 let field_type =
1324 self.generate_field_type(&schema.name, field_name, prop, is_required, analysis);
1325
1326 let serde_attrs =
1327 self.generate_serde_field_attrs(field_name, prop, is_required, analysis);
1328 let specta_attrs = self.generate_specta_field_attrs(field_name);
1329
1330 let doc_comment = if let Some(desc) = &prop.description {
1331 let sanitized_desc = self.sanitize_doc_comment(desc);
1332 quote! { #[doc = #sanitized_desc] }
1333 } else {
1334 TokenStream::new()
1335 };
1336 let constraint_doc = self.generate_constraint_doc(&prop.constraints);
1337
1338 quote! {
1339 #doc_comment
1340 #constraint_doc
1341 #serde_attrs
1342 #specta_attrs
1343 pub #field_ident: #field_type,
1344 }
1345 })
1346 .collect();
1347
1348 match additional_properties {
1354 crate::analysis::ObjectAdditionalProperties::Forbidden => {}
1355 crate::analysis::ObjectAdditionalProperties::Untyped => {
1356 fields.push(quote! {
1357 #[serde(flatten)]
1359 pub additional_properties:
1360 std::collections::BTreeMap<String, serde_json::Value>,
1361 });
1362 }
1363 crate::analysis::ObjectAdditionalProperties::Typed { value_type } => {
1364 let value_tokens = self.generate_array_item_type(value_type, analysis);
1365 fields.push(quote! {
1366 #[serde(flatten)]
1369 pub additional_properties:
1370 std::collections::BTreeMap<String, #value_tokens>,
1371 });
1372 }
1373 }
1374
1375 let doc_comment = if let Some(desc) = &schema.description {
1376 quote! { #[doc = #desc] }
1377 } else {
1378 TokenStream::new()
1379 };
1380
1381 let derives = if self.config.enable_specta {
1385 quote! {
1386 #[derive(Debug, Clone, Deserialize, Serialize)]
1387 #[cfg_attr(feature = "specta", derive(specta::Type))]
1388 }
1389 } else {
1390 quote! {
1391 #[derive(Debug, Clone, Deserialize, Serialize)]
1392 }
1393 };
1394
1395 Ok(quote! {
1396 #doc_comment
1397 #derives
1398 pub struct #struct_name {
1399 #(#fields)*
1400 }
1401 })
1402 }
1403
1404 fn generate_discriminated_enum(
1405 &self,
1406 schema: &crate::analysis::AnalyzedSchema,
1407 discriminator_field: &str,
1408 variants: &[crate::analysis::UnionVariant],
1409 analysis: &crate::analysis::SchemaAnalysis,
1410 ) -> Result<TokenStream> {
1411 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1412
1413 let has_nested_discriminated_union = variants.iter().any(|variant| {
1415 if let Some(variant_schema) = analysis.schemas.get(&variant.type_name) {
1416 matches!(
1417 variant_schema.schema_type,
1418 crate::analysis::SchemaType::DiscriminatedUnion { .. }
1419 )
1420 } else {
1421 false
1422 }
1423 });
1424
1425 if has_nested_discriminated_union {
1427 let schema_refs: Vec<crate::analysis::SchemaRef> = variants
1429 .iter()
1430 .map(|v| crate::analysis::SchemaRef {
1431 target: v.type_name.clone(),
1432 nullable: false,
1433 })
1434 .collect();
1435 return self.generate_union_enum(schema, &schema_refs, analysis);
1436 }
1437
1438 let enclosing = self.to_rust_type_name(&schema.name);
1439 let enum_variants = variants.iter().map(|variant| {
1440 let variant_name = format_ident!("{}", variant.rust_name);
1441 let variant_value = &variant.discriminator_value;
1442
1443 let variant_type = format_ident!("{}", self.to_rust_type_name(&variant.type_name));
1444 let payload = if self.to_rust_type_name(&variant.type_name) == enclosing
1448 || analysis
1449 .dependencies
1450 .recursive_schemas
1451 .contains(&variant.type_name)
1452 {
1453 quote! { Box<#variant_type> }
1454 } else {
1455 quote! { #variant_type }
1456 };
1457 quote! {
1458 #[serde(rename = #variant_value)]
1459 #variant_name(#payload),
1460 }
1461 });
1462
1463 let doc_comment = if let Some(desc) = &schema.description {
1464 quote! { #[doc = #desc] }
1465 } else {
1466 TokenStream::new()
1467 };
1468
1469 let derives = if self.config.enable_specta {
1471 quote! {
1472 #[derive(Debug, Clone, Deserialize, Serialize)]
1473 #[cfg_attr(feature = "specta", derive(specta::Type))]
1474 #[serde(tag = #discriminator_field)]
1475 }
1476 } else {
1477 quote! {
1478 #[derive(Debug, Clone, Deserialize, Serialize)]
1479 #[serde(tag = #discriminator_field)]
1480 }
1481 };
1482
1483 Ok(quote! {
1484 #doc_comment
1485 #derives
1486 pub enum #enum_name {
1487 #(#enum_variants)*
1488 }
1489 })
1490 }
1491
1492 fn should_use_untagged_discriminated_union(
1494 &self,
1495 schema: &crate::analysis::AnalyzedSchema,
1496 analysis: &crate::analysis::SchemaAnalysis,
1497 ) -> bool {
1498 for other_schema in analysis.schemas.values() {
1503 if let crate::analysis::SchemaType::DiscriminatedUnion {
1504 variants,
1505 discriminator_field: _,
1506 } = &other_schema.schema_type
1507 {
1508 for variant in variants {
1509 if variant.type_name == schema.name {
1510 if let crate::analysis::SchemaType::DiscriminatedUnion {
1515 discriminator_field: current_discriminator,
1516 variants: current_variants,
1517 ..
1518 } = &schema.schema_type
1519 {
1520 for current_variant in current_variants {
1522 if let Some(variant_schema) =
1523 analysis.schemas.get(¤t_variant.type_name)
1524 {
1525 if let crate::analysis::SchemaType::Object {
1526 properties, ..
1527 } = &variant_schema.schema_type
1528 {
1529 if properties.contains_key(current_discriminator) {
1530 return false;
1533 }
1534 }
1535 }
1536 }
1537 }
1538
1539 return true;
1541 }
1542 }
1543 }
1544 }
1545 false
1546 }
1547
1548 fn generate_union_enum(
1549 &self,
1550 schema: &crate::analysis::AnalyzedSchema,
1551 variants: &[crate::analysis::SchemaRef],
1552 analysis: &crate::analysis::SchemaAnalysis,
1553 ) -> Result<TokenStream> {
1554 let enum_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
1555
1556 let mut used_variant_names = std::collections::HashSet::new();
1558 let enum_variants = variants.iter().enumerate().map(|(i, variant)| {
1559 let base_variant_name = self.type_name_to_variant_name(&variant.target);
1561 let variant_name = self.ensure_unique_variant_name_generator(
1562 base_variant_name,
1563 &mut used_variant_names,
1564 i,
1565 );
1566 let variant_name_ident = format_ident!("{}", variant_name);
1567
1568 let variant_type_tokens = if matches!(
1570 variant.target.as_str(),
1571 "bool"
1572 | "i8"
1573 | "i16"
1574 | "i32"
1575 | "i64"
1576 | "i128"
1577 | "u8"
1578 | "u16"
1579 | "u32"
1580 | "u64"
1581 | "u128"
1582 | "f32"
1583 | "f64"
1584 | "String"
1585 ) {
1586 let type_ident = format_ident!("{}", variant.target);
1587 quote! { #type_ident }
1588 } else if variant.target == "serde_json::Value" {
1589 quote! { serde_json::Value }
1592 } else if variant.target.starts_with("Vec<") && variant.target.ends_with(">") {
1593 let inner = &variant.target[4..variant.target.len() - 1];
1595
1596 if inner.starts_with("Vec<") && inner.ends_with(">") {
1598 let inner_inner = &inner[4..inner.len() - 1];
1599 if inner_inner == "serde_json::Value" {
1600 quote! { Vec<Vec<serde_json::Value>> }
1601 } else {
1602 let inner_inner_type = if matches!(
1603 inner_inner,
1604 "bool"
1605 | "i8"
1606 | "i16"
1607 | "i32"
1608 | "i64"
1609 | "i128"
1610 | "u8"
1611 | "u16"
1612 | "u32"
1613 | "u64"
1614 | "u128"
1615 | "f32"
1616 | "f64"
1617 | "String"
1618 ) {
1619 format_ident!("{}", inner_inner)
1620 } else {
1621 format_ident!("{}", self.to_rust_type_name(inner_inner))
1622 };
1623 quote! { Vec<Vec<#inner_inner_type>> }
1624 }
1625 } else if inner == "serde_json::Value" {
1626 quote! { Vec<serde_json::Value> }
1627 } else {
1628 let inner_type = if matches!(
1629 inner,
1630 "bool"
1631 | "i8"
1632 | "i16"
1633 | "i32"
1634 | "i64"
1635 | "i128"
1636 | "u8"
1637 | "u16"
1638 | "u32"
1639 | "u64"
1640 | "u128"
1641 | "f32"
1642 | "f64"
1643 | "String"
1644 ) {
1645 format_ident!("{}", inner)
1646 } else {
1647 format_ident!("{}", self.to_rust_type_name(inner))
1648 };
1649 quote! { Vec<#inner_type> }
1650 }
1651 } else if variant.target.contains("::") || variant.target.contains('<') {
1652 parse_rust_type(&variant.target).unwrap_or_else(|_| {
1657 let fallback = format_ident!("{}", self.to_rust_type_name(&variant.target));
1658 quote! { #fallback }
1659 })
1660 } else {
1661 let type_ident = format_ident!("{}", self.to_rust_type_name(&variant.target));
1662 quote! { #type_ident }
1663 };
1664
1665 let target_rust_name = self.to_rust_type_name(&variant.target);
1669 let enclosing_name = self.to_rust_type_name(&schema.name);
1670 let is_self_ref = target_rust_name == enclosing_name;
1671 let is_recursive_target = analysis
1675 .dependencies
1676 .recursive_schemas
1677 .contains(&variant.target);
1678 let variant_type_tokens = if is_self_ref || is_recursive_target {
1679 quote! { Box<#variant_type_tokens> }
1680 } else {
1681 variant_type_tokens
1682 };
1683
1684 quote! {
1685 #variant_name_ident(#variant_type_tokens),
1686 }
1687 });
1688
1689 let doc_comment = if let Some(desc) = &schema.description {
1690 quote! { #[doc = #desc] }
1691 } else {
1692 TokenStream::new()
1693 };
1694
1695 let derives = if self.config.enable_specta {
1697 quote! {
1698 #[derive(Debug, Clone, Deserialize, Serialize)]
1699 #[cfg_attr(feature = "specta", derive(specta::Type))]
1700 #[serde(untagged)]
1701 }
1702 } else {
1703 quote! {
1704 #[derive(Debug, Clone, Deserialize, Serialize)]
1705 #[serde(untagged)]
1706 }
1707 };
1708
1709 Ok(quote! {
1710 #doc_comment
1711 #derives
1712 pub enum #enum_name {
1713 #(#enum_variants)*
1714 }
1715 })
1716 }
1717
1718 fn target_aliases_back_to(
1723 &self,
1724 target: &str,
1725 enclosing_rust_name: &str,
1726 analysis: &crate::analysis::SchemaAnalysis,
1727 ) -> bool {
1728 let mut current = target.to_string();
1729 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
1730 for _ in 0..16 {
1731 if !visited.insert(current.clone()) {
1732 return true;
1733 }
1734 let Some(schema) = analysis.schemas.get(¤t) else {
1735 return false;
1736 };
1737 if let crate::analysis::SchemaType::Reference { target: next } = &schema.schema_type {
1738 if self.to_rust_type_name(next) == enclosing_rust_name {
1739 return true;
1740 }
1741 current = next.clone();
1742 continue;
1743 }
1744 return false;
1745 }
1746 false
1747 }
1748
1749 fn generate_field_type(
1750 &self,
1751 schema_name: &str,
1752 field_name: &str,
1753 prop: &crate::analysis::PropertyInfo,
1754 is_required: bool,
1755 analysis: &crate::analysis::SchemaAnalysis,
1756 ) -> TokenStream {
1757 use crate::analysis::SchemaType;
1758
1759 let base_type = match &prop.schema_type {
1760 SchemaType::Primitive { rust_type, .. } => {
1761 parse_rust_type(rust_type).unwrap_or_else(|_| {
1764 eprintln!(
1769 "⚠️ TypeMapper produced un-parseable type `{rust_type}`; \
1770 falling back to String"
1771 );
1772 quote! { String }
1773 })
1774 }
1775 SchemaType::Reference { target } => {
1776 let target_rust_name = self.to_rust_type_name(target);
1777 let target_type = format_ident!("{}", target_rust_name);
1778 let enclosing_rust_name = self.to_rust_type_name(schema_name);
1791 let is_self_via_rust_name = target_rust_name == enclosing_rust_name;
1792 let is_alias_chain_self =
1793 self.target_aliases_back_to(target, &enclosing_rust_name, analysis);
1794 if analysis.dependencies.recursive_schemas.contains(target)
1795 || is_self_via_rust_name
1796 || is_alias_chain_self
1797 {
1798 quote! { Box<#target_type> }
1799 } else {
1800 quote! { #target_type }
1801 }
1802 }
1803 SchemaType::Array { item_type } => {
1804 let inner_type = self.generate_array_item_type(item_type, analysis);
1805 quote! { Vec<#inner_type> }
1806 }
1807 _ => {
1808 quote! { serde_json::Value }
1810 }
1811 };
1812
1813 let override_key = format!("{schema_name}.{field_name}");
1815 let is_nullable_override = self
1816 .config
1817 .nullable_field_overrides
1818 .get(&override_key)
1819 .copied()
1820 .unwrap_or(false);
1821
1822 if is_required && !prop.nullable && !is_nullable_override {
1823 if prop.default.is_some() && self.type_lacks_default(&prop.schema_type, analysis) {
1826 quote! { Option<#base_type> }
1827 } else {
1828 base_type
1829 }
1830 } else {
1831 quote! { Option<#base_type> }
1832 }
1833 }
1834
1835 fn generate_serde_field_attrs(
1836 &self,
1837 field_name: &str,
1838 prop: &crate::analysis::PropertyInfo,
1839 is_required: bool,
1840 analysis: &crate::analysis::SchemaAnalysis,
1841 ) -> TokenStream {
1842 let mut attrs = Vec::new();
1843
1844 let rust_field_name = self.to_rust_field_name(field_name);
1847 let comparison_name = rust_field_name
1848 .strip_prefix("r#")
1849 .unwrap_or(&rust_field_name);
1850 if comparison_name != field_name {
1851 attrs.push(quote! { rename = #field_name });
1852 }
1853
1854 if !is_required || prop.nullable {
1856 attrs.push(quote! { skip_serializing_if = "Option::is_none" });
1857 }
1858
1859 if prop.default.is_some()
1863 && (is_required && !prop.nullable)
1864 && !self.type_lacks_default(&prop.schema_type, analysis)
1865 {
1866 attrs.push(quote! { default });
1867 }
1868
1869 if let crate::analysis::SchemaType::Primitive {
1877 serde_with: Some(codec),
1878 ..
1879 } = &prop.schema_type
1880 {
1881 let is_option_wrapped = !is_required || prop.nullable;
1885 let codec_path = if is_option_wrapped {
1886 format!("{codec}::option")
1887 } else {
1888 codec.clone()
1889 };
1890 attrs.push(quote! { with = #codec_path });
1891 }
1892
1893 if attrs.is_empty() {
1894 TokenStream::new()
1895 } else {
1896 quote! { #[serde(#(#attrs),*)] }
1897 }
1898 }
1899
1900 fn type_lacks_default(
1904 &self,
1905 schema_type: &crate::analysis::SchemaType,
1906 analysis: &crate::analysis::SchemaAnalysis,
1907 ) -> bool {
1908 use crate::analysis::SchemaType;
1909 match schema_type {
1910 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => true,
1911 SchemaType::Primitive { rust_type, .. } => matches!(
1915 rust_type.as_str(),
1916 "chrono::DateTime<chrono::Utc>"
1917 | "chrono::NaiveDate"
1918 | "chrono::NaiveTime"
1919 | "chrono::Duration"
1920 | "url::Url"
1921 | "time::OffsetDateTime"
1922 | "time::Date"
1923 | "time::Time"
1924 | "iso8601::Duration"
1925 | "email_address::EmailAddress"
1926 ),
1927 SchemaType::Reference { target } => {
1928 if let Some(schema) = analysis.schemas.get(target) {
1929 self.type_lacks_default(&schema.schema_type, analysis)
1930 } else {
1931 false
1932 }
1933 }
1934 _ => false,
1935 }
1936 }
1937
1938 fn generate_specta_field_attrs(&self, field_name: &str) -> TokenStream {
1939 if !self.config.enable_specta {
1940 return TokenStream::new();
1941 }
1942
1943 let camel_case_name = self.to_camel_case(field_name);
1945
1946 if camel_case_name != field_name {
1948 quote! { #[cfg_attr(feature = "specta", specta(rename = #camel_case_name))] }
1949 } else {
1950 TokenStream::new()
1951 }
1952 }
1953
1954 pub(crate) fn to_rust_enum_variant(&self, s: &str) -> String {
1955 let neg_prefix =
1959 if s.starts_with('-') && s.chars().skip(1).all(|c| c.is_ascii_digit() || c == '.') {
1960 "Neg"
1961 } else {
1962 ""
1963 };
1964
1965 let mut result = String::new();
1967 let mut next_upper = true;
1968 let mut prev_was_upper = false;
1969
1970 for (i, c) in s.chars().enumerate() {
1971 match c {
1972 'a'..='z' => {
1973 if next_upper {
1974 result.push(c.to_ascii_uppercase());
1975 next_upper = false;
1976 } else {
1977 result.push(c);
1978 }
1979 prev_was_upper = false;
1980 }
1981 'A'..='Z' => {
1982 if next_upper || (!prev_was_upper && i > 0) {
1983 result.push(c);
1985 next_upper = false;
1986 } else {
1987 result.push(c.to_ascii_lowercase());
1989 }
1990 prev_was_upper = true;
1991 }
1992 '0'..='9' => {
1993 result.push(c);
1994 next_upper = false;
1995 prev_was_upper = false;
1996 }
1997 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => {
1998 next_upper = true;
2000 prev_was_upper = false;
2001 }
2002 _ => {
2003 next_upper = true;
2005 prev_was_upper = false;
2006 }
2007 }
2008 }
2009
2010 if result.is_empty() {
2012 result = "Value".to_string();
2013 }
2014
2015 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2017 result = format!("Variant{neg_prefix}{result}");
2018 } else if !neg_prefix.is_empty() {
2019 result = format!("{neg_prefix}{result}");
2022 }
2023
2024 match result.as_str() {
2026 "Null" => "NullValue".to_string(),
2027 "True" => "TrueValue".to_string(),
2028 "False" => "FalseValue".to_string(),
2029 "Type" => "Type_".to_string(),
2030 "Match" => "Match_".to_string(),
2031 "Fn" => "Fn_".to_string(),
2032 "Impl" => "Impl_".to_string(),
2033 "Trait" => "Trait_".to_string(),
2034 "Struct" => "Struct_".to_string(),
2035 "Enum" => "Enum_".to_string(),
2036 "Mod" => "Mod_".to_string(),
2037 "Use" => "Use_".to_string(),
2038 "Pub" => "Pub_".to_string(),
2039 "Const" => "Const_".to_string(),
2040 "Static" => "Static_".to_string(),
2041 "Let" => "Let_".to_string(),
2042 "Mut" => "Mut_".to_string(),
2043 "Ref" => "Ref_".to_string(),
2044 "Move" => "Move_".to_string(),
2045 "Return" => "Return_".to_string(),
2046 "If" => "If_".to_string(),
2047 "Else" => "Else_".to_string(),
2048 "While" => "While_".to_string(),
2049 "For" => "For_".to_string(),
2050 "Loop" => "Loop_".to_string(),
2051 "Break" => "Break_".to_string(),
2052 "Continue" => "Continue_".to_string(),
2053 "Self" => "Self_".to_string(),
2054 "Super" => "Super_".to_string(),
2055 "Crate" => "Crate_".to_string(),
2056 "Async" => "Async_".to_string(),
2057 "Await" => "Await_".to_string(),
2058 _ => result,
2059 }
2060 }
2061
2062 #[allow(dead_code)]
2063 fn to_rust_identifier(&self, s: &str) -> String {
2064 let mut result = s
2066 .chars()
2067 .map(|c| match c {
2068 'a'..='z' | 'A'..='Z' | '0'..='9' => c,
2069 '.' | '-' | '_' | ' ' | '@' | '#' | '$' | '/' | '\\' => '_',
2070 _ => '_',
2071 })
2072 .collect::<String>();
2073
2074 result = result.trim_matches('_').to_string();
2076
2077 if result.is_empty() {
2079 result = "value".to_string();
2080 }
2081
2082 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2084 result = format!("variant_{result}");
2085 }
2086
2087 match result.as_str() {
2089 "null" => "null_value".to_string(),
2090 "true" => "true_value".to_string(),
2091 "false" => "false_value".to_string(),
2092 "type" => "type_".to_string(),
2093 "match" => "match_".to_string(),
2094 "fn" => "fn_".to_string(),
2095 "impl" => "impl_".to_string(),
2096 "trait" => "trait_".to_string(),
2097 "struct" => "struct_".to_string(),
2098 "enum" => "enum_".to_string(),
2099 "mod" => "mod_".to_string(),
2100 "use" => "use_".to_string(),
2101 "pub" => "pub_".to_string(),
2102 "const" => "const_".to_string(),
2103 "static" => "static_".to_string(),
2104 "let" => "let_".to_string(),
2105 "mut" => "mut_".to_string(),
2106 "ref" => "ref_".to_string(),
2107 "move" => "move_".to_string(),
2108 "return" => "return_".to_string(),
2109 "if" => "if_".to_string(),
2110 "else" => "else_".to_string(),
2111 "while" => "while_".to_string(),
2112 "for" => "for_".to_string(),
2113 "loop" => "loop_".to_string(),
2114 "break" => "break_".to_string(),
2115 "continue" => "continue_".to_string(),
2116 "self" => "self_".to_string(),
2117 "super" => "super_".to_string(),
2118 "crate" => "crate_".to_string(),
2119 "async" => "async_".to_string(),
2120 "await" => "await_".to_string(),
2121 "override" => "override_".to_string(),
2123 "box" => "box_".to_string(),
2124 "dyn" => "dyn_".to_string(),
2125 "where" => "where_".to_string(),
2126 "in" => "in_".to_string(),
2127 "abstract" => "abstract_".to_string(),
2129 "become" => "become_".to_string(),
2130 "do" => "do_".to_string(),
2131 "final" => "final_".to_string(),
2132 "macro" => "macro_".to_string(),
2133 "priv" => "priv_".to_string(),
2134 "try" => "try_".to_string(),
2135 "typeof" => "typeof_".to_string(),
2136 "unsized" => "unsized_".to_string(),
2137 "virtual" => "virtual_".to_string(),
2138 "yield" => "yield_".to_string(),
2139 _ => result,
2140 }
2141 }
2142
2143 fn generate_constraint_doc(
2151 &self,
2152 constraints: &crate::analysis::PropertyConstraints,
2153 ) -> TokenStream {
2154 use crate::type_mapping::ConstraintMode;
2155
2156 if constraints.is_empty() {
2157 return TokenStream::new();
2158 }
2159 match self.config.types.constraint_mode() {
2160 ConstraintMode::Off => TokenStream::new(),
2161 ConstraintMode::Doc => {
2162 let formatted = format_constraints_doc(constraints);
2163 quote! { #[doc = #formatted] }
2164 }
2165 }
2166 }
2167
2168 fn sanitize_doc_comment(&self, desc: &str) -> String {
2169 let mut result = desc.to_string();
2171
2172 if result.contains('\n')
2180 && (result.contains('{')
2181 || result.contains("```")
2182 || result.contains("Human:")
2183 || result.contains("Assistant:")
2184 || result
2185 .lines()
2186 .any(|line| line.trim().starts_with('"') && line.trim().ends_with('"')))
2187 {
2188 if result.contains("```") {
2190 result = result.replace("```", "```ignore");
2191 } else {
2192 if result.lines().any(|line| {
2194 let trimmed = line.trim();
2195 trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 2
2196 }) {
2197 result = format!("```ignore\n{result}\n```");
2198 }
2199 }
2200 }
2201
2202 result
2203 }
2204
2205 pub(crate) fn to_rust_type_name(&self, s: &str) -> String {
2206 let mut result = String::new();
2208 let mut next_upper = true;
2209 let mut prev_was_lower = false;
2210
2211 for c in s.chars() {
2212 match c {
2213 'a'..='z' => {
2214 if next_upper {
2215 result.push(c.to_ascii_uppercase());
2216 next_upper = false;
2217 } else {
2218 result.push(c);
2219 }
2220 prev_was_lower = true;
2221 }
2222 'A'..='Z' => {
2223 result.push(c);
2224 next_upper = false;
2225 prev_was_lower = false;
2226 }
2227 '0'..='9' => {
2228 if prev_was_lower && !result.chars().last().unwrap_or(' ').is_ascii_digit() {
2231 }
2233 result.push(c);
2234 next_upper = false;
2235 prev_was_lower = false;
2236 }
2237 '_' | '-' | '.' | ' ' => {
2238 next_upper = true;
2240 prev_was_lower = false;
2241 }
2242 _ => {
2243 next_upper = true;
2245 prev_was_lower = false;
2246 }
2247 }
2248 }
2249
2250 if result.is_empty() {
2252 result = "Type".to_string();
2253 }
2254
2255 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2257 result = format!("Type{result}");
2258 }
2259
2260 if matches!(
2267 result.as_str(),
2268 "Result"
2269 | "Option"
2270 | "Box"
2271 | "Vec"
2272 | "String"
2273 | "Some"
2274 | "None"
2275 | "Ok"
2276 | "Err"
2277 | "Default"
2278 | "Clone"
2279 | "Debug"
2280 | "Send"
2281 | "Sync"
2282 | "Sized"
2283 | "Iterator"
2284 | "From"
2285 | "Into"
2286 | "TryFrom"
2287 | "TryInto"
2288 | "AsRef"
2289 | "AsMut"
2290 ) {
2291 result.push_str("Type");
2292 }
2293
2294 result
2295 }
2296
2297 fn to_rust_field_name(&self, s: &str) -> String {
2298 let leading_marker = match s.chars().next() {
2302 Some('-') if s.len() > 1 => "neg_",
2303 Some('+') if s.len() > 1 => "pos_",
2304 _ => "",
2305 };
2306
2307 let mut result = String::new();
2309 let mut prev_was_upper = false;
2310 let mut prev_was_underscore = false;
2311
2312 for (i, c) in s.chars().enumerate() {
2313 match c {
2314 'A'..='Z' => {
2315 if i > 0 && !prev_was_upper && !prev_was_underscore {
2317 result.push('_');
2318 }
2319 result.push(c.to_ascii_lowercase());
2320 prev_was_upper = true;
2321 prev_was_underscore = false;
2322 }
2323 'a'..='z' | '0'..='9' => {
2324 result.push(c);
2325 prev_was_upper = false;
2326 prev_was_underscore = false;
2327 }
2328 '-' | '.' | '_' | '@' | '#' | '$' | ' ' => {
2329 if !prev_was_underscore && !result.is_empty() {
2330 result.push('_');
2331 prev_was_underscore = true;
2332 }
2333 prev_was_upper = false;
2334 }
2335 _ => {
2336 if !prev_was_underscore && !result.is_empty() {
2338 result.push('_');
2339 }
2340 prev_was_upper = false;
2341 prev_was_underscore = true;
2342 }
2343 }
2344 }
2345
2346 let mut result = result.trim_matches('_').to_string();
2348 if result.is_empty() {
2349 return "field".to_string();
2350 }
2351
2352 if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
2354 result = format!("field_{leading_marker}{result}");
2355 } else if !leading_marker.is_empty() {
2356 result = format!("{leading_marker}{result}");
2357 }
2358
2359 if matches!(result.as_str(), "self" | "super" | "crate" | "Self") {
2363 return format!("{result}_field");
2364 }
2365 if Self::is_rust_keyword(&result) {
2367 format!("r#{result}")
2368 } else {
2369 result
2370 }
2371 }
2372
2373 pub fn is_rust_keyword(s: &str) -> bool {
2375 matches!(
2376 s,
2377 "type"
2378 | "match"
2379 | "fn"
2380 | "struct"
2381 | "enum"
2382 | "impl"
2383 | "trait"
2384 | "mod"
2385 | "use"
2386 | "pub"
2387 | "const"
2388 | "static"
2389 | "let"
2390 | "mut"
2391 | "ref"
2392 | "move"
2393 | "return"
2394 | "if"
2395 | "else"
2396 | "while"
2397 | "for"
2398 | "loop"
2399 | "break"
2400 | "continue"
2401 | "self"
2402 | "super"
2403 | "crate"
2404 | "async"
2405 | "await"
2406 | "override"
2407 | "box"
2408 | "dyn"
2409 | "where"
2410 | "in"
2411 | "abstract"
2412 | "become"
2413 | "do"
2414 | "final"
2415 | "macro"
2416 | "priv"
2417 | "try"
2418 | "typeof"
2419 | "unsized"
2420 | "virtual"
2421 | "yield"
2422 | "gen"
2424 )
2425 }
2426
2427 pub fn to_field_ident(name: &str) -> proc_macro2::Ident {
2429 if let Some(raw) = name.strip_prefix("r#") {
2430 proc_macro2::Ident::new_raw(raw, proc_macro2::Span::call_site())
2431 } else {
2432 proc_macro2::Ident::new(name, proc_macro2::Span::call_site())
2433 }
2434 }
2435
2436 fn to_camel_case(&self, s: &str) -> String {
2437 let mut result = String::new();
2439 let mut capitalize_next = false;
2440
2441 for (i, c) in s.chars().enumerate() {
2442 match c {
2443 '_' | '-' | '.' | ' ' => {
2444 capitalize_next = true;
2446 }
2447 'A'..='Z' => {
2448 if i == 0 {
2449 result.push(c.to_ascii_lowercase());
2451 } else if capitalize_next {
2452 result.push(c);
2453 capitalize_next = false;
2454 } else {
2455 result.push(c.to_ascii_lowercase());
2456 }
2457 }
2458 'a'..='z' | '0'..='9' => {
2459 if capitalize_next {
2460 result.push(c.to_ascii_uppercase());
2461 capitalize_next = false;
2462 } else {
2463 result.push(c);
2464 }
2465 }
2466 _ => {
2467 capitalize_next = true;
2469 }
2470 }
2471 }
2472
2473 if result.is_empty() {
2474 return "field".to_string();
2475 }
2476
2477 result
2478 }
2479
2480 fn generate_composition_struct(
2481 &self,
2482 schema: &crate::analysis::AnalyzedSchema,
2483 schemas: &[crate::analysis::SchemaRef],
2484 ) -> Result<TokenStream> {
2485 let struct_name = format_ident!("{}", self.to_rust_type_name(&schema.name));
2486
2487 let fields = schemas.iter().enumerate().map(|(i, schema_ref)| {
2493 let field_name = format_ident!("part_{}", i);
2494 let field_type = format_ident!("{}", self.to_rust_type_name(&schema_ref.target));
2495
2496 quote! {
2497 #[serde(flatten)]
2498 pub #field_name: #field_type,
2499 }
2500 });
2501
2502 let doc_comment = if let Some(desc) = &schema.description {
2503 quote! { #[doc = #desc] }
2504 } else {
2505 TokenStream::new()
2506 };
2507
2508 let derives = if self.config.enable_specta {
2510 quote! {
2511 #[derive(Debug, Clone, Deserialize, Serialize)]
2512 #[cfg_attr(feature = "specta", derive(specta::Type))]
2513 }
2514 } else {
2515 quote! {
2516 #[derive(Debug, Clone, Deserialize, Serialize)]
2517 }
2518 };
2519
2520 Ok(quote! {
2521 #doc_comment
2522 #derives
2523 pub struct #struct_name {
2524 #(#fields)*
2525 }
2526 })
2527 }
2528
2529 #[allow(dead_code)]
2530 fn find_missing_types(&self, analysis: &SchemaAnalysis) -> std::collections::HashSet<String> {
2531 let mut missing = std::collections::HashSet::new();
2532 let defined_types: std::collections::HashSet<String> =
2533 analysis.schemas.keys().cloned().collect();
2534
2535 for schema in analysis.schemas.values() {
2537 match &schema.schema_type {
2538 crate::analysis::SchemaType::Union { variants } => {
2539 for variant in variants {
2540 if !defined_types.contains(&variant.target) {
2541 missing.insert(variant.target.clone());
2542 }
2543 }
2544 }
2545 crate::analysis::SchemaType::DiscriminatedUnion { variants, .. } => {
2546 for variant in variants {
2547 if !defined_types.contains(&variant.type_name) {
2548 missing.insert(variant.type_name.clone());
2549 }
2550 }
2551 }
2552 crate::analysis::SchemaType::Object { properties, .. } => {
2553 let mut sorted_props: Vec<_> = properties.iter().collect();
2555 sorted_props.sort_by_key(|(name, _)| name.as_str());
2556 for (_, prop) in sorted_props {
2557 if let crate::analysis::SchemaType::Reference { target } = &prop.schema_type
2558 {
2559 if !defined_types.contains(target) {
2560 missing.insert(target.clone());
2561 }
2562 }
2563 }
2564 }
2565 crate::analysis::SchemaType::Reference { target }
2566 if !defined_types.contains(target) =>
2567 {
2568 missing.insert(target.clone());
2569 }
2570 _ => {}
2571 }
2572 }
2573
2574 missing
2575 }
2576
2577 #[allow(clippy::only_used_in_recursion)]
2578 fn generate_array_item_type(
2579 &self,
2580 item_type: &crate::analysis::SchemaType,
2581 analysis: &crate::analysis::SchemaAnalysis,
2582 ) -> TokenStream {
2583 use crate::analysis::SchemaType;
2584
2585 match item_type {
2586 SchemaType::Primitive { rust_type, .. } => {
2587 if let Ok(parsed) = syn::parse_str::<syn::Type>(rust_type) {
2592 quote! { #parsed }
2593 } else if rust_type.contains("::") {
2594 let parts: Vec<_> = rust_type
2595 .split("::")
2596 .map(|p| format_ident!("{}", p))
2597 .collect();
2598 quote! { #(#parts)::* }
2599 } else {
2600 let type_ident = format_ident!("{}", rust_type);
2601 quote! { #type_ident }
2602 }
2603 }
2604 SchemaType::Reference { target } => {
2605 let target_type = format_ident!("{}", self.to_rust_type_name(target));
2606 if analysis.dependencies.recursive_schemas.contains(target) {
2608 quote! { Box<#target_type> }
2609 } else {
2610 quote! { #target_type }
2611 }
2612 }
2613 SchemaType::Array { item_type } => {
2614 let inner_type = self.generate_array_item_type(item_type, analysis);
2616 quote! { Vec<#inner_type> }
2617 }
2618 _ => {
2619 quote! { serde_json::Value }
2621 }
2622 }
2623 }
2624
2625 fn type_name_to_variant_name(&self, type_name: &str) -> String {
2627 match type_name {
2629 "bool" => return "Boolean".to_string(),
2630 "i8" | "i16" | "i32" | "i64" | "i128" => return "Integer".to_string(),
2631 "u8" | "u16" | "u32" | "u64" | "u128" => return "UnsignedInteger".to_string(),
2632 "f32" | "f64" => return "Number".to_string(),
2633 "String" => return "String".to_string(),
2634 "serde_json::Value" => return "Value".to_string(),
2635 "bytes::Bytes" => return "Binary".to_string(),
2639 "chrono::DateTime<chrono::Utc>" => return "DateTime".to_string(),
2640 "chrono::NaiveDate" => return "Date".to_string(),
2641 "chrono::NaiveTime" => return "Time".to_string(),
2642 "uuid::Uuid" => return "Uuid".to_string(),
2643 "url::Url" => return "Url".to_string(),
2644 "std::net::Ipv4Addr" => return "Ipv4".to_string(),
2645 "std::net::Ipv6Addr" => return "Ipv6".to_string(),
2646 _ => {}
2647 }
2648
2649 if type_name.starts_with("Vec<") && type_name.ends_with(">") {
2651 let inner = &type_name[4..type_name.len() - 1];
2652 if inner.starts_with("Vec<") && inner.ends_with(">") {
2654 let inner_inner = &inner[4..inner.len() - 1];
2655 return format!("{}ArrayArray", self.type_name_to_variant_name(inner_inner));
2656 }
2657 return format!("{}Array", self.type_name_to_variant_name(inner));
2658 }
2659
2660 let clean_name = type_name
2666 .trim_end_matches("Type")
2667 .trim_end_matches("Schema")
2668 .trim_end_matches("Item");
2669
2670 self.to_rust_type_name(clean_name)
2672 }
2673
2674 fn ensure_unique_variant_name_generator(
2676 &self,
2677 base_name: String,
2678 used_names: &mut std::collections::HashSet<String>,
2679 fallback_index: usize,
2680 ) -> String {
2681 if used_names.insert(base_name.clone()) {
2682 return base_name;
2683 }
2684
2685 for i in 2..100 {
2687 let numbered_name = format!("{base_name}{i}");
2688 if used_names.insert(numbered_name.clone()) {
2689 return numbered_name;
2690 }
2691 }
2692
2693 let fallback = format!("Variant{fallback_index}");
2695 used_names.insert(fallback.clone());
2696 fallback
2697 }
2698
2699 fn find_request_type_for_operation(
2701 &self,
2702 operation_id: &str,
2703 analysis: &SchemaAnalysis,
2704 ) -> Option<String> {
2705 analysis.operations.get(operation_id).and_then(|op| {
2707 op.request_body
2708 .as_ref()
2709 .and_then(|rb| rb.schema_name().map(|s| s.to_string()))
2710 })
2711 }
2712
2713 fn resolve_streaming_event_type(
2715 &self,
2716 endpoint: &crate::streaming::StreamingEndpoint,
2717 analysis: &SchemaAnalysis,
2718 ) -> Result<String> {
2719 match &endpoint.event_flow {
2720 crate::streaming::EventFlow::Simple => {
2721 if analysis.schemas.contains_key(&endpoint.event_union_type) {
2724 Ok(endpoint.event_union_type.to_string())
2725 } else {
2726 Err(crate::error::GeneratorError::ValidationError(format!(
2727 "Streaming response type '{}' not found in schema for simple streaming endpoint '{}'",
2728 endpoint.event_union_type, endpoint.operation_id
2729 )))
2730 }
2731 }
2732 crate::streaming::EventFlow::StartDeltaStop { .. } => {
2733 if analysis.schemas.contains_key(&endpoint.event_union_type) {
2736 Ok(endpoint.event_union_type.to_string())
2737 } else {
2738 Err(crate::error::GeneratorError::ValidationError(format!(
2739 "Event union type '{}' not found in schema for complex streaming endpoint '{}'",
2740 endpoint.event_union_type, endpoint.operation_id
2741 )))
2742 }
2743 }
2744 }
2745 }
2746
2747 fn generate_streaming_error_types(&self) -> Result<TokenStream> {
2749 Ok(quote! {
2750 #[derive(Debug, thiserror::Error)]
2752 pub enum StreamingError {
2753 #[error("Connection error: {0}")]
2754 Connection(String),
2755 #[error("HTTP error: {status}")]
2756 Http { status: u16 },
2757 #[error("SSE parsing error: {0}")]
2758 Parsing(String),
2759 #[error("Authentication error: {0}")]
2760 Authentication(String),
2761 #[error("Rate limit error: {0}")]
2762 RateLimit(String),
2763 #[error("API error: {0}")]
2764 Api(String),
2765 #[error("Timeout error: {0}")]
2766 Timeout(String),
2767 #[error("JSON serialization/deserialization error: {0}")]
2768 Json(#[from] serde_json::Error),
2769 #[error("Request error: {0}")]
2770 Request(reqwest::Error),
2771 }
2772
2773 impl From<reqwest::header::InvalidHeaderValue> for StreamingError {
2774 fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
2775 StreamingError::Api(format!("Invalid header value: {}", err))
2776 }
2777 }
2778
2779 impl From<reqwest::Error> for StreamingError {
2780 fn from(err: reqwest::Error) -> Self {
2781 if err.is_timeout() {
2782 StreamingError::Timeout(err.to_string())
2783 } else if err.is_status() {
2784 if let Some(status) = err.status() {
2785 StreamingError::Http { status: status.as_u16() }
2786 } else {
2787 StreamingError::Connection(err.to_string())
2788 }
2789 } else {
2790 StreamingError::Request(err)
2791 }
2792 }
2793 }
2794 })
2795 }
2796
2797 fn generate_endpoint_trait(
2799 &self,
2800 endpoint: &crate::streaming::StreamingEndpoint,
2801 analysis: &SchemaAnalysis,
2802 ) -> Result<TokenStream> {
2803 use crate::streaming::HttpMethod;
2804
2805 let trait_name = format_ident!(
2806 "{}StreamingClient",
2807 self.to_rust_type_name(&endpoint.operation_id)
2808 );
2809 let method_name =
2810 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
2811 let event_type =
2812 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
2813
2814 let method_signature = match endpoint.http_method {
2816 HttpMethod::Get => {
2817 let mut param_defs = Vec::new();
2819 for qp in &endpoint.query_parameters {
2820 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
2821 if qp.required {
2822 param_defs.push(quote! { #param_name: &str });
2823 } else {
2824 param_defs.push(quote! { #param_name: Option<&str> });
2825 }
2826 }
2827 quote! {
2828 async fn #method_name(
2829 &self,
2830 #(#param_defs),*
2831 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2832 }
2833 }
2834 HttpMethod::Post => {
2835 let request_type = self
2837 .find_request_type_for_operation(&endpoint.operation_id, analysis)
2838 .unwrap_or_else(|| "serde_json::Value".to_string());
2839 let request_type_ident = if request_type.contains("::") {
2840 let parts: Vec<&str> = request_type.split("::").collect();
2841 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
2842 quote! { #(#path_parts)::* }
2843 } else {
2844 let ident = format_ident!("{}", request_type);
2845 quote! { #ident }
2846 };
2847 quote! {
2848 async fn #method_name(
2849 &self,
2850 request: #request_type_ident,
2851 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error>;
2852 }
2853 }
2854 };
2855
2856 Ok(quote! {
2857 #[async_trait]
2859 pub trait #trait_name {
2860 type Error: std::error::Error + Send + Sync + 'static;
2861
2862 #method_signature
2864 }
2865 })
2866 }
2867
2868 fn generate_streaming_client_impl(
2870 &self,
2871 streaming_config: &crate::streaming::StreamingConfig,
2872 analysis: &SchemaAnalysis,
2873 ) -> Result<TokenStream> {
2874 let client_name = format_ident!(
2875 "{}Client",
2876 self.to_rust_type_name(&streaming_config.client_module_name)
2877 );
2878
2879 let mut struct_fields = vec![
2882 quote! { base_url: String },
2883 quote! { api_key: Option<String> },
2884 quote! { http_client: reqwest::Client },
2885 quote! { custom_headers: std::collections::BTreeMap<String, String> },
2886 ];
2887
2888 let has_optional_headers = !streaming_config
2889 .endpoints
2890 .iter()
2891 .all(|e| e.optional_headers.is_empty());
2892
2893 if has_optional_headers {
2894 struct_fields
2895 .push(quote! { optional_headers: std::collections::BTreeMap<String, String> });
2896 }
2897
2898 let default_base_url = if let Some(ref streaming_config) = self.config.streaming_config {
2901 streaming_config
2902 .endpoints
2903 .first()
2904 .and_then(|e| e.base_url.as_deref())
2905 .unwrap_or("https://api.example.com")
2906 } else {
2907 "https://api.example.com"
2908 };
2909
2910 let constructor_fields = if has_optional_headers {
2912 quote! {
2913 base_url: #default_base_url.to_string(),
2914 api_key: None,
2915 http_client: reqwest::Client::new(),
2916 custom_headers: std::collections::BTreeMap::new(),
2917 optional_headers: std::collections::BTreeMap::new(),
2918 }
2919 } else {
2920 quote! {
2921 base_url: #default_base_url.to_string(),
2922 api_key: None,
2923 http_client: reqwest::Client::new(),
2924 custom_headers: std::collections::BTreeMap::new(),
2925 }
2926 };
2927
2928 let optional_headers_method = if has_optional_headers {
2930 quote! {
2931 pub fn set_optional_headers(&mut self, headers: std::collections::BTreeMap<String, String>) {
2933 self.optional_headers = headers;
2934 }
2935 }
2936 } else {
2937 TokenStream::new()
2938 };
2939
2940 let constructor = quote! {
2941 impl #client_name {
2942 pub fn new() -> Self {
2944 Self {
2945 #constructor_fields
2946 }
2947 }
2948
2949 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
2951 self.base_url = base_url.into();
2952 self
2953 }
2954
2955 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
2957 self.api_key = Some(api_key.into());
2958 self
2959 }
2960
2961 pub fn with_header(
2963 mut self,
2964 name: impl Into<String>,
2965 value: impl Into<String>,
2966 ) -> Self {
2967 self.custom_headers.insert(name.into(), value.into());
2968 self
2969 }
2970
2971 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
2973 self.http_client = client;
2974 self
2975 }
2976
2977 #optional_headers_method
2978 }
2979 };
2980
2981 let mut trait_impls = Vec::new();
2983 for endpoint in &streaming_config.endpoints {
2984 let trait_impl = self.generate_endpoint_trait_impl(endpoint, &client_name, analysis)?;
2985 trait_impls.push(trait_impl);
2986 }
2987
2988 let default_impl = quote! {
2990 impl Default for #client_name {
2991 fn default() -> Self {
2992 Self::new()
2993 }
2994 }
2995 };
2996
2997 Ok(quote! {
2998 #[derive(Debug, Clone)]
3000 pub struct #client_name {
3001 #(#struct_fields,)*
3002 }
3003
3004 #constructor
3005
3006 #default_impl
3007
3008 #(#trait_impls)*
3009 })
3010 }
3011
3012 fn generate_endpoint_trait_impl(
3014 &self,
3015 endpoint: &crate::streaming::StreamingEndpoint,
3016 client_name: &proc_macro2::Ident,
3017 analysis: &SchemaAnalysis,
3018 ) -> Result<TokenStream> {
3019 use crate::streaming::HttpMethod;
3020
3021 let trait_name = format_ident!(
3022 "{}StreamingClient",
3023 self.to_rust_type_name(&endpoint.operation_id)
3024 );
3025 let method_name =
3026 format_ident!("stream_{}", self.to_rust_field_name(&endpoint.operation_id));
3027 let event_type =
3028 format_ident!("{}", self.resolve_streaming_event_type(endpoint, analysis)?);
3029
3030 let mut header_setup = Vec::new();
3032 for (name, value) in &endpoint.required_headers {
3033 header_setup.push(quote! {
3034 headers.insert(#name, HeaderValue::from_static(#value));
3035 });
3036 }
3037
3038 if let Some(auth_header) = &endpoint.auth_header {
3041 match auth_header {
3042 crate::streaming::AuthHeader::Bearer(header_name) => {
3043 header_setup.push(quote! {
3044 if let Some(ref api_key) = self.api_key {
3045 headers.insert(#header_name, HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3046 }
3047 });
3048 }
3049 crate::streaming::AuthHeader::ApiKey(header_name) => {
3050 header_setup.push(quote! {
3051 if let Some(ref api_key) = self.api_key {
3052 headers.insert(#header_name, HeaderValue::from_str(api_key)?);
3053 }
3054 });
3055 }
3056 }
3057 } else {
3058 header_setup.push(quote! {
3060 if let Some(ref api_key) = self.api_key {
3061 headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {}", api_key))?);
3062 }
3063 });
3064 }
3065
3066 header_setup.push(quote! {
3068 for (name, value) in &self.custom_headers {
3069 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(value)) {
3070 headers.insert(header_name, header_value);
3071 }
3072 }
3073 });
3074
3075 if !endpoint.optional_headers.is_empty() {
3077 header_setup.push(quote! {
3078 for (key, value) in &self.optional_headers {
3079 if let (Ok(header_name), Ok(header_value)) = (reqwest::header::HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_str(value)) {
3080 headers.insert(header_name, header_value);
3081 }
3082 }
3083 });
3084 }
3085
3086 match endpoint.http_method {
3088 HttpMethod::Get => self.generate_get_streaming_impl(
3089 endpoint,
3090 client_name,
3091 &trait_name,
3092 &method_name,
3093 &event_type,
3094 &header_setup,
3095 ),
3096 HttpMethod::Post => self.generate_post_streaming_impl(
3097 endpoint,
3098 client_name,
3099 &trait_name,
3100 &method_name,
3101 &event_type,
3102 &header_setup,
3103 analysis,
3104 ),
3105 }
3106 }
3107
3108 fn generate_get_streaming_impl(
3110 &self,
3111 endpoint: &crate::streaming::StreamingEndpoint,
3112 client_name: &proc_macro2::Ident,
3113 trait_name: &proc_macro2::Ident,
3114 method_name: &proc_macro2::Ident,
3115 event_type: &proc_macro2::Ident,
3116 header_setup: &[TokenStream],
3117 ) -> Result<TokenStream> {
3118 let path = &endpoint.path;
3119
3120 let mut param_defs = Vec::new();
3122 let mut query_params = Vec::new();
3123
3124 for qp in &endpoint.query_parameters {
3125 let param_name = format_ident!("{}", self.to_rust_field_name(&qp.name));
3126 let param_name_str = &qp.name;
3127
3128 if qp.required {
3129 param_defs.push(quote! { #param_name: &str });
3130 query_params.push(quote! {
3131 url.query_pairs_mut().append_pair(#param_name_str, #param_name);
3132 });
3133 } else {
3134 param_defs.push(quote! { #param_name: Option<&str> });
3135 query_params.push(quote! {
3136 if let Some(v) = #param_name {
3137 url.query_pairs_mut().append_pair(#param_name_str, v);
3138 }
3139 });
3140 }
3141 }
3142
3143 let url_construction = quote! {
3145 let base_url = url::Url::parse(&self.base_url)
3146 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3147 let path_to_join = #path.trim_start_matches('/');
3148 let mut url = base_url.join(path_to_join)
3149 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?;
3150 #(#query_params)*
3151 };
3152
3153 let instrument_skip = quote! { #[instrument(skip(self), name = "streaming_get_request")] };
3154
3155 Ok(quote! {
3156 #[async_trait]
3157 impl #trait_name for #client_name {
3158 type Error = StreamingError;
3159
3160 #instrument_skip
3161 async fn #method_name(
3162 &self,
3163 #(#param_defs),*
3164 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3165 debug!("Starting streaming GET request");
3166
3167 let mut headers = HeaderMap::new();
3168 #(#header_setup)*
3169
3170 #url_construction
3171 let url_str = url.to_string();
3172 debug!("Making streaming GET request to: {}", url_str);
3173
3174 let request_builder = self.http_client
3175 .get(url_str)
3176 .headers(headers);
3177
3178 debug!("Creating SSE stream from request");
3179 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3180 info!("SSE stream created successfully");
3181 Ok(Box::pin(stream))
3182 }
3183 }
3184 })
3185 }
3186
3187 #[allow(clippy::too_many_arguments)]
3189 fn generate_post_streaming_impl(
3190 &self,
3191 endpoint: &crate::streaming::StreamingEndpoint,
3192 client_name: &proc_macro2::Ident,
3193 trait_name: &proc_macro2::Ident,
3194 method_name: &proc_macro2::Ident,
3195 event_type: &proc_macro2::Ident,
3196 header_setup: &[TokenStream],
3197 analysis: &SchemaAnalysis,
3198 ) -> Result<TokenStream> {
3199 let path = &endpoint.path;
3200
3201 let request_type = self
3203 .find_request_type_for_operation(&endpoint.operation_id, analysis)
3204 .unwrap_or_else(|| "serde_json::Value".to_string());
3205 let request_type_ident = if request_type.contains("::") {
3206 let parts: Vec<&str> = request_type.split("::").collect();
3207 let path_parts: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
3208 quote! { #(#path_parts)::* }
3209 } else {
3210 let ident = format_ident!("{}", request_type);
3211 quote! { #ident }
3212 };
3213
3214 let url_construction = quote! {
3216 let base_url = url::Url::parse(&self.base_url)
3217 .map_err(|e| StreamingError::Connection(format!("Invalid base URL: {}", e)))?;
3218 let path_to_join = #path.trim_start_matches('/');
3219 let url = base_url.join(path_to_join)
3220 .map_err(|e| StreamingError::Connection(format!("URL join error: {}", e)))?
3221 .to_string();
3222 };
3223
3224 let stream_param = &endpoint.stream_parameter;
3226 let stream_setup = if stream_param.is_empty() {
3227 quote! {
3228 let streaming_request = request;
3229 }
3230 } else {
3231 quote! {
3232 let mut streaming_request = request;
3234 if let Ok(mut request_value) = serde_json::to_value(&streaming_request) {
3235 if let Some(obj) = request_value.as_object_mut() {
3236 obj.insert(#stream_param.to_string(), serde_json::Value::Bool(true));
3237 }
3238 streaming_request = serde_json::from_value(request_value)?;
3239 }
3240 }
3241 };
3242
3243 Ok(quote! {
3244 #[async_trait]
3245 impl #trait_name for #client_name {
3246 type Error = StreamingError;
3247
3248 #[instrument(skip(self, request), name = "streaming_post_request")]
3249 async fn #method_name(
3250 &self,
3251 request: #request_type_ident,
3252 ) -> Result<Pin<Box<dyn Stream<Item = Result<#event_type, Self::Error>> + Send>>, Self::Error> {
3253 debug!("Starting streaming POST request");
3254
3255 #stream_setup
3256
3257 let mut headers = HeaderMap::new();
3258 #(#header_setup)*
3259
3260 #url_construction
3261 debug!("Making streaming POST request to: {}", url);
3262
3263 let request_builder = self.http_client
3264 .post(&url)
3265 .headers(headers)
3266 .json(&streaming_request);
3267
3268 debug!("Creating SSE stream from request");
3269 let stream = parse_sse_stream::<#event_type>(request_builder).await?;
3270 info!("SSE stream created successfully");
3271 Ok(Box::pin(stream))
3272 }
3273 }
3274 })
3275 }
3276
3277 fn generate_sse_parser_utilities(
3279 &self,
3280 _streaming_config: &crate::streaming::StreamingConfig,
3281 ) -> Result<TokenStream> {
3282 Ok(quote! {
3283 pub async fn parse_sse_stream<T>(
3285 request_builder: reqwest::RequestBuilder
3286 ) -> Result<impl Stream<Item = Result<T, StreamingError>>, StreamingError>
3287 where
3288 T: serde::de::DeserializeOwned + Send + 'static,
3289 {
3290 let mut event_source = reqwest_eventsource::EventSource::new(request_builder).map_err(|e| {
3291 StreamingError::Connection(format!("Failed to create event source: {}", e))
3292 })?;
3293
3294 let stream = event_source.filter_map(|event_result| async move {
3295 match event_result {
3296 Ok(reqwest_eventsource::Event::Open) => {
3297 debug!("SSE connection opened");
3298 None
3299 }
3300 Ok(reqwest_eventsource::Event::Message(message)) => {
3301 if message.event == "ping" {
3303 debug!("Received SSE ping event, skipping");
3304 return None;
3305 }
3306
3307 if message.data.trim().is_empty() {
3309 debug!("Empty SSE data, skipping");
3310 return None;
3311 }
3312
3313 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&message.data) {
3315 if let Some(event_type) = json_value.get("event").and_then(|v| v.as_str()) {
3316 if event_type == "ping" {
3317 debug!("Received ping event in JSON data, skipping");
3318 return None;
3319 }
3320 }
3321
3322 match serde_json::from_value::<T>(json_value) {
3324 Ok(parsed_event) => {
3325 Some(Ok(parsed_event))
3326 }
3327 Err(e) => {
3328 if message.data.contains("ping") || message.event.contains("ping") {
3329 debug!("Ignoring ping-related event: {}", message.data);
3330 None
3331 } else {
3332 Some(Err(StreamingError::Parsing(
3333 format!("Failed to parse SSE event: {} (raw: {})", e, message.data)
3334 )))
3335 }
3336 }
3337 }
3338 } else {
3339 Some(Err(StreamingError::Parsing(
3341 format!("SSE event is not valid JSON: {}", message.data)
3342 )))
3343 }
3344 }
3345 Err(e) => {
3346 match e {
3348 reqwest_eventsource::Error::StreamEnded => {
3349 debug!("SSE stream completed normally");
3350 None }
3352 reqwest_eventsource::Error::InvalidStatusCode(status, response) => {
3353 let status_code = status.as_u16();
3355
3356 let error_body = match response.text().await {
3358 Ok(body) => body,
3359 Err(_) => "Failed to read error response body".to_string()
3360 };
3361
3362 error!("SSE connection error - HTTP {}: {}", status_code, error_body);
3363
3364 let detailed_error = format!(
3365 "HTTP {} error: {}",
3366 status_code,
3367 error_body
3368 );
3369
3370 Some(Err(StreamingError::Connection(detailed_error)))
3371 }
3372 _ => {
3373 let error_str = e.to_string();
3374 if error_str.contains("stream closed") {
3375 debug!("SSE stream closed");
3376 None
3377 } else {
3378 error!("SSE connection error: {}", e);
3379 Some(Err(StreamingError::Connection(error_str)))
3380 }
3381 }
3382 }
3383 }
3384 }
3385 });
3386
3387 Ok(stream)
3388 }
3389 })
3390 }
3391
3392 fn generate_reconnection_utilities(
3394 &self,
3395 reconnect_config: &crate::streaming::ReconnectionConfig,
3396 ) -> Result<TokenStream> {
3397 let max_retries = reconnect_config.max_retries;
3398 let initial_delay = reconnect_config.initial_delay_ms;
3399 let max_delay = reconnect_config.max_delay_ms;
3400 let backoff_multiplier = reconnect_config.backoff_multiplier;
3401
3402 Ok(quote! {
3403 #[derive(Debug, Clone)]
3405 pub struct ReconnectionManager {
3406 max_retries: u32,
3407 initial_delay_ms: u64,
3408 max_delay_ms: u64,
3409 backoff_multiplier: f64,
3410 current_attempt: u32,
3411 }
3412
3413 impl ReconnectionManager {
3414 pub fn new() -> Self {
3416 Self {
3417 max_retries: #max_retries,
3418 initial_delay_ms: #initial_delay,
3419 max_delay_ms: #max_delay,
3420 backoff_multiplier: #backoff_multiplier,
3421 current_attempt: 0,
3422 }
3423 }
3424
3425 pub fn should_retry(&self) -> bool {
3427 self.current_attempt < self.max_retries
3428 }
3429
3430 pub fn next_retry_delay(&mut self) -> Duration {
3432 if !self.should_retry() {
3433 return Duration::from_secs(0);
3434 }
3435
3436 let delay_ms = (self.initial_delay_ms as f64
3437 * self.backoff_multiplier.powi(self.current_attempt as i32)) as u64;
3438 let delay_ms = delay_ms.min(self.max_delay_ms);
3439
3440 self.current_attempt += 1;
3441 Duration::from_millis(delay_ms)
3442 }
3443
3444 pub fn reset(&mut self) {
3446 self.current_attempt = 0;
3447 }
3448
3449 pub fn current_attempt(&self) -> u32 {
3451 self.current_attempt
3452 }
3453 }
3454
3455 impl Default for ReconnectionManager {
3456 fn default() -> Self {
3457 Self::new()
3458 }
3459 }
3460 })
3461 }
3462}