1use crate::openapi::{Discriminator, OpenApiSpec, Schema, SchemaType as OpenApiSchemaType};
2use crate::type_mapping::TypeMapper;
3use crate::{GeneratorError, Result};
4use serde_json::Value;
5use std::collections::{BTreeMap, HashSet};
6use std::path::Path;
7
8fn extract_enum_extensions(
15 original: &Value,
16 enum_value_count: usize,
17 schema_name: &str,
18) -> Option<EnumExtensions> {
19 let obj = original.as_object()?;
20
21 let read_string_array = |key: &str| -> Option<Vec<String>> {
22 let arr = obj.get(key)?.as_array()?;
23 let mut out = Vec::with_capacity(arr.len());
24 for v in arr {
25 out.push(v.as_str()?.to_string());
26 }
27 Some(out)
28 };
29
30 let varnames_raw = read_string_array("x-enum-varnames");
31 let descriptions_raw = read_string_array("x-enum-descriptions");
32
33 if varnames_raw.is_none() && descriptions_raw.is_none() {
34 return None;
35 }
36
37 let validate = |label: &str, vals: Option<Vec<String>>| -> Vec<String> {
38 let Some(vals) = vals else {
39 return Vec::new();
40 };
41 if vals.len() == enum_value_count {
42 vals
43 } else {
44 eprintln!(
45 "⚠️ {schema_name}: dropping {label} (expected {enum_value_count} entries, got {})",
46 vals.len()
47 );
48 Vec::new()
49 }
50 };
51
52 let varnames = validate("x-enum-varnames", varnames_raw);
53 let descriptions = validate("x-enum-descriptions", descriptions_raw);
54
55 if varnames.is_empty() && descriptions.is_empty() {
56 return None;
57 }
58 Some(EnumExtensions {
59 varnames,
60 descriptions,
61 })
62}
63
64#[derive(Debug, Clone)]
65pub struct SchemaAnalysis {
66 pub schemas: BTreeMap<String, AnalyzedSchema>,
68 pub dependencies: DependencyGraph,
70 pub patterns: DetectedPatterns,
72 pub operations: BTreeMap<String, OperationInfo>,
74 pub used_type_features: crate::type_mapping::UsedFeatures,
82 pub enum_extensions: BTreeMap<String, EnumExtensions>,
90}
91
92#[derive(Debug, Clone, Default)]
97pub struct EnumExtensions {
98 pub varnames: Vec<String>,
103 pub descriptions: Vec<String>,
105}
106
107#[derive(Debug, Clone)]
108pub struct AnalyzedSchema {
109 pub name: String,
110 pub original: Value,
111 pub schema_type: SchemaType,
112 pub dependencies: HashSet<String>,
113 pub nullable: bool,
114 pub description: Option<String>,
115 pub default: Option<serde_json::Value>,
116}
117
118#[derive(Debug, Clone)]
119pub enum SchemaType {
120 Primitive {
126 rust_type: String,
127 serde_with: Option<String>,
128 },
129 Object {
131 properties: BTreeMap<String, PropertyInfo>,
132 required: HashSet<String>,
133 additional_properties: ObjectAdditionalProperties,
134 },
135 DiscriminatedUnion {
137 discriminator_field: String,
138 variants: Vec<UnionVariant>,
139 },
140 Union { variants: Vec<SchemaRef> },
142 Array { item_type: Box<SchemaType> },
144 StringEnum { values: Vec<String> },
146 ExtensibleEnum { known_values: Vec<String> },
148 Composition { schemas: Vec<SchemaRef> },
150 Reference { target: String },
152}
153
154#[derive(Debug, Clone)]
159pub enum ObjectAdditionalProperties {
160 Forbidden,
163 Untyped,
166 Typed { value_type: Box<SchemaType> },
169}
170
171impl ObjectAdditionalProperties {
172 pub fn is_open(&self) -> bool {
175 !matches!(self, Self::Forbidden)
176 }
177}
178
179#[derive(Debug, Clone)]
180pub struct PropertyInfo {
181 pub schema_type: SchemaType,
182 pub nullable: bool,
183 pub description: Option<String>,
184 pub default: Option<serde_json::Value>,
185 pub serde_attrs: Vec<String>,
186 pub constraints: PropertyConstraints,
191}
192
193#[derive(Debug, Clone, Default)]
198pub struct PropertyConstraints {
199 pub minimum: Option<f64>,
200 pub maximum: Option<f64>,
201 pub exclusive_minimum: Option<f64>,
202 pub exclusive_maximum: Option<f64>,
203 pub multiple_of: Option<f64>,
204 pub min_length: Option<u64>,
205 pub max_length: Option<u64>,
206 pub pattern: Option<String>,
207 pub min_items: Option<u64>,
208 pub max_items: Option<u64>,
209 pub unique_items: Option<bool>,
210}
211
212impl PropertyConstraints {
213 pub fn is_empty(&self) -> bool {
214 self.minimum.is_none()
215 && self.maximum.is_none()
216 && self.exclusive_minimum.is_none()
217 && self.exclusive_maximum.is_none()
218 && self.multiple_of.is_none()
219 && self.min_length.is_none()
220 && self.max_length.is_none()
221 && self.pattern.is_none()
222 && self.min_items.is_none()
223 && self.max_items.is_none()
224 && self.unique_items.is_none()
225 }
226
227 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
232 use crate::openapi::ExclusiveBound;
233 let exclusive_minimum = match &details.exclusive_minimum {
234 Some(ExclusiveBound::Number(v)) => Some(*v),
235 _ => None,
236 };
237 let exclusive_maximum = match &details.exclusive_maximum {
238 Some(ExclusiveBound::Number(v)) => Some(*v),
239 _ => None,
240 };
241 Self {
242 minimum: details.minimum,
243 maximum: details.maximum,
244 exclusive_minimum,
245 exclusive_maximum,
246 multiple_of: details.multiple_of,
247 min_length: details.min_length,
248 max_length: details.max_length,
249 pattern: details.pattern.clone(),
250 min_items: details.min_items,
251 max_items: details.max_items,
252 unique_items: details.unique_items,
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
258pub struct UnionVariant {
259 pub rust_name: String,
260 pub type_name: String,
261 pub discriminator_value: String,
262 pub schema_ref: String,
263}
264
265#[derive(Debug, Clone)]
266pub struct SchemaRef {
267 pub target: String,
268 pub nullable: bool,
269}
270
271#[derive(Debug, Clone)]
272pub struct DependencyGraph {
273 pub edges: BTreeMap<String, HashSet<String>>,
274 pub recursive_schemas: HashSet<String>,
276}
277
278#[derive(Debug, Clone)]
279pub struct DetectedPatterns {
280 pub tagged_enum_schemas: HashSet<String>,
282 pub untagged_enum_schemas: HashSet<String>,
284 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
286}
287
288#[derive(Debug, Clone, Default, serde::Serialize)]
290pub struct OperationInfo {
291 pub operation_id: String,
293 pub method: String,
295 pub path: String,
297 pub summary: Option<String>,
299 pub description: Option<String>,
301 pub request_body: Option<RequestBodyContent>,
303 pub request_body_required: bool,
306 pub response_schemas: BTreeMap<String, String>,
308 pub parameters: Vec<ParameterInfo>,
310 pub supports_streaming: bool,
312 pub stream_parameter: Option<String>,
314 pub tags: Vec<String>,
318}
319
320#[derive(Debug, Clone, serde::Serialize)]
322#[serde(tag = "kind")]
323pub enum RequestBodyContent {
324 Json { schema_name: String },
325 FormUrlEncoded { schema_name: String },
326 Multipart,
327 OctetStream,
328 TextPlain,
329}
330
331impl RequestBodyContent {
332 pub fn schema_name(&self) -> Option<&str> {
334 match self {
335 Self::Json { schema_name } | Self::FormUrlEncoded { schema_name } => Some(schema_name),
336 _ => None,
337 }
338 }
339}
340
341fn base_param_ident(name: &str) -> String {
345 use heck::ToSnakeCase;
346 let suffix = if name.ends_with("<=") {
347 "_lte"
348 } else if name.ends_with(">=") {
349 "_gte"
350 } else if name.ends_with('<') {
351 "_lt"
352 } else if name.ends_with('>') {
353 "_gt"
354 } else {
355 ""
356 };
357 let stripped = name.trim_end_matches(['<', '>', '=']);
358 let mut snake = stripped.to_snake_case();
359 snake.push_str(suffix);
360 snake
361}
362
363#[derive(Debug, Clone, serde::Serialize)]
365pub struct ParameterInfo {
366 pub name: String,
368 pub location: String,
370 pub required: bool,
372 pub schema_ref: Option<String>,
374 pub rust_type: String,
376 pub description: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
384 pub enum_values: Option<Vec<String>>,
385 #[serde(skip_serializing_if = "Option::is_none")]
393 pub rust_ident: Option<String>,
394}
395
396impl Default for DependencyGraph {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402impl DependencyGraph {
403 pub fn new() -> Self {
404 Self {
405 edges: BTreeMap::new(),
406 recursive_schemas: HashSet::new(),
407 }
408 }
409
410 pub fn add_dependency(&mut self, from: String, to: String) {
411 self.edges.entry(from).or_default().insert(to);
412 }
413
414 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
416 self.detect_recursive_schemas();
418
419 let mut temp_edges = self.edges.clone();
421 for (schema, deps) in &mut temp_edges {
422 deps.remove(schema); }
424
425 let mut visited = HashSet::new();
426 let mut temp_visited = HashSet::new();
427 let mut result = Vec::new();
428
429 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
431 all_nodes.sort();
432 for node in all_nodes {
433 if !visited.contains(node) {
434 self.visit_node_recursive(
435 node,
436 &temp_edges,
437 &mut visited,
438 &mut temp_visited,
439 &mut result,
440 )?;
441 }
442 }
443
444 result.reverse();
445 Ok(result)
446 }
447
448 fn detect_recursive_schemas(&mut self) {
449 for (schema, deps) in &self.edges {
450 if deps.contains(schema) {
451 self.recursive_schemas.insert(schema.clone());
453 } else {
454 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
456 self.recursive_schemas.insert(schema.clone());
457 }
458 }
459 }
460
461 for (schema, deps) in &self.edges {
463 for dep in deps {
464 if let Some(dep_deps) = self.edges.get(dep) {
465 if dep_deps.contains(schema) {
466 self.recursive_schemas.insert(schema.clone());
468 self.recursive_schemas.insert(dep.clone());
469 }
470 }
471 }
472 }
473 }
474
475 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
476 if visited.contains(current) {
477 return false; }
479
480 visited.insert(current.to_string());
481
482 if let Some(deps) = self.edges.get(current) {
483 for dep in deps {
484 if dep == start {
485 return true; }
487 if self.has_cycle_from(start, dep, visited) {
488 return true;
489 }
490 }
491 }
492
493 false
494 }
495
496 #[allow(clippy::only_used_in_recursion)]
497 fn visit_node_recursive(
498 &self,
499 node: &str,
500 temp_edges: &BTreeMap<String, HashSet<String>>,
501 visited: &mut HashSet<String>,
502 temp_visited: &mut HashSet<String>,
503 result: &mut Vec<String>,
504 ) -> Result<()> {
505 if temp_visited.contains(node) {
506 return Ok(());
508 }
509
510 if visited.contains(node) {
511 return Ok(());
512 }
513
514 temp_visited.insert(node.to_string());
515
516 if let Some(dependencies) = temp_edges.get(node) {
517 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
519 sorted_deps.sort();
520 for dep in sorted_deps {
521 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
522 }
523 }
524
525 temp_visited.remove(node);
526 visited.insert(node.to_string());
527 result.push(node.to_string());
528
529 Ok(())
530 }
531}
532
533pub fn merge_schema_extensions(
536 main_spec: Value,
537 extension_paths: &[impl AsRef<Path>],
538) -> Result<Value> {
539 let mut result = main_spec;
540
541 for path in extension_paths {
542 let extension = load_extension_file(path.as_ref())?;
543 result = merge_json_objects_with_replacements(result, extension)?;
544 }
545
546 Ok(result)
547}
548
549fn load_extension_file(path: &Path) -> Result<Value> {
551 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
552 message: format!("Failed to read file {}: {}", path.display(), e),
553 })?;
554
555 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
556}
557
558fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
560 let replacements = extract_replacement_rules(&extension);
562
563 Ok(merge_json_objects_with_rules(
565 main,
566 extension,
567 &replacements,
568 ))
569}
570
571fn extract_replacement_rules(
573 extension: &Value,
574) -> std::collections::HashMap<String, (String, String)> {
575 let mut rules = std::collections::HashMap::new();
576
577 if let Some(x_replacements) = extension.get("x-replacements") {
578 if let Some(x_replacements_obj) = x_replacements.as_object() {
579 for (schema_name, replacement_rule) in x_replacements_obj {
580 if let Some(rule_obj) = replacement_rule.as_object() {
581 if let (Some(replace), Some(with)) = (
582 rule_obj.get("replace").and_then(|v| v.as_str()),
583 rule_obj.get("with").and_then(|v| v.as_str()),
584 ) {
585 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
586 }
588 }
589 }
590 }
591 }
592
593 rules
594}
595
596fn should_replace_variant(
598 schema_name: &str,
599 extension_refs: &[String],
600 replacements: &std::collections::HashMap<String, (String, String)>,
601) -> bool {
602 for (replace_schema, with_schema) in replacements.values() {
604 if schema_name == replace_schema {
605 let replacement_exists = extension_refs.iter().any(|ext_ref| {
607 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
608 ext_schema_name == with_schema
609 });
610
611 if replacement_exists {
612 return true;
613 }
614 }
615 }
616
617 extension_refs.iter().any(|ext_ref| {
619 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
620 schema_name == ext_schema_name
621 })
622}
623
624fn merge_json_objects_with_rules(
629 main: Value,
630 extension: Value,
631 replacements: &std::collections::HashMap<String, (String, String)>,
632) -> Value {
633 match (main, extension) {
634 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
636 let main_union_keyword = if main_obj.contains_key("oneOf") {
639 Some("oneOf")
640 } else if main_obj.contains_key("anyOf") {
641 Some("anyOf")
642 } else {
643 None
644 };
645 if let (Some(main_variants), Some(ext_variants)) = (
646 extract_schema_variants(&Value::Object(main_obj.clone())),
647 extract_schema_variants(&Value::Object(ext_obj.clone())),
648 ) {
649 let union_key = main_union_keyword.unwrap_or("oneOf");
650 println!(
651 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
652 main_variants.len(),
653 ext_variants.len()
654 );
655 let mut merged_variants = Vec::new();
658 let extension_refs: Vec<String> = ext_variants
659 .iter()
660 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
661 .map(|s| s.to_string())
662 .collect();
663
664 for main_variant in main_variants {
666 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
667 let schema_name = main_ref.split('/').next_back().unwrap_or("");
669 let should_replace =
670 should_replace_variant(schema_name, &extension_refs, replacements);
671
672 if should_replace {
673 println!("🔄 REPLACING {} (explicit rule)", schema_name);
674 }
675
676 if !should_replace {
677 merged_variants.push(main_variant);
678 }
679 } else {
680 merged_variants.push(main_variant);
682 }
683 }
684
685 for ext_variant in ext_variants {
687 merged_variants.push(ext_variant);
688 }
689
690 main_obj.remove("oneOf");
692 main_obj.remove("anyOf");
693 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
694
695 for (key, ext_value) in ext_obj {
697 if key != "oneOf" && key != "anyOf" {
698 match main_obj.get(&key) {
699 Some(main_value) => {
700 let merged_value = merge_json_objects_with_rules(
701 main_value.clone(),
702 ext_value,
703 replacements,
704 );
705 main_obj.insert(key, merged_value);
706 }
707 None => {
708 main_obj.insert(key, ext_value);
709 }
710 }
711 }
712 }
713
714 return Value::Object(main_obj);
715 }
716
717 for (key, ext_value) in ext_obj {
719 match main_obj.get(&key) {
720 Some(main_value) => {
721 let merged_value = merge_json_objects_with_rules(
723 main_value.clone(),
724 ext_value,
725 replacements,
726 );
727 main_obj.insert(key, merged_value);
728 }
729 None => {
730 main_obj.insert(key, ext_value);
732 }
733 }
734 }
735 Value::Object(main_obj)
736 }
737
738 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
740 main_arr.extend(ext_arr);
741 Value::Array(main_arr)
742 }
743
744 (_, extension) => extension,
746 }
747}
748
749fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
751 if let Value::Object(map) = obj {
752 if let Some(Value::Array(variants)) = map.get("oneOf") {
753 return Some(variants.clone());
754 }
755 if let Some(Value::Array(variants)) = map.get("anyOf") {
756 return Some(variants.clone());
757 }
758 }
759 None
760}
761
762pub struct SchemaAnalyzer {
763 schemas: BTreeMap<String, Schema>,
764 resolved_cache: BTreeMap<String, AnalyzedSchema>,
765 openapi_spec: Value,
766 current_schema_name: Option<String>,
767 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
768 type_mapper: TypeMapper,
773}
774
775impl SchemaAnalyzer {
776 pub fn new(openapi_spec: Value) -> Result<Self> {
780 Self::with_type_mapper(openapi_spec, TypeMapper::default())
781 }
782
783 pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
787 let spec: OpenApiSpec =
788 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
789 let schemas = Self::extract_schemas(&spec)?;
790
791 let component_parameters = spec
792 .components
793 .as_ref()
794 .and_then(|c| c.parameters.as_ref())
795 .cloned()
796 .unwrap_or_default();
797
798 Ok(Self {
799 schemas,
800 resolved_cache: BTreeMap::new(),
801 openapi_spec,
802 current_schema_name: None,
803 component_parameters,
804 type_mapper,
805 })
806 }
807
808 pub fn new_with_extensions(
811 openapi_spec: Value,
812 extension_paths: &[std::path::PathBuf],
813 ) -> Result<Self> {
814 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
815 Self::new(merged_spec)
816 }
817
818 pub fn new_with_extensions_and_type_mapper(
821 openapi_spec: Value,
822 extension_paths: &[std::path::PathBuf],
823 type_mapper: TypeMapper,
824 ) -> Result<Self> {
825 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
826 Self::with_type_mapper(merged_spec, type_mapper)
827 }
828
829 pub fn type_mapper(&self) -> &TypeMapper {
833 &self.type_mapper
834 }
835
836 fn generate_context_aware_name(
839 &self,
840 base_context: &str,
841 type_hint: &str,
842 index: usize,
843 schema: Option<&Schema>,
844 ) -> String {
845 if let Some(schema) = schema {
847 if type_hint == "Array"
849 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
850 {
851 if let Some(items_schema) = &schema.details().items {
852 if let Some(item_type) = items_schema.schema_type() {
854 match item_type {
855 OpenApiSchemaType::Object => {
856 return format!("{base_context}ItemArray");
857 }
858 OpenApiSchemaType::String => {
859 return format!("{base_context}StringArray");
860 }
861 _ => {}
862 }
863 }
864 }
865 }
866 }
867
868 match type_hint {
870 "Array" => {
871 format!("{base_context}Array")
873 }
874 "Variant" | "InlineVariant" => {
875 if index == 0 {
877 format!("{base_context}{type_hint}")
878 } else {
879 format!("{}{}{}", base_context, type_hint, index + 1)
880 }
881 }
882 _ => {
883 format!("{base_context}{type_hint}{index}")
885 }
886 }
887 }
888
889 fn to_pascal_case(&self, s: &str) -> String {
891 s.split(['_', '-'])
892 .filter(|part| !part.is_empty())
893 .map(|part| {
894 let mut chars = part.chars();
895 match chars.next() {
896 None => String::new(),
897 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
898 }
899 })
900 .collect()
901 }
902
903 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
904 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
909 Ok(schemas
910 .map(|m| {
911 m.iter()
912 .map(|(k, v)| (k.clone(), v.clone()))
913 .collect::<BTreeMap<_, _>>()
914 })
915 .unwrap_or_default())
916 }
917
918 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
919 let mut analysis = SchemaAnalysis {
920 schemas: BTreeMap::new(),
921 dependencies: DependencyGraph::new(),
922 patterns: DetectedPatterns {
923 tagged_enum_schemas: HashSet::new(),
924 untagged_enum_schemas: HashSet::new(),
925 type_mappings: BTreeMap::new(),
926 },
927 operations: BTreeMap::new(),
928 used_type_features: crate::type_mapping::UsedFeatures::default(),
929 enum_extensions: BTreeMap::new(),
930 };
931
932 self.detect_patterns(&mut analysis.patterns)?;
934
935 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
937 for schema_name in schema_names {
938 let analyzed = self.analyze_schema(&schema_name)?;
939
940 for dep in &analyzed.dependencies {
942 analysis
943 .dependencies
944 .add_dependency(schema_name.clone(), dep.clone());
945 }
946
947 analysis.schemas.insert(schema_name, analyzed);
948 }
949
950 for (inline_name, inline_schema) in &self.resolved_cache {
953 if !analysis.schemas.contains_key(inline_name) {
954 analysis
956 .schemas
957 .insert(inline_name.clone(), inline_schema.clone());
958
959 for dep in &inline_schema.dependencies {
961 analysis
962 .dependencies
963 .add_dependency(inline_name.clone(), dep.clone());
964 }
965
966 let mut schemas_to_update = Vec::new();
971 for (schema_name, schema) in &analysis.schemas {
972 if schema_name == inline_name {
974 continue;
975 }
976
977 if schema.dependencies.contains(inline_name) {
978 schemas_to_update.push(schema_name.clone());
980 }
981 }
982
983 for schema_name in schemas_to_update {
985 analysis
986 .dependencies
987 .add_dependency(schema_name, inline_name.clone());
988 }
989 }
990 }
991
992 self.analyze_operations(&mut analysis)?;
994
995 for (inline_name, inline_schema) in &self.resolved_cache {
998 if !analysis.schemas.contains_key(inline_name) {
999 analysis
1000 .schemas
1001 .insert(inline_name.clone(), inline_schema.clone());
1002
1003 for dep in &inline_schema.dependencies {
1005 analysis
1006 .dependencies
1007 .add_dependency(inline_name.clone(), dep.clone());
1008 }
1009 }
1010 }
1011
1012 analysis.used_type_features = self.type_mapper.used_features();
1016
1017 for (name, analyzed) in &analysis.schemas {
1022 let enum_value_count = match &analyzed.schema_type {
1023 SchemaType::StringEnum { values } => values.len(),
1024 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1025 _ => continue,
1026 };
1027 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1028 analysis.enum_extensions.insert(name.clone(), ext);
1029 }
1030 }
1031
1032 Ok(analysis)
1033 }
1034
1035 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1036 for (schema_name, schema) in &self.schemas {
1037 if self.is_discriminated_union(schema) {
1039 patterns.tagged_enum_schemas.insert(schema_name.clone());
1040
1041 if let Some(mappings) = self.extract_type_mappings(schema)? {
1043 patterns.type_mappings.insert(schema_name.clone(), mappings);
1044 }
1045 }
1046 else if self.is_simple_union(schema) {
1048 patterns.untagged_enum_schemas.insert(schema_name.clone());
1049 }
1050 }
1051
1052 Ok(())
1053 }
1054
1055 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1056 if schema.is_discriminated_union() {
1058 return true;
1059 }
1060
1061 if let Some(variants) = schema.union_variants() {
1063 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1064 }
1065
1066 false
1067 }
1068
1069 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1070 variants.iter().all(|variant| {
1071 if let Some(ref_str) = variant.reference() {
1072 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1074 if let Some(schema) = self.schemas.get(schema_name) {
1075 return self.has_const_discriminator_field(schema, field_name);
1076 }
1077 }
1078 } else {
1079 return self.has_const_discriminator_field(variant, field_name);
1081 }
1082 false
1083 })
1084 }
1085
1086 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1095 if let Some(ref_str) = schema.reference() {
1097 return match self
1098 .extract_schema_name(ref_str)
1099 .and_then(|n| self.schemas.get(n))
1100 {
1101 Some(target) => self.branch_resolves_to_object(target),
1102 None => false,
1103 };
1104 }
1105 if matches!(
1108 schema,
1109 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1110 ) {
1111 return true;
1112 }
1113 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1114 return true;
1115 }
1116 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1117 return true;
1118 }
1119 false
1122 }
1123
1124 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1128 if variants.is_empty() {
1129 return None;
1130 }
1131
1132 let first_variant = &variants[0];
1134 let first_schema = if let Some(ref_str) = first_variant.reference() {
1135 let schema_name = self.extract_schema_name(ref_str)?;
1136 self.schemas.get(schema_name)?
1137 } else {
1138 first_variant
1139 };
1140
1141 let properties = first_schema.details().properties.as_ref()?;
1142 let mut candidates: Vec<String> = Vec::new();
1143
1144 for (field_name, field_schema) in properties {
1145 let details = field_schema.details();
1146 let is_const = details.const_value.is_some()
1147 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1148 || details.extra.contains_key("const");
1149 if is_const {
1150 candidates.push(field_name.clone());
1151 }
1152 }
1153
1154 if candidates.is_empty() {
1155 return None;
1156 }
1157
1158 candidates.sort_by(|a, b| {
1160 if a == "type" {
1161 std::cmp::Ordering::Less
1162 } else if b == "type" {
1163 std::cmp::Ordering::Greater
1164 } else {
1165 a.cmp(b)
1166 }
1167 });
1168
1169 for candidate in &candidates {
1171 if self.all_variants_have_const_field(variants, candidate) {
1172 return Some(candidate.clone());
1173 }
1174 }
1175
1176 None
1177 }
1178
1179 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1180 if let Some(properties) = &schema.details().properties {
1181 if let Some(field) = properties.get(field_name) {
1182 if field.details().const_value.is_some() {
1184 return true;
1185 }
1186 if let Some(enum_vals) = &field.details().enum_values {
1188 return enum_vals.len() == 1;
1189 }
1190 return field.details().extra.contains_key("const");
1192 }
1193 }
1194 false
1195 }
1196
1197 fn is_simple_union(&self, schema: &Schema) -> bool {
1198 if let Some(variants) = schema.union_variants() {
1199 if variants.len() > 1 && !schema.is_nullable_pattern() {
1201 let has_refs = variants.iter().any(|v| v.is_reference());
1202 return has_refs;
1203 }
1204 }
1205 false
1206 }
1207
1208 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1209 let variants = schema.union_variants().ok_or_else(|| {
1210 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1211 })?;
1212
1213 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1215 discriminator.property_name.clone()
1216 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1217 detected
1218 } else {
1219 "type".to_string() };
1221
1222 let mut mappings = BTreeMap::new();
1223
1224 for variant in variants {
1225 if let Some(ref_str) = variant.reference() {
1226 if let Some(type_name) = self.extract_schema_name(ref_str) {
1227 if let Some(variant_schema) = self.schemas.get(type_name) {
1228 if let Some(discriminator_value) = self
1229 .extract_discriminator_value_for_field(
1230 variant_schema,
1231 &discriminator_field,
1232 )
1233 {
1234 mappings.insert(type_name.to_string(), discriminator_value);
1235 }
1236 }
1237 }
1238 }
1239 }
1240
1241 if mappings.is_empty() {
1242 Ok(None)
1243 } else {
1244 Ok(Some(mappings))
1245 }
1246 }
1247
1248 #[allow(dead_code)]
1249 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1250 self.extract_discriminator_value_for_field(schema, "type")
1251 }
1252
1253 fn extract_discriminator_value_for_field(
1254 &self,
1255 schema: &Schema,
1256 field_name: &str,
1257 ) -> Option<String> {
1258 if let Some(properties) = &schema.details().properties {
1259 if let Some(type_field) = properties.get(field_name) {
1260 if let Some(const_value) = &type_field.details().const_value {
1262 if let Some(value) = const_value.as_str() {
1263 return Some(value.to_string());
1264 }
1265 }
1266 if let Some(enum_values) = &type_field.details().enum_values {
1268 if enum_values.len() == 1 {
1269 return enum_values[0].as_str().map(|s| s.to_string());
1270 }
1271 }
1272 if let Some(const_value) = type_field.details().extra.get("const") {
1274 return const_value.as_str().map(|s| s.to_string());
1275 }
1276 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1278 if stainless_const.as_bool() == Some(true) {
1279 if let Some(default_value) = &type_field.details().default {
1280 if let Some(value) = default_value.as_str() {
1281 return Some(value.to_string());
1282 }
1283 }
1284 }
1285 }
1286 }
1287 }
1288 None
1289 }
1290
1291 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1292 schema.reference().or_else(|| schema.recursive_reference())
1293 }
1294
1295 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1296 if ref_str == "#" {
1297 return None; }
1299
1300 let parts: Vec<&str> = ref_str.split('/').collect();
1301
1302 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1304 return Some(parts[3]);
1305 }
1306
1307 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1310 return Some(parts[2]);
1311 }
1312
1313 let last = parts.last()?;
1319 if last.is_empty()
1320 || last.chars().all(|c| c.is_ascii_digit())
1321 || matches!(
1322 *last,
1323 "schema" | "properties" | "items" | "additionalProperties"
1324 )
1325 {
1326 return None;
1327 }
1328 let first = last.chars().next().unwrap_or(' ');
1329 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1330 return None;
1331 }
1332 Some(last)
1333 }
1334
1335 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1336 if let Some(cached) = self.resolved_cache.get(schema_name) {
1338 return Ok(cached.clone());
1339 }
1340
1341 self.current_schema_name = Some(schema_name.to_string());
1343
1344 let schema = self
1345 .schemas
1346 .get(schema_name)
1347 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1348 .clone();
1349
1350 self.resolved_cache.insert(
1352 schema_name.to_string(),
1353 AnalyzedSchema {
1354 name: schema_name.to_string(),
1355 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1356 schema_type: SchemaType::Reference {
1357 target: "placeholder".to_string(),
1358 },
1359 dependencies: HashSet::new(),
1360 nullable: false,
1361 description: None,
1362 default: None,
1363 },
1364 );
1365
1366 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1367
1368 self.resolved_cache
1370 .insert(schema_name.to_string(), analyzed.clone());
1371
1372 Ok(analyzed)
1373 }
1374
1375 fn analyze_schema_value(
1376 &mut self,
1377 schema: &Schema,
1378 schema_name: &str,
1379 ) -> Result<AnalyzedSchema> {
1380 let details = schema.details();
1381 let description = details.description.clone();
1382 let nullable = details.is_nullable() || schema.type_array_contains_null();
1384 let mut dependencies = HashSet::new();
1385
1386 let schema_type = match schema {
1387 Schema::Reference { reference, .. } => {
1388 match self.extract_schema_name(reference) {
1393 Some(name) => {
1394 let target = name.to_string();
1395 dependencies.insert(target.clone());
1396 SchemaType::Reference { target }
1397 }
1398 None => {
1399 eprintln!(
1400 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1401 reference
1402 );
1403 SchemaType::Primitive {
1404 rust_type: "serde_json::Value".to_string(),
1405 serde_with: None,
1406 }
1407 }
1408 }
1409 }
1410 Schema::RecursiveRef { recursive_ref, .. }
1411 | Schema::DynamicRef {
1412 dynamic_ref: recursive_ref,
1413 ..
1414 } => {
1415 if recursive_ref == "#" {
1421 dependencies.insert(schema_name.to_string());
1422 SchemaType::Reference {
1423 target: schema_name.to_string(),
1424 }
1425 } else {
1426 let target = self
1427 .extract_schema_name(recursive_ref)
1428 .unwrap_or(schema_name)
1429 .to_string();
1430 dependencies.insert(target.clone());
1431 SchemaType::Reference { target }
1432 }
1433 }
1434 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1435 let primary = schema
1436 .schema_type()
1437 .cloned()
1438 .unwrap_or(OpenApiSchemaType::Object);
1439 let format = details.format.as_deref();
1440 match primary {
1441 OpenApiSchemaType::String => {
1442 if let Some(values) = details.string_enum_values() {
1443 SchemaType::StringEnum { values }
1444 } else {
1445 SchemaType::Primitive {
1446 rust_type: self.type_mapper.string_format(format).rust_type,
1447 serde_with: None,
1448 }
1449 }
1450 }
1451 OpenApiSchemaType::Integer => SchemaType::Primitive {
1452 rust_type: self.type_mapper.integer_format(format).rust_type,
1453 serde_with: None,
1454 },
1455 OpenApiSchemaType::Number => SchemaType::Primitive {
1456 rust_type: self.type_mapper.number_format(format).rust_type,
1457 serde_with: None,
1458 },
1459 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1460 rust_type: self.type_mapper.boolean().rust_type,
1461 serde_with: None,
1462 },
1463 OpenApiSchemaType::Array => {
1464 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1466 }
1467 OpenApiSchemaType::Object => {
1468 if self.should_use_dynamic_json(schema) {
1470 SchemaType::Primitive {
1471 rust_type: self.type_mapper.dynamic_json().rust_type,
1472 serde_with: None,
1473 }
1474 } else {
1475 self.analyze_object_schema(schema, &mut dependencies)?
1477 }
1478 }
1479 _ => SchemaType::Primitive {
1480 rust_type: self.type_mapper.dynamic_json().rust_type,
1481 serde_with: None,
1482 },
1483 }
1484 }
1485 Schema::AnyOf {
1486 any_of,
1487 discriminator,
1488 ..
1489 } => {
1490 self.analyze_anyof_union(
1492 any_of,
1493 discriminator.as_ref(),
1494 &mut dependencies,
1495 schema_name,
1496 )?
1497 }
1498 Schema::OneOf {
1499 one_of,
1500 discriminator,
1501 ..
1502 } => {
1503 self.analyze_oneof_union(
1505 one_of,
1506 discriminator.as_ref(),
1507 schema_name,
1508 &mut dependencies,
1509 )?
1510 }
1511 Schema::AllOf { all_of, .. } => {
1512 self.analyze_allof_composition(all_of, &mut dependencies)?
1514 }
1515 Schema::Untyped { .. } => {
1516 if let Some(inferred) = schema.inferred_type() {
1518 match inferred {
1519 OpenApiSchemaType::Object => {
1520 if self.should_use_dynamic_json(schema) {
1521 SchemaType::Primitive {
1522 rust_type: "serde_json::Value".to_string(),
1523 serde_with: None,
1524 }
1525 } else {
1526 self.analyze_object_schema(schema, &mut dependencies)?
1527 }
1528 }
1529 OpenApiSchemaType::String if details.is_string_enum() => {
1530 SchemaType::StringEnum {
1531 values: details.string_enum_values().unwrap_or_default(),
1532 }
1533 }
1534 _ => SchemaType::Primitive {
1535 rust_type: "serde_json::Value".to_string(),
1536 serde_with: None,
1537 },
1538 }
1539 } else {
1540 SchemaType::Primitive {
1541 rust_type: "serde_json::Value".to_string(),
1542 serde_with: None,
1543 }
1544 }
1545 }
1546 };
1547
1548 Ok(AnalyzedSchema {
1549 name: schema_name.to_string(),
1550 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1552 dependencies,
1553 nullable,
1554 description,
1555 default: details.default.clone(),
1556 })
1557 }
1558
1559 fn analyze_object_schema(
1560 &mut self,
1561 schema: &Schema,
1562 dependencies: &mut HashSet<String>,
1563 ) -> Result<SchemaType> {
1564 let details = schema.details();
1565 let properties = &details.properties;
1566 let required = details
1567 .required
1568 .as_ref()
1569 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1570 .unwrap_or_default();
1571
1572 let mut property_info = BTreeMap::new();
1573
1574 if let Some(props) = properties {
1575 for (prop_name, prop_schema) in props {
1576 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1578 if self.should_use_dynamic_json(prop_schema) {
1580 SchemaType::Primitive {
1582 rust_type: "serde_json::Value".to_string(),
1583 serde_with: None,
1584 }
1585 } else if prop_schema.is_nullable_pattern()
1586 && let Some(non_null) = prop_schema.non_null_variant()
1587 {
1588 self.analyze_property_schema_with_context(
1596 non_null,
1597 Some(prop_name),
1598 dependencies,
1599 )?
1600 } else {
1601 let context_name = self
1604 .current_schema_name
1605 .clone()
1606 .unwrap_or_else(|| "Unknown".to_string());
1607
1608 let prop_pascal = self.to_pascal_case(prop_name);
1610 let mut union_type_name = format!("{context_name}{prop_pascal}");
1611
1612 if self.schemas.contains_key(&union_type_name)
1615 || self.resolved_cache.contains_key(&union_type_name)
1616 {
1617 let mut suffix = 2;
1618 loop {
1619 let candidate = format!("{union_type_name}Union{suffix}");
1620 if !self.schemas.contains_key(&candidate)
1621 && !self.resolved_cache.contains_key(&candidate)
1622 {
1623 union_type_name = candidate;
1624 break;
1625 }
1626 suffix += 1;
1627 if suffix > 1000 {
1628 break;
1629 }
1630 }
1631 }
1632
1633 let union_schema_type = self.analyze_anyof_union(
1635 any_of,
1636 prop_schema.discriminator(),
1637 dependencies,
1638 &union_type_name,
1639 )?;
1640
1641 self.resolved_cache.insert(
1643 union_type_name.clone(),
1644 AnalyzedSchema {
1645 name: union_type_name.clone(),
1646 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1647 schema_type: union_schema_type,
1648 dependencies: HashSet::new(),
1649 nullable: false,
1650 description: prop_schema.details().description.clone(),
1651 default: None,
1652 },
1653 );
1654
1655 dependencies.insert(union_type_name.clone());
1657 SchemaType::Reference {
1658 target: union_type_name,
1659 }
1660 }
1661 } else if let Schema::OneOf {
1662 one_of,
1663 discriminator,
1664 ..
1665 } = prop_schema
1666 {
1667 if prop_schema.is_nullable_pattern()
1674 && let Some(non_null) = prop_schema.non_null_variant()
1675 {
1676 let unwrapped = self.analyze_property_schema_with_context(
1677 non_null,
1678 Some(prop_name),
1679 dependencies,
1680 )?;
1681 let prop_details = prop_schema.details();
1682 let prop_nullable = true;
1683 let prop_description = prop_details.description.clone();
1684 let prop_default = prop_details.default.clone();
1685 property_info.insert(
1686 prop_name.clone(),
1687 PropertyInfo {
1688 schema_type: unwrapped,
1689 nullable: prop_nullable,
1690 description: prop_description,
1691 default: prop_default,
1692 serde_attrs: Vec::new(),
1693 constraints: PropertyConstraints::from_schema_details(prop_details),
1694 },
1695 );
1696 continue;
1697 }
1698
1699 let context_name = self
1701 .current_schema_name
1702 .clone()
1703 .unwrap_or_else(|| "Unknown".to_string());
1704 let prop_pascal = self.to_pascal_case(prop_name);
1705 let mut union_type_name = format!("{context_name}{prop_pascal}");
1706 if self.schemas.contains_key(&union_type_name)
1708 || self.resolved_cache.contains_key(&union_type_name)
1709 {
1710 let mut suffix = 2;
1711 loop {
1712 let candidate = format!("{union_type_name}Union{suffix}");
1713 if !self.schemas.contains_key(&candidate)
1714 && !self.resolved_cache.contains_key(&candidate)
1715 {
1716 union_type_name = candidate;
1717 break;
1718 }
1719 suffix += 1;
1720 if suffix > 1000 {
1721 break;
1722 }
1723 }
1724 }
1725
1726 let union_schema_type = self.analyze_oneof_union(
1728 one_of,
1729 discriminator.as_ref(),
1730 &union_type_name,
1731 dependencies,
1732 )?;
1733
1734 self.resolved_cache.insert(
1736 union_type_name.clone(),
1737 AnalyzedSchema {
1738 name: union_type_name.clone(),
1739 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1740 schema_type: union_schema_type,
1741 dependencies: HashSet::new(),
1742 nullable: false,
1743 description: prop_schema.details().description.clone(),
1744 default: None,
1745 },
1746 );
1747
1748 dependencies.insert(union_type_name.clone());
1750 SchemaType::Reference {
1751 target: union_type_name,
1752 }
1753 } else {
1754 self.analyze_property_schema_with_context(
1756 prop_schema,
1757 Some(prop_name),
1758 dependencies,
1759 )?
1760 };
1761
1762 let prop_details = prop_schema.details();
1763 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1765 let prop_description = prop_details.description.clone();
1766 let prop_default = prop_details.default.clone();
1767
1768 property_info.insert(
1769 prop_name.clone(),
1770 PropertyInfo {
1771 schema_type: prop_type,
1772 nullable: prop_nullable,
1773 description: prop_description,
1774 default: prop_default,
1775 serde_attrs: Vec::new(),
1776 constraints: PropertyConstraints::from_schema_details(prop_details),
1777 },
1778 );
1779 }
1780 }
1781
1782 let typed_enabled = self
1790 .type_mapper
1791 .config()
1792 .shape
1793 .as_ref()
1794 .and_then(|s| s.additional_properties_typed)
1795 .unwrap_or(true);
1796
1797 let additional_properties = match &details.additional_properties {
1798 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
1799 ObjectAdditionalProperties::Untyped
1800 }
1801 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
1802 ObjectAdditionalProperties::Forbidden
1803 }
1804 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
1805 let analyzed =
1806 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
1807 ObjectAdditionalProperties::Typed {
1808 value_type: Box::new(analyzed),
1809 }
1810 }
1811 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1812 ObjectAdditionalProperties::Untyped
1814 }
1815 None => ObjectAdditionalProperties::Forbidden,
1816 };
1817
1818 Ok(SchemaType::Object {
1819 properties: property_info,
1820 required,
1821 additional_properties,
1822 })
1823 }
1824
1825 fn analyze_property_schema_with_context(
1826 &mut self,
1827 schema: &Schema,
1828 property_name: Option<&str>,
1829 dependencies: &mut HashSet<String>,
1830 ) -> Result<SchemaType> {
1831 if let Some(ref_str) = self.get_any_reference(schema) {
1832 let target_opt = if ref_str == "#" {
1833 Some(
1834 self.find_recursive_anchor_schema()
1835 .unwrap_or_else(|| "UnknownRecursive".to_string()),
1836 )
1837 } else {
1838 self.extract_schema_name(ref_str).map(|s| s.to_string())
1839 };
1840 match target_opt {
1841 Some(target) => {
1842 dependencies.insert(target.clone());
1843 return Ok(SchemaType::Reference { target });
1844 }
1845 None => {
1846 eprintln!(
1847 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1848 ref_str
1849 );
1850 return Ok(SchemaType::Primitive {
1851 rust_type: "serde_json::Value".to_string(),
1852 serde_with: None,
1853 });
1854 }
1855 }
1856 }
1857
1858 if let Some(schema_type) = schema.schema_type() {
1859 match schema_type {
1860 OpenApiSchemaType::String => {
1861 if let Some(enum_values) = schema.details().string_enum_values() {
1863 let context_name = self
1866 .current_schema_name
1867 .clone()
1868 .unwrap_or_else(|| "Unknown".to_string());
1869
1870 let primary_name = if let Some(prop_name) = property_name {
1872 let prop_pascal = self.to_pascal_case(prop_name);
1874 format!("{context_name}{prop_pascal}")
1875 } else {
1876 let suffix = if !enum_values.is_empty() {
1879 let first_value = self.to_pascal_case(&enum_values[0]);
1880 format!("{first_value}Enum")
1881 } else {
1882 "StringEnum".to_string()
1883 };
1884 format!("{context_name}{suffix}")
1885 };
1886
1887 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1907 matches!(
1908 &existing.schema_type,
1909 SchemaType::StringEnum { values: existing_values }
1910 if existing_values == values
1911 )
1912 }
1913
1914 let mut enum_type_name = primary_name.clone();
1915 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1916 None => true,
1917 Some(existing) if matches_values(existing, &enum_values) => false,
1918 Some(_) => {
1919 let suffix = enum_values
1922 .first()
1923 .map(|v| self.to_pascal_case(v))
1924 .unwrap_or_else(|| "Variant".to_string());
1925 let candidate = format!("{primary_name}{suffix}");
1926
1927 let resolved = match self.resolved_cache.get(&candidate) {
1928 None => Some((candidate.clone(), true)),
1929 Some(existing) if matches_values(existing, &enum_values) => {
1930 Some((candidate.clone(), false))
1931 }
1932 Some(_) => {
1933 let mut found = None;
1936 for n in 2..1000 {
1937 let numbered = format!("{candidate}_{n}");
1938 match self.resolved_cache.get(&numbered) {
1939 None => {
1940 found = Some((numbered, true));
1941 break;
1942 }
1943 Some(existing)
1944 if matches_values(existing, &enum_values) =>
1945 {
1946 found = Some((numbered, false));
1947 break;
1948 }
1949 Some(_) => continue,
1950 }
1951 }
1952 found
1953 }
1954 };
1955
1956 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
1957 enum_type_name = resolved_name;
1958 insert
1959 }
1960 };
1961
1962 if should_insert {
1965 self.resolved_cache.insert(
1966 enum_type_name.clone(),
1967 AnalyzedSchema {
1968 name: enum_type_name.clone(),
1969 original: serde_json::to_value(schema).unwrap_or(Value::Null),
1970 schema_type: SchemaType::StringEnum {
1971 values: enum_values,
1972 },
1973 dependencies: HashSet::new(),
1974 nullable: false,
1975 description: schema.details().description.clone(),
1976 default: schema.details().default.clone(),
1977 },
1978 );
1979 let _ = &mut should_insert;
1982 }
1983
1984 dependencies.insert(enum_type_name.clone());
1986 return Ok(SchemaType::Reference {
1987 target: enum_type_name,
1988 });
1989 } else {
1990 let mapped = self
1996 .type_mapper
1997 .string_format(schema.details().format.as_deref());
1998 return Ok(SchemaType::Primitive {
1999 rust_type: mapped.rust_type,
2000 serde_with: mapped.serde_with,
2001 });
2002 }
2003 }
2004 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2005 let details = schema.details();
2006 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2007 return Ok(SchemaType::Primitive {
2008 rust_type,
2009 serde_with: None,
2010 });
2011 }
2012 OpenApiSchemaType::Boolean => {
2013 return Ok(SchemaType::Primitive {
2014 rust_type: "bool".to_string(),
2015 serde_with: None,
2016 });
2017 }
2018 OpenApiSchemaType::Array => {
2019 let context_name = if let Some(prop_name) = property_name {
2021 let prop_pascal = self.to_pascal_case(prop_name);
2023 format!(
2024 "{}{}",
2025 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2026 prop_pascal
2027 )
2028 } else {
2029 "ArrayItem".to_string()
2031 };
2032 return self.analyze_array_schema(schema, &context_name, dependencies);
2033 }
2034 OpenApiSchemaType::Object => {
2035 if self.should_use_dynamic_json(schema) {
2037 return Ok(SchemaType::Primitive {
2038 rust_type: "serde_json::Value".to_string(),
2039 serde_with: None,
2040 });
2041 }
2042 let object_type_name = if let Some(prop_name) = property_name {
2044 let prop_pascal = self.to_pascal_case(prop_name);
2046 format!(
2047 "{}{}",
2048 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2049 prop_pascal
2050 )
2051 } else {
2052 format!(
2054 "{}Object",
2055 self.current_schema_name.as_deref().unwrap_or("Unknown")
2056 )
2057 };
2058
2059 let object_type = self.analyze_object_schema(schema, dependencies)?;
2061
2062 let inline_schema = AnalyzedSchema {
2064 name: object_type_name.clone(),
2065 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2066 schema_type: object_type,
2067 dependencies: dependencies.clone(),
2068 nullable: false,
2069 description: schema.details().description.clone(),
2070 default: None,
2071 };
2072
2073 self.resolved_cache
2075 .insert(object_type_name.clone(), inline_schema);
2076 dependencies.insert(object_type_name.clone());
2077
2078 return Ok(SchemaType::Reference {
2080 target: object_type_name,
2081 });
2082 }
2083 _ => {
2084 return Ok(SchemaType::Primitive {
2085 rust_type: "serde_json::Value".to_string(),
2086 serde_with: None,
2087 });
2088 }
2089 }
2090 }
2091
2092 if schema.is_nullable_pattern() {
2094 if let Some(non_null) = schema.non_null_variant() {
2095 return self.analyze_property_schema_with_context(
2096 non_null,
2097 property_name,
2098 dependencies,
2099 );
2100 }
2101 }
2102
2103 if self.should_use_dynamic_json(schema) {
2105 return Ok(SchemaType::Primitive {
2106 rust_type: "serde_json::Value".to_string(),
2107 serde_with: None,
2108 });
2109 }
2110
2111 if let Schema::AllOf { all_of, .. } = schema {
2113 return self.analyze_allof_composition(all_of, dependencies);
2114 }
2115
2116 if let Some(variants) = schema.union_variants() {
2118 match variants.len().cmp(&1) {
2119 std::cmp::Ordering::Equal => {
2120 return self.analyze_property_schema_with_context(
2122 &variants[0],
2123 property_name,
2124 dependencies,
2125 );
2126 }
2127 std::cmp::Ordering::Greater => {
2128 let union_name = if let Some(prop_name) = property_name {
2131 let prop_pascal = self.to_pascal_case(prop_name);
2133 format!(
2134 "{}{}",
2135 self.current_schema_name.as_deref().unwrap_or(""),
2136 prop_pascal
2137 )
2138 } else {
2139 "UnionType".to_string()
2140 };
2141
2142 if let Schema::OneOf {
2144 one_of,
2145 discriminator,
2146 ..
2147 } = schema
2148 {
2149 let oneof_result = self.analyze_oneof_union(
2151 one_of,
2152 discriminator.as_ref(),
2153 &union_name,
2154 dependencies,
2155 )?;
2156
2157 if let SchemaType::Union {
2159 variants: _union_variants,
2160 } = &oneof_result
2161 {
2162 self.resolved_cache.insert(
2164 union_name.clone(),
2165 AnalyzedSchema {
2166 name: union_name.clone(),
2167 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2168 schema_type: oneof_result.clone(),
2169 dependencies: dependencies.clone(),
2170 nullable: false,
2171 description: schema.details().description.clone(),
2172 default: None,
2173 },
2174 );
2175
2176 dependencies.insert(union_name.clone());
2178 return Ok(SchemaType::Reference { target: union_name });
2179 }
2180
2181 return Ok(oneof_result);
2182 } else if let Schema::AnyOf {
2183 any_of,
2184 discriminator,
2185 ..
2186 } = schema
2187 {
2188 let union_analysis = self.analyze_anyof_union(
2190 any_of,
2191 discriminator.as_ref(),
2192 dependencies,
2193 &union_name,
2194 )?;
2195 return Ok(union_analysis);
2196 } else {
2197 let mut union_variants = Vec::new();
2200 for variant in variants {
2201 if let Some(ref_str) = variant.reference() {
2202 if let Some(target) = self.extract_schema_name(ref_str) {
2203 dependencies.insert(target.to_string());
2204 union_variants.push(SchemaRef {
2205 target: target.to_string(),
2206 nullable: false,
2207 });
2208 }
2209 }
2210 }
2211 return Ok(SchemaType::Union {
2212 variants: union_variants,
2213 });
2214 }
2215 }
2216 std::cmp::Ordering::Less => {}
2217 }
2218 }
2219
2220 if let Some(inferred_type) = schema.inferred_type() {
2222 match inferred_type {
2223 OpenApiSchemaType::Object => {
2224 if self.should_use_dynamic_json(schema) {
2226 return Ok(SchemaType::Primitive {
2227 rust_type: "serde_json::Value".to_string(),
2228 serde_with: None,
2229 });
2230 }
2231 return self.analyze_object_schema(schema, dependencies);
2232 }
2233 OpenApiSchemaType::Array => {
2234 let context_name = if let Some(prop_name) = property_name {
2235 let prop_pascal = self.to_pascal_case(prop_name);
2237 format!(
2238 "{}{}",
2239 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2240 prop_pascal
2241 )
2242 } else {
2243 "ArrayItem".to_string()
2245 };
2246 return self.analyze_array_schema(schema, &context_name, dependencies);
2247 }
2248 OpenApiSchemaType::String => {
2249 if let Some(enum_values) = schema.details().string_enum_values() {
2250 return Ok(SchemaType::StringEnum {
2251 values: enum_values,
2252 });
2253 } else {
2254 return Ok(SchemaType::Primitive {
2255 rust_type: "String".to_string(),
2256 serde_with: None,
2257 });
2258 }
2259 }
2260 _ => {
2261 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2263 return Ok(SchemaType::Primitive {
2264 rust_type,
2265 serde_with: None,
2266 });
2267 }
2268 }
2269 }
2270
2271 Ok(SchemaType::Primitive {
2272 rust_type: "serde_json::Value".to_string(),
2273 serde_with: None,
2274 })
2275 }
2276
2277 fn analyze_allof_composition(
2278 &mut self,
2279 all_of_schemas: &[Schema],
2280 dependencies: &mut HashSet<String>,
2281 ) -> Result<SchemaType> {
2282 if all_of_schemas.len() == 1 {
2285 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
2286 if let Some(target) = self.extract_schema_name(reference) {
2287 dependencies.insert(target.to_string());
2288 return Ok(SchemaType::Reference {
2289 target: target.to_string(),
2290 });
2291 }
2292 }
2293 }
2294
2295 let mut merged_properties = BTreeMap::new();
2297 let mut merged_required = HashSet::new();
2298 let mut descriptions = Vec::new();
2299
2300 let current_context = self.current_schema_name.clone();
2302
2303 for schema in all_of_schemas {
2304 match schema {
2305 Schema::Reference { reference, .. } => {
2306 if let Some(target) = self.extract_schema_name(reference) {
2308 dependencies.insert(target.to_string());
2309
2310 let analyzed_ref = self.analyze_schema(target)?;
2312
2313 match &analyzed_ref.schema_type {
2315 SchemaType::Object {
2316 properties,
2317 required,
2318 ..
2319 } => {
2320 for (prop_name, prop_info) in properties {
2322 merged_properties.insert(prop_name.clone(), prop_info.clone());
2323 }
2324 for req in required {
2326 merged_required.insert(req.clone());
2327 }
2328 }
2329 _ => {
2330 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2332 self.merge_schema_into_properties(
2333 &ref_schema,
2334 &mut merged_properties,
2335 &mut merged_required,
2336 dependencies,
2337 )?;
2338 }
2339 }
2340 }
2341 }
2342 }
2343 Schema::Typed {
2344 schema_type: OpenApiSchemaType::Object,
2345 ..
2346 }
2347 | Schema::Untyped { .. } => {
2348 let saved_context = self.current_schema_name.clone();
2350 self.current_schema_name = current_context.clone();
2351
2352 self.merge_schema_into_properties(
2354 schema,
2355 &mut merged_properties,
2356 &mut merged_required,
2357 dependencies,
2358 )?;
2359
2360 self.current_schema_name = saved_context;
2362 }
2363 _ => {
2364 self.merge_schema_into_properties(
2367 schema,
2368 &mut merged_properties,
2369 &mut merged_required,
2370 dependencies,
2371 )?;
2372 }
2373 }
2374
2375 if let Some(desc) = &schema.details().description {
2377 descriptions.push(desc.clone());
2378 }
2379 }
2380
2381 if !merged_properties.is_empty() {
2383 Ok(SchemaType::Object {
2384 properties: merged_properties,
2385 required: merged_required,
2386 additional_properties: ObjectAdditionalProperties::Forbidden,
2387 })
2388 } else {
2389 Ok(SchemaType::Composition {
2391 schemas: all_of_schemas
2392 .iter()
2393 .filter_map(|s| {
2394 if let Some(ref_str) = s.reference() {
2395 if let Some(target) = self.extract_schema_name(ref_str) {
2396 dependencies.insert(target.to_string());
2397 Some(SchemaRef {
2398 target: target.to_string(),
2399 nullable: false,
2400 })
2401 } else {
2402 None
2403 }
2404 } else {
2405 None
2406 }
2407 })
2408 .collect(),
2409 })
2410 }
2411 }
2412
2413 fn merge_schema_into_properties(
2414 &mut self,
2415 schema: &Schema,
2416 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2417 merged_required: &mut HashSet<String>,
2418 dependencies: &mut HashSet<String>,
2419 ) -> Result<()> {
2420 let details = schema.details();
2421
2422 if let Some(properties) = &details.properties {
2424 for (prop_name, prop_schema) in properties {
2425 let prop_type = self.analyze_property_schema_with_context(
2426 prop_schema,
2427 Some(prop_name),
2428 dependencies,
2429 )?;
2430 let prop_details = prop_schema.details();
2431
2432 let nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
2438 merged_properties.insert(
2439 prop_name.clone(),
2440 PropertyInfo {
2441 schema_type: prop_type,
2442 nullable,
2443 description: prop_details.description.clone(),
2444 default: prop_details.default.clone(),
2445 serde_attrs: Vec::new(),
2446 constraints: PropertyConstraints::from_schema_details(prop_details),
2447 },
2448 );
2449 }
2450 }
2451
2452 if let Some(required) = &details.required {
2454 for field in required {
2455 merged_required.insert(field.clone());
2456 }
2457 }
2458
2459 Ok(())
2460 }
2461
2462 fn analyze_oneof_union(
2463 &mut self,
2464 one_of_schemas: &[Schema],
2465 discriminator: Option<&crate::openapi::Discriminator>,
2466 parent_name: &str,
2467 dependencies: &mut HashSet<String>,
2468 ) -> Result<SchemaType> {
2469 if one_of_schemas.len() == 2 {
2472 let null_count = one_of_schemas
2473 .iter()
2474 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2475 .count();
2476 if null_count == 1 {
2477 if let Some(non_null) = one_of_schemas
2478 .iter()
2479 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2480 {
2481 return self
2482 .analyze_schema_value(non_null, parent_name)
2483 .map(|a| a.schema_type);
2484 }
2485 }
2486 }
2487
2488 if discriminator.is_none() {
2490 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2492 }
2493
2494 if one_of_schemas
2500 .iter()
2501 .any(|s| !self.branch_resolves_to_object(s))
2502 {
2503 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2504 }
2505
2506 let discriminator_field = discriminator
2508 .ok_or_else(|| {
2509 GeneratorError::InvalidDiscriminator(
2510 "expected discriminator after guard check".to_string(),
2511 )
2512 })?
2513 .property_name
2514 .clone();
2515
2516 let mut variants = Vec::new();
2517 let mut used_variant_names = std::collections::HashSet::new();
2518
2519 for variant_schema in one_of_schemas {
2520 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2522 Some((ref_str, false))
2523 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2524 Some((recursive_ref, true))
2525 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2526 if all_of.len() == 1 {
2528 if let Some(ref_str) = all_of[0].reference() {
2529 Some((ref_str, false))
2530 } else {
2531 all_of[0]
2532 .recursive_reference()
2533 .map(|recursive_ref| (recursive_ref, true))
2534 }
2535 } else {
2536 None
2537 }
2538 } else {
2539 None
2540 };
2541
2542 if let Some((ref_str, is_recursive)) = ref_info {
2543 let schema_name = if is_recursive && ref_str == "#" {
2544 self.find_recursive_anchor_schema()
2546 .or_else(|| self.current_schema_name.clone())
2547 .unwrap_or_else(|| "CompoundFilter".to_string())
2548 } else {
2549 self.extract_schema_name(ref_str)
2550 .map(|s| s.to_string())
2551 .unwrap_or_else(|| "UnknownRef".to_string())
2552 };
2553
2554 if !schema_name.is_empty() {
2555 dependencies.insert(schema_name.clone());
2556
2557 let discriminator_value = if let Some(disc) = discriminator {
2562 if let Some(mappings) = &disc.mapping {
2563 mappings
2566 .iter()
2567 .find(|(_, target_ref)| {
2568 target_ref.as_str() == ref_str
2570 || self
2571 .extract_schema_name(target_ref)
2572 .map(|s| s.to_string())
2573 == Some(schema_name.clone())
2574 })
2575 .map(|(key, _)| key.clone())
2576 .unwrap_or_else(|| {
2577 self.fallback_discriminator_value_for_field(
2578 &schema_name,
2579 &discriminator_field,
2580 )
2581 })
2582 } else {
2583 self.fallback_discriminator_value_for_field(
2584 &schema_name,
2585 &discriminator_field,
2586 )
2587 }
2588 } else {
2589 self.fallback_discriminator_value_for_field(
2590 &schema_name,
2591 &discriminator_field,
2592 )
2593 };
2594
2595 let base_name = self.to_rust_variant_name(&schema_name);
2597 let rust_name =
2598 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2599
2600 let final_discriminator_value = discriminator_value;
2602
2603 variants.push(UnionVariant {
2604 rust_name,
2605 type_name: schema_name,
2606 discriminator_value: final_discriminator_value,
2607 schema_ref: ref_str.to_string(),
2608 });
2609 }
2610 } else {
2611 let variant_index = variants.len();
2613 let inline_type_name =
2614 self.generate_inline_type_name(variant_schema, variant_index);
2615
2616 let discriminator_value = if let Some(disc) = discriminator {
2618 if let Some(mappings) = &disc.mapping {
2619 mappings
2621 .iter()
2622 .find(|(_, target_ref)| {
2623 target_ref.contains(&format!("variant_{variant_index}"))
2624 })
2625 .map(|(key, _)| key.clone())
2626 .unwrap_or_else(|| {
2627 self.extract_inline_discriminator_value(
2628 variant_schema,
2629 &discriminator_field,
2630 variant_index,
2631 )
2632 })
2633 } else {
2634 self.extract_inline_discriminator_value(
2635 variant_schema,
2636 &discriminator_field,
2637 variant_index,
2638 )
2639 }
2640 } else {
2641 self.extract_inline_discriminator_value(
2642 variant_schema,
2643 &discriminator_field,
2644 variant_index,
2645 )
2646 };
2647
2648 let base_name = if discriminator_value.starts_with("variant_") {
2650 format!("Variant{variant_index}")
2651 } else {
2652 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2654 self.to_rust_variant_name(&clean_name)
2655 };
2656 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2657
2658 let final_discriminator_value = discriminator_value;
2660
2661 variants.push(UnionVariant {
2662 rust_name,
2663 type_name: inline_type_name.clone(),
2664 discriminator_value: final_discriminator_value,
2665 schema_ref: format!("inline_{variant_index}"),
2666 });
2667
2668 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2670 }
2671 }
2672
2673 if variants.is_empty() {
2674 let mut union_variants = Vec::new();
2677
2678 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2679 if let Some(ref_str) = variant_schema.reference() {
2681 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2682 dependencies.insert(schema_name.to_string());
2683 union_variants.push(SchemaRef {
2684 target: schema_name.to_string(),
2685 nullable: false,
2686 });
2687 }
2688 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2689 let schema_name = if recursive_ref == "#" {
2690 self.find_recursive_anchor_schema()
2692 .or_else(|| self.current_schema_name.clone())
2693 .unwrap_or_else(|| "CompoundFilter".to_string())
2694 } else {
2695 self.extract_schema_name(recursive_ref)
2696 .map(|s| s.to_string())
2697 .unwrap_or_else(|| "RecursiveType".to_string())
2698 };
2699 dependencies.insert(schema_name.clone());
2700 union_variants.push(SchemaRef {
2701 target: schema_name,
2702 nullable: false,
2703 });
2704 } else {
2705 let inline_name = self.generate_context_aware_name(
2707 parent_name,
2708 "InlineVariant",
2709 variant_index,
2710 Some(variant_schema),
2711 );
2712 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2713 let variant_type = analyzed.schema_type;
2714
2715 for dep in &analyzed.dependencies {
2717 dependencies.insert(dep.clone());
2718 }
2719
2720 match &variant_type {
2721 SchemaType::Primitive { rust_type, .. } => {
2723 union_variants.push(SchemaRef {
2724 target: rust_type.clone(),
2725 nullable: false,
2726 });
2727 }
2728 SchemaType::Array { item_type } => {
2730 match item_type.as_ref() {
2731 SchemaType::Primitive { rust_type, .. } => {
2732 let type_name = format!("Vec<{rust_type}>");
2733 union_variants.push(SchemaRef {
2734 target: type_name,
2735 nullable: false,
2736 });
2737 }
2738 SchemaType::Reference { target } => {
2739 let type_name = format!("Vec<{target}>");
2740 union_variants.push(SchemaRef {
2741 target: type_name,
2742 nullable: false,
2743 });
2744 }
2745 _ => {
2746 let inline_type_name = self.generate_context_aware_name(
2748 parent_name,
2749 "Variant",
2750 variant_index,
2751 None,
2752 );
2753 self.add_inline_schema(
2754 &inline_type_name,
2755 variant_schema,
2756 dependencies,
2757 )?;
2758 union_variants.push(SchemaRef {
2759 target: inline_type_name,
2760 nullable: false,
2761 });
2762 }
2763 }
2764 }
2765 SchemaType::Reference { target } => {
2767 union_variants.push(SchemaRef {
2768 target: target.clone(),
2769 nullable: false,
2770 });
2771 }
2772 _ => {
2774 let inline_type_name =
2775 format!("{}Variant{}", parent_name, variant_index + 1);
2776 self.add_inline_schema(
2777 &inline_type_name,
2778 variant_schema,
2779 dependencies,
2780 )?;
2781 union_variants.push(SchemaRef {
2782 target: inline_type_name,
2783 nullable: false,
2784 });
2785 }
2786 }
2787 }
2788 }
2789
2790 if !union_variants.is_empty() {
2791 return Ok(SchemaType::Union {
2792 variants: union_variants,
2793 });
2794 }
2795
2796 return Ok(SchemaType::Primitive {
2798 rust_type: "serde_json::Value".to_string(),
2799 serde_with: None,
2800 });
2801 }
2802
2803 Ok(SchemaType::DiscriminatedUnion {
2804 discriminator_field,
2805 variants,
2806 })
2807 }
2808
2809 fn analyze_untagged_oneof_union(
2810 &mut self,
2811 one_of_schemas: &[Schema],
2812 parent_name: &str,
2813 dependencies: &mut HashSet<String>,
2814 ) -> Result<SchemaType> {
2815 let filtered: Vec<&Schema> = one_of_schemas
2819 .iter()
2820 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2821 .collect();
2822
2823 if filtered.len() == 1 {
2825 return self
2826 .analyze_schema_value(filtered[0], parent_name)
2827 .map(|a| a.schema_type);
2828 }
2829
2830 let mut union_variants = Vec::new();
2831
2832 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2833 if let Some(ref_str) = variant_schema.reference() {
2835 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2836 dependencies.insert(schema_name.to_string());
2837 union_variants.push(SchemaRef {
2838 target: schema_name.to_string(),
2839 nullable: false,
2840 });
2841 }
2842 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2843 let schema_name = if recursive_ref == "#" {
2844 self.find_recursive_anchor_schema()
2846 .or_else(|| self.current_schema_name.clone())
2847 .unwrap_or_else(|| "CompoundFilter".to_string())
2848 } else {
2849 self.extract_schema_name(recursive_ref)
2850 .map(|s| s.to_string())
2851 .unwrap_or_else(|| "RecursiveType".to_string())
2852 };
2853 dependencies.insert(schema_name.clone());
2854 union_variants.push(SchemaRef {
2855 target: schema_name,
2856 nullable: false,
2857 });
2858 } else {
2859 let inline_name = self.generate_context_aware_name(
2861 parent_name,
2862 "InlineVariant",
2863 variant_index,
2864 Some(variant_schema),
2865 );
2866 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2867 let variant_type = analyzed.schema_type;
2868
2869 for dep in &analyzed.dependencies {
2871 dependencies.insert(dep.clone());
2872 }
2873
2874 match &variant_type {
2875 SchemaType::Primitive { rust_type, .. } => {
2877 union_variants.push(SchemaRef {
2878 target: rust_type.clone(),
2879 nullable: false,
2880 });
2881 }
2882 SchemaType::Array { item_type } => {
2884 match item_type.as_ref() {
2885 SchemaType::Primitive { rust_type, .. } => {
2886 let type_name = format!("Vec<{rust_type}>");
2887 union_variants.push(SchemaRef {
2888 target: type_name,
2889 nullable: false,
2890 });
2891 }
2892 SchemaType::Reference { target } => {
2893 let type_name = format!("Vec<{target}>");
2894 union_variants.push(SchemaRef {
2895 target: type_name,
2896 nullable: false,
2897 });
2898 }
2899 SchemaType::Array {
2901 item_type: inner_item_type,
2902 } => {
2903 match inner_item_type.as_ref() {
2904 SchemaType::Primitive { rust_type, .. } => {
2905 let type_name = format!("Vec<Vec<{rust_type}>>");
2906 union_variants.push(SchemaRef {
2907 target: type_name,
2908 nullable: false,
2909 });
2910 }
2911 SchemaType::Reference { target } => {
2912 let type_name = format!("Vec<Vec<{target}>>");
2913 union_variants.push(SchemaRef {
2914 target: type_name,
2915 nullable: false,
2916 });
2917 }
2918 _ => {
2919 let inline_type_name = self.generate_context_aware_name(
2921 parent_name,
2922 "Variant",
2923 variant_index,
2924 None,
2925 );
2926 self.add_inline_schema(
2927 &inline_type_name,
2928 variant_schema,
2929 dependencies,
2930 )?;
2931 union_variants.push(SchemaRef {
2932 target: inline_type_name,
2933 nullable: false,
2934 });
2935 }
2936 }
2937 }
2938 _ => {
2939 let inline_type_name = self.generate_context_aware_name(
2941 parent_name,
2942 "Variant",
2943 variant_index,
2944 None,
2945 );
2946 self.add_inline_schema(
2947 &inline_type_name,
2948 variant_schema,
2949 dependencies,
2950 )?;
2951 union_variants.push(SchemaRef {
2952 target: inline_type_name,
2953 nullable: false,
2954 });
2955 }
2956 }
2957 }
2958 SchemaType::Reference { target } => {
2960 union_variants.push(SchemaRef {
2961 target: target.clone(),
2962 nullable: false,
2963 });
2964 }
2965 _ => {
2967 let inline_type_name = self.generate_context_aware_name(
2968 parent_name,
2969 "Variant",
2970 variant_index,
2971 None,
2972 );
2973 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2974 union_variants.push(SchemaRef {
2975 target: inline_type_name,
2976 nullable: false,
2977 });
2978 }
2979 }
2980 }
2981 }
2982
2983 if !union_variants.is_empty() {
2984 return Ok(SchemaType::Union {
2985 variants: union_variants,
2986 });
2987 }
2988
2989 Ok(SchemaType::Primitive {
2991 rust_type: "serde_json::Value".to_string(),
2992 serde_with: None,
2993 })
2994 }
2995
2996 fn add_inline_schema(
2997 &mut self,
2998 type_name: &str,
2999 schema: &Schema,
3000 dependencies: &mut HashSet<String>,
3001 ) -> Result<()> {
3002 if let Some(schema_type) = schema.schema_type() {
3004 match schema_type {
3005 OpenApiSchemaType::String
3006 | OpenApiSchemaType::Integer
3007 | OpenApiSchemaType::Number
3008 | OpenApiSchemaType::Boolean => {
3009 let rust_type =
3010 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3011
3012 self.resolved_cache.insert(
3014 type_name.to_string(),
3015 AnalyzedSchema {
3016 name: type_name.to_string(),
3017 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3018 schema_type: SchemaType::Primitive {
3019 rust_type,
3020 serde_with: None,
3021 },
3022 dependencies: HashSet::new(),
3023 nullable: false,
3024 description: schema.details().description.clone(),
3025 default: None,
3026 },
3027 );
3028 return Ok(());
3029 }
3030 _ => {}
3031 }
3032 }
3033
3034 let previous_schema_name = self.current_schema_name.take();
3038 self.current_schema_name = Some(type_name.to_string());
3039 let analyzed = self.analyze_schema_value(schema, type_name)?;
3040 self.current_schema_name = previous_schema_name;
3041
3042 self.resolved_cache.insert(type_name.to_string(), analyzed);
3044
3045 if let Some(cached) = self.resolved_cache.get(type_name) {
3047 for dep in &cached.dependencies {
3048 dependencies.insert(dep.clone());
3049 }
3050 }
3051
3052 Ok(())
3053 }
3054
3055 fn extract_inline_discriminator_value(
3056 &self,
3057 schema: &Schema,
3058 discriminator_field: &str,
3059 variant_index: usize,
3060 ) -> String {
3061 if let Some(properties) = &schema.details().properties {
3063 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3064 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3066 if enum_values.len() == 1 {
3067 if let Some(value) = enum_values[0].as_str() {
3068 return value.to_string();
3069 }
3070 }
3071 }
3072 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3074 if let Some(value) = const_value.as_str() {
3075 return value.to_string();
3076 }
3077 }
3078 if let Some(const_value) = &discriminator_prop.details().const_value {
3080 if let Some(value) = const_value.as_str() {
3081 return value.to_string();
3082 }
3083 }
3084 }
3085 }
3086
3087 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3089 return inferred_name;
3090 }
3091
3092 format!("variant_{variant_index}")
3094 }
3095
3096 fn infer_variant_name_from_structure(
3097 &self,
3098 schema: &Schema,
3099 _variant_index: usize,
3100 ) -> Option<String> {
3101 let details = schema.details();
3102
3103 if let Some(properties) = &details.properties {
3105 if properties.contains_key("text") && properties.len() <= 3 {
3107 return Some("text".to_string());
3108 }
3109 if properties.contains_key("image") || properties.contains_key("source") {
3110 return Some("image".to_string());
3111 }
3112 if properties.contains_key("document") {
3113 return Some("document".to_string());
3114 }
3115 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3116 return Some("tool_result".to_string());
3117 }
3118 if properties.contains_key("content") && properties.contains_key("is_error") {
3119 return Some("tool_result".to_string());
3120 }
3121 if properties.contains_key("partial_json") {
3122 return Some("partial_json".to_string());
3123 }
3124
3125 let property_names: Vec<&String> = properties.keys().collect();
3127
3128 for prop_name in &property_names {
3130 if prop_name.contains("result") {
3131 return Some("result".to_string());
3132 }
3133 if prop_name.contains("error") {
3134 return Some("error".to_string());
3135 }
3136 if prop_name.contains("content") && property_names.len() <= 2 {
3137 return Some("content".to_string());
3138 }
3139 }
3140
3141 let significant_props = property_names
3143 .iter()
3144 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3145 .collect::<Vec<_>>();
3146
3147 if significant_props.len() == 1 {
3148 return Some((*significant_props[0]).clone());
3149 }
3150 }
3151
3152 if let Some(description) = &details.description {
3154 let desc_lower = description.to_lowercase();
3155 if desc_lower.contains("text") && desc_lower.len() < 100 {
3156 return Some("text".to_string());
3157 }
3158 if desc_lower.contains("image") {
3159 return Some("image".to_string());
3160 }
3161 if desc_lower.contains("document") {
3162 return Some("document".to_string());
3163 }
3164 if desc_lower.contains("tool") && desc_lower.contains("result") {
3165 return Some("tool_result".to_string());
3166 }
3167 }
3168
3169 None
3170 }
3171
3172 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3173 if discriminator.is_empty() {
3175 return "Variant".to_string();
3176 }
3177
3178 let mut result = String::new();
3179 let mut next_upper = true;
3180
3181 for c in discriminator.chars() {
3182 match c {
3183 'a'..='z' => {
3184 if next_upper {
3185 result.push(c.to_ascii_uppercase());
3186 next_upper = false;
3187 } else {
3188 result.push(c);
3189 }
3190 }
3191 'A'..='Z' => {
3192 result.push(c);
3193 next_upper = false;
3194 }
3195 '0'..='9' => {
3196 result.push(c);
3197 next_upper = false;
3198 }
3199 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3200 next_upper = true;
3202 }
3203 _ => {
3204 next_upper = true;
3206 }
3207 }
3208 }
3209
3210 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3212 result = format!("Variant{result}");
3213 }
3214
3215 result
3216 }
3217
3218 fn ensure_unique_variant_name(
3219 &self,
3220 base_name: String,
3221 used_names: &mut std::collections::HashSet<String>,
3222 ) -> String {
3223 let mut candidate = base_name.clone();
3224 let mut counter = 1;
3225
3226 while used_names.contains(&candidate) {
3227 counter += 1;
3228 candidate = format!("{base_name}{counter}");
3229 }
3230
3231 used_names.insert(candidate.clone());
3232 candidate
3233 }
3234
3235 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3236 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3238 return meaningful_name;
3239 }
3240
3241 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3243 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3244 }
3245
3246 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3247 let details = schema.details();
3248
3249 if let Some(description) = &details.description {
3251 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3252 return Some(name_from_desc);
3253 }
3254 }
3255
3256 if let Some(properties) = &details.properties {
3258 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3259 return Some(format!("{name_from_props}Block"));
3260 }
3261 }
3262
3263 None
3264 }
3265
3266 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3267 if description.len() > 100 || description.contains('\n') {
3269 return None;
3270 }
3271
3272 let words: Vec<&str> = description
3274 .split_whitespace()
3275 .take(2) .filter(|word| {
3277 let w = word.to_lowercase();
3278 word.len() > 2
3279 && ![
3280 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3281 ]
3282 .contains(&w.as_str())
3283 })
3284 .collect();
3285
3286 if words.is_empty() {
3287 return None;
3288 }
3289
3290 let combined = words.join("_");
3292 let pascal_name = self.discriminator_to_variant_name(&combined);
3293
3294 if !pascal_name.ends_with("Content")
3296 && !pascal_name.ends_with("Block")
3297 && !pascal_name.ends_with("Type")
3298 {
3299 Some(format!("{pascal_name}Content"))
3300 } else {
3301 Some(pascal_name)
3302 }
3303 }
3304
3305 fn extract_type_name_from_properties(
3306 &self,
3307 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3308 ) -> Option<String> {
3309 let significant_props: Vec<&String> = properties
3311 .keys()
3312 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3313 .collect();
3314
3315 if significant_props.is_empty() {
3316 return None;
3317 }
3318
3319 if significant_props.len() == 1 {
3321 let prop_name = significant_props[0];
3322 return Some(self.discriminator_to_variant_name(prop_name));
3323 }
3324
3325 let mut sorted_props = significant_props.clone();
3328 sorted_props.sort();
3329 if let Some(first_prop) = sorted_props.first() {
3330 return Some(self.discriminator_to_variant_name(first_prop));
3331 }
3332
3333 None
3334 }
3335
3336 fn openapi_type_to_rust_type(
3337 &self,
3338 openapi_type: OpenApiSchemaType,
3339 details: &crate::openapi::SchemaDetails,
3340 ) -> String {
3341 self.type_mapper.map(openapi_type, details).rust_type
3346 }
3347
3348 #[allow(dead_code)]
3349 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3350 self.fallback_discriminator_value_for_field(schema_name, "type")
3351 }
3352
3353 fn fallback_discriminator_value_for_field(
3354 &self,
3355 schema_name: &str,
3356 field_name: &str,
3357 ) -> String {
3358 if let Some(ref_schema) = self.schemas.get(schema_name) {
3360 if let Some(extracted) =
3361 self.extract_discriminator_value_for_field(ref_schema, field_name)
3362 {
3363 return extracted;
3364 }
3365 }
3366
3367 self.generate_discriminator_value_from_name(schema_name)
3369 }
3370
3371 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3372 let mut result = String::new();
3374 let mut chars = schema_name.chars().peekable();
3375 let mut first = true;
3376
3377 while let Some(c) = chars.next() {
3378 if c.is_uppercase()
3379 && !first
3380 && chars
3381 .peek()
3382 .map(|&next| next.is_lowercase())
3383 .unwrap_or(false)
3384 {
3385 result.push('.');
3386 }
3387 result.push(c.to_ascii_lowercase());
3388 first = false;
3389 }
3390
3391 if result.ends_with("event") {
3393 result = result[..result.len() - 5].to_string();
3394 }
3395
3396 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3398 result = format!("response.{}", result.trim_start_matches("response"));
3399 }
3400
3401 result
3402 }
3403
3404 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3405 let mut name = schema_name;
3407
3408 if name.starts_with("Response") && name.len() > 8 {
3410 name = &name[8..]; }
3412
3413 if name.ends_with("Event") && name.len() > 5 {
3415 name = &name[..name.len() - 5]; }
3417
3418 name = name.trim_matches('_');
3420
3421 if name.is_empty() {
3423 schema_name.to_string()
3424 } else {
3425 self.discriminator_to_variant_name(name)
3427 }
3428 }
3429
3430 fn analyze_array_schema(
3431 &mut self,
3432 schema: &Schema,
3433 parent_schema_name: &str,
3434 dependencies: &mut HashSet<String>,
3435 ) -> Result<SchemaType> {
3436 let details = schema.details();
3437
3438 if let Some(items_schema) = &details.items {
3440 let item_type = match items_schema.as_ref() {
3442 Schema::Reference { reference, .. } => {
3443 let target = self
3445 .extract_schema_name(reference)
3446 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3447 .to_string();
3448 dependencies.insert(target.clone());
3449 SchemaType::Reference { target }
3450 }
3451 Schema::RecursiveRef { recursive_ref, .. } => {
3452 if recursive_ref == "#" {
3454 let target = self
3456 .find_recursive_anchor_schema()
3457 .unwrap_or_else(|| parent_schema_name.to_string());
3458 dependencies.insert(target.clone());
3459 SchemaType::Reference { target }
3460 } else {
3461 let target = self
3462 .extract_schema_name(recursive_ref)
3463 .unwrap_or("RecursiveType")
3464 .to_string();
3465 dependencies.insert(target.clone());
3466 SchemaType::Reference { target }
3467 }
3468 }
3469 Schema::Typed { schema_type, .. } => {
3470 match schema_type {
3472 OpenApiSchemaType::String => SchemaType::Primitive {
3473 rust_type: "String".to_string(),
3474 serde_with: None,
3475 },
3476 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3477 let details = items_schema.details();
3478 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3479 SchemaType::Primitive {
3480 rust_type,
3481 serde_with: None,
3482 }
3483 }
3484 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3485 rust_type: "bool".to_string(),
3486 serde_with: None,
3487 },
3488 OpenApiSchemaType::Object => {
3489 let object_type_name = format!("{parent_schema_name}Item");
3491
3492 let object_type =
3494 self.analyze_object_schema(items_schema, dependencies)?;
3495
3496 let inline_schema = AnalyzedSchema {
3498 name: object_type_name.clone(),
3499 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3500 schema_type: object_type,
3501 dependencies: dependencies.clone(),
3502 nullable: false,
3503 description: items_schema.details().description.clone(),
3504 default: None,
3505 };
3506
3507 self.resolved_cache
3509 .insert(object_type_name.clone(), inline_schema);
3510 dependencies.insert(object_type_name.clone());
3511
3512 SchemaType::Reference {
3514 target: object_type_name,
3515 }
3516 }
3517 OpenApiSchemaType::Array => {
3518 self.analyze_array_schema(
3520 items_schema,
3521 parent_schema_name,
3522 dependencies,
3523 )?
3524 }
3525 _ => SchemaType::Primitive {
3526 rust_type: "serde_json::Value".to_string(),
3527 serde_with: None,
3528 },
3529 }
3530 }
3531 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3532 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3534
3535 match &analyzed.schema_type {
3537 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3538 let union_name = format!("{parent_schema_name}ItemUnion");
3541
3542 let mut union_schema = analyzed;
3544 union_schema.name = union_name.clone();
3545
3546 self.resolved_cache.insert(union_name.clone(), union_schema);
3548
3549 dependencies.insert(union_name.clone());
3551
3552 SchemaType::Reference { target: union_name }
3554 }
3555 _ => analyzed.schema_type,
3556 }
3557 }
3558 Schema::Untyped { .. } => {
3559 if let Some(inferred) = items_schema.inferred_type() {
3561 match inferred {
3562 OpenApiSchemaType::Object => {
3563 let object_type_name = format!("{parent_schema_name}Item");
3565
3566 let object_type =
3568 self.analyze_object_schema(items_schema, dependencies)?;
3569
3570 let inline_schema = AnalyzedSchema {
3572 name: object_type_name.clone(),
3573 original: serde_json::to_value(items_schema)
3574 .unwrap_or(Value::Null),
3575 schema_type: object_type,
3576 dependencies: dependencies.clone(),
3577 nullable: false,
3578 description: items_schema.details().description.clone(),
3579 default: None,
3580 };
3581
3582 self.resolved_cache
3584 .insert(object_type_name.clone(), inline_schema);
3585 dependencies.insert(object_type_name.clone());
3586
3587 SchemaType::Reference {
3589 target: object_type_name,
3590 }
3591 }
3592 OpenApiSchemaType::String => SchemaType::Primitive {
3593 rust_type: "String".to_string(),
3594 serde_with: None,
3595 },
3596 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3597 let details = items_schema.details();
3598 let rust_type = self.get_number_rust_type(inferred, details);
3599 SchemaType::Primitive {
3600 rust_type,
3601 serde_with: None,
3602 }
3603 }
3604 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3605 rust_type: "bool".to_string(),
3606 serde_with: None,
3607 },
3608 _ => SchemaType::Primitive {
3609 rust_type: "serde_json::Value".to_string(),
3610 serde_with: None,
3611 },
3612 }
3613 } else {
3614 SchemaType::Primitive {
3615 rust_type: "serde_json::Value".to_string(),
3616 serde_with: None,
3617 }
3618 }
3619 }
3620 _ => SchemaType::Primitive {
3621 rust_type: "serde_json::Value".to_string(),
3622 serde_with: None,
3623 },
3624 };
3625
3626 Ok(SchemaType::Array {
3627 item_type: Box::new(item_type),
3628 })
3629 } else {
3630 Ok(SchemaType::Primitive {
3632 rust_type: "Vec<serde_json::Value>".to_string(),
3633 serde_with: None,
3634 })
3635 }
3636 }
3637
3638 fn get_number_rust_type(
3639 &self,
3640 schema_type: OpenApiSchemaType,
3641 details: &crate::openapi::SchemaDetails,
3642 ) -> String {
3643 let format = details.format.as_deref();
3647 match schema_type {
3648 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
3649 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
3650 _ => self.type_mapper.dynamic_json().rust_type,
3651 }
3652 }
3653
3654 fn analyze_anyof_union(
3655 &mut self,
3656 any_of_schemas: &[Schema],
3657 discriminator: Option<&Discriminator>,
3658 dependencies: &mut HashSet<String>,
3659 context_name: &str,
3660 ) -> Result<SchemaType> {
3661 let filtered_owned: Vec<Schema>;
3666 let any_of_schemas: &[Schema] = if any_of_schemas
3667 .iter()
3668 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3669 {
3670 filtered_owned = any_of_schemas
3671 .iter()
3672 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3673 .cloned()
3674 .collect();
3675 if filtered_owned.is_empty() {
3676 return Ok(SchemaType::Primitive {
3677 rust_type: "serde_json::Value".to_string(),
3678 serde_with: None,
3679 });
3680 }
3681 if filtered_owned.len() == 1 {
3682 return self
3683 .analyze_schema_value(&filtered_owned[0], context_name)
3684 .map(|a| a.schema_type);
3685 }
3686 &filtered_owned
3687 } else {
3688 any_of_schemas
3689 };
3690
3691 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3693 let has_objects = any_of_schemas.iter().any(|s| {
3694 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3695 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3696 });
3697 let has_arrays = any_of_schemas
3698 .iter()
3699 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3700
3701 let all_string_like = any_of_schemas.iter().all(|s| {
3704 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3705 || s.details().const_value.is_some()
3706 });
3707
3708 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3709 if let Some(disc) = discriminator {
3711 return self.analyze_oneof_union(
3713 any_of_schemas,
3714 Some(disc),
3715 context_name,
3716 dependencies,
3717 );
3718 }
3719
3720 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3722 return self.analyze_oneof_union(
3723 any_of_schemas,
3724 Some(&Discriminator {
3725 property_name: disc_field,
3726 mapping: None,
3727 default_mapping: None,
3728 extensions: crate::extensions::Extensions::default(),
3729 }),
3730 context_name,
3731 dependencies,
3732 );
3733 }
3734
3735 let mut variants = Vec::new();
3737
3738 for schema in any_of_schemas {
3739 if let Some(ref_str) = schema.reference() {
3740 if let Some(target) = self.extract_schema_name(ref_str) {
3741 dependencies.insert(target.to_string());
3742 variants.push(SchemaRef {
3743 target: target.to_string(),
3744 nullable: false,
3745 });
3746 }
3747 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3748 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3749 {
3750 let inline_index = variants.len();
3752 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3753
3754 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3756
3757 variants.push(SchemaRef {
3758 target: inline_type_name,
3759 nullable: false,
3760 });
3761 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3762 let array_type =
3764 self.analyze_array_schema(schema, context_name, dependencies)?;
3765
3766 let array_type_name = if let Some(items_schema) = &schema.details().items {
3768 if let Some(ref_str) = items_schema.reference() {
3769 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3770 dependencies.insert(item_type_name.to_string());
3771 format!("{item_type_name}Array")
3772 } else {
3773 self.generate_context_aware_name(
3774 context_name,
3775 "Array",
3776 variants.len(),
3777 Some(schema),
3778 )
3779 }
3780 } else {
3781 self.generate_context_aware_name(
3782 context_name,
3783 "Array",
3784 variants.len(),
3785 Some(schema),
3786 )
3787 }
3788 } else {
3789 self.generate_context_aware_name(
3790 context_name,
3791 "Array",
3792 variants.len(),
3793 Some(schema),
3794 )
3795 };
3796
3797 self.resolved_cache.insert(
3799 array_type_name.clone(),
3800 AnalyzedSchema {
3801 name: array_type_name.clone(),
3802 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3803 schema_type: array_type,
3804 dependencies: HashSet::new(),
3805 nullable: false,
3806 description: Some("Array variant in union".to_string()),
3807 default: None,
3808 },
3809 );
3810
3811 dependencies.insert(array_type_name.clone());
3813
3814 variants.push(SchemaRef {
3815 target: array_type_name,
3816 nullable: false,
3817 });
3818 } else if let Some(schema_type) = schema.schema_type() {
3819 let primitive_unions = self
3829 .type_mapper
3830 .config_shape_primitive_unions()
3831 .unwrap_or(true);
3832
3833 if primitive_unions {
3834 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
3835 variants.push(SchemaRef {
3836 target: mapped.rust_type,
3837 nullable: false,
3838 });
3839 } else {
3840 let inline_index = variants.len();
3841 let inline_type_name = match schema_type {
3842 OpenApiSchemaType::String => {
3843 if inline_index == 0 {
3844 format!("{context_name}String")
3845 } else {
3846 format!("{context_name}StringVariant{inline_index}")
3847 }
3848 }
3849 OpenApiSchemaType::Number => {
3850 if inline_index == 0 {
3851 format!("{context_name}Number")
3852 } else {
3853 format!("{context_name}NumberVariant{inline_index}")
3854 }
3855 }
3856 OpenApiSchemaType::Integer => {
3857 if inline_index == 0 {
3858 format!("{context_name}Integer")
3859 } else {
3860 format!("{context_name}IntegerVariant{inline_index}")
3861 }
3862 }
3863 OpenApiSchemaType::Boolean => {
3864 if inline_index == 0 {
3865 format!("{context_name}Boolean")
3866 } else {
3867 format!("{context_name}BooleanVariant{inline_index}")
3868 }
3869 }
3870 _ => format!("{context_name}Variant{inline_index}"),
3871 };
3872
3873 let rust_type =
3874 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3875
3876 self.resolved_cache.insert(
3877 inline_type_name.clone(),
3878 AnalyzedSchema {
3879 name: inline_type_name.clone(),
3880 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3881 schema_type: SchemaType::Primitive {
3882 rust_type,
3883 serde_with: None,
3884 },
3885 dependencies: HashSet::new(),
3886 nullable: false,
3887 description: schema.details().description.clone(),
3888 default: None,
3889 },
3890 );
3891
3892 dependencies.insert(inline_type_name.clone());
3893
3894 variants.push(SchemaRef {
3895 target: inline_type_name,
3896 nullable: false,
3897 });
3898 }
3899 }
3900 }
3901
3902 if !variants.is_empty() {
3903 return Ok(SchemaType::Union { variants });
3904 }
3905 }
3906
3907 let all_strings = any_of_schemas.iter().all(|schema| {
3909 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3910 || schema.details().const_value.is_some()
3911 });
3912
3913 if all_strings {
3914 let mut enum_values = Vec::new();
3916 let mut has_open_string = false;
3917
3918 for schema in any_of_schemas {
3919 if let Some(const_val) = &schema.details().const_value {
3920 if let Some(const_str) = const_val.as_str() {
3921 enum_values.push(const_str.to_string());
3922 }
3923 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3924 has_open_string = true;
3925 }
3926 }
3927
3928 if !enum_values.is_empty() {
3929 if has_open_string {
3930 return Ok(SchemaType::ExtensibleEnum {
3933 known_values: enum_values,
3934 });
3935 } else {
3936 return Ok(SchemaType::StringEnum {
3938 values: enum_values,
3939 });
3940 }
3941 }
3942 }
3943
3944 Ok(SchemaType::Primitive {
3946 rust_type: "serde_json::Value".to_string(),
3947 serde_with: None,
3948 })
3949 }
3950
3951 fn find_recursive_anchor_schema(&self) -> Option<String> {
3953 for (schema_name, schema) in &self.schemas {
3955 let details = schema.details();
3956 if details.recursive_anchor == Some(true) {
3957 return Some(schema_name.clone());
3958 }
3959 }
3960
3961 None
3965 }
3966
3967 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
3970 if let Schema::AnyOf { any_of, .. } = schema {
3972 if any_of.len() == 2 {
3973 let has_null = any_of
3974 .iter()
3975 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
3976 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
3977
3978 if has_null && has_empty_object {
3979 return true;
3980 }
3981 }
3982 }
3983
3984 self.is_dynamic_object_pattern(schema)
3986 }
3987
3988 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
3990 let is_object = match schema.schema_type() {
3992 Some(OpenApiSchemaType::Object) => true,
3993 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
3994 _ => false,
3995 };
3996
3997 if !is_object {
3998 return false;
3999 }
4000
4001 let details = schema.details();
4002
4003 if self.has_explicit_additional_properties(schema) {
4006 return false;
4007 }
4008
4009 let no_properties = details
4011 .properties
4012 .as_ref()
4013 .map(|props| props.is_empty())
4014 .unwrap_or(true);
4015
4016 if no_properties {
4017 let has_structural_constraints = details
4020 .required
4021 .as_ref()
4022 .map(|req| req.iter().any(|r| r != "type"))
4023 .unwrap_or(false)
4024 || details.pattern_properties.is_some()
4025 || details.property_names.is_some()
4026 || details.min_properties.is_some()
4027 || details.max_properties.is_some()
4028 || details.dependent_required.is_some()
4029 || details.dependent_schemas.is_some()
4030 || details.if_schema.is_some()
4031 || details.then_schema.is_some()
4032 || details.else_schema.is_some();
4033
4034 return !has_structural_constraints;
4035 }
4036
4037 false
4038 }
4039
4040 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4042 let details = schema.details();
4043
4044 matches!(
4046 &details.additional_properties,
4047 Some(crate::openapi::AdditionalProperties::Boolean(true))
4048 | Some(crate::openapi::AdditionalProperties::Schema(_))
4049 )
4050 }
4051
4052 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4054 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4055 .map_err(GeneratorError::ParseError)?;
4056
4057 if let Some(paths) = &spec.paths {
4058 for (path, path_item) in paths {
4059 let resolved = self.resolve_path_item(path_item, &spec)?;
4061 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4062 self.ingest_path_item_operations(path, pi, analysis)?;
4063 }
4064 }
4065 if let Some(webhooks) = &spec.webhooks {
4072 for (name, path_item) in webhooks {
4073 let synthetic_path = format!("__webhook__/{name}");
4074 self.ingest_path_item_operations(&synthetic_path, path_item, analysis)?;
4075 }
4076 }
4077 Ok(())
4078 }
4079
4080 fn resolve_path_item(
4084 &self,
4085 path_item: &crate::openapi::PathItem,
4086 spec: &crate::openapi::OpenApiSpec,
4087 ) -> Result<Option<crate::openapi::PathItem>> {
4088 let Some(reference) = &path_item.reference else {
4089 return Ok(None);
4090 };
4091 let target_name = reference
4092 .strip_prefix("#/components/pathItems/")
4093 .ok_or_else(|| {
4094 GeneratorError::UnresolvedReference(format!(
4095 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4096 ))
4097 })?;
4098 let pi = spec
4099 .components
4100 .as_ref()
4101 .and_then(|c| c.path_items.as_ref())
4102 .and_then(|map| map.get(target_name))
4103 .ok_or_else(|| {
4104 GeneratorError::UnresolvedReference(format!(
4105 "Path Item ref {reference} not found in components/pathItems"
4106 ))
4107 })?;
4108 Ok(Some(pi.clone()))
4109 }
4110
4111 fn ingest_path_item_operations(
4112 &mut self,
4113 path: &str,
4114 path_item: &crate::openapi::PathItem,
4115 analysis: &mut SchemaAnalysis,
4116 ) -> Result<()> {
4117 for (method, operation) in path_item.operations() {
4118 let raw_operation_id = operation
4120 .operation_id
4121 .clone()
4122 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4123
4124 use heck::ToPascalCase;
4135 let canon = |s: &str| s.replace('.', "_").to_pascal_case();
4136 let key_collides = |id: &str| -> bool {
4137 let target = canon(id);
4138 analysis
4139 .operations
4140 .keys()
4141 .any(|existing| canon(existing) == target)
4142 };
4143 let operation_id = if key_collides(&raw_operation_id) {
4144 let method_lower = method.to_lowercase();
4145 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4146 let mut suffix = 2;
4147 while key_collides(&candidate) {
4148 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4149 suffix += 1;
4150 }
4151 eprintln!(
4152 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4153 raw_operation_id, method, path, candidate
4154 );
4155 candidate
4156 } else {
4157 raw_operation_id
4158 };
4159
4160 let op_info = self.analyze_single_operation(
4161 &operation_id,
4162 method,
4163 path,
4164 operation,
4165 path_item.parameters.as_ref(),
4166 analysis,
4167 )?;
4168 analysis.operations.insert(operation_id, op_info);
4169 }
4170 Ok(())
4171 }
4172
4173 fn generate_operation_id(method: &str, path: &str) -> String {
4176 let mut operation_id = method.to_lowercase();
4178
4179 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4181
4182 for part in path_parts {
4183 if part.is_empty() {
4184 continue;
4185 }
4186
4187 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4189 &part[1..part.len() - 1]
4190 } else {
4191 part
4192 };
4193
4194 let pascal_case_part = cleaned_part
4196 .split(&['-', '_'][..])
4197 .map(|s| {
4198 let mut chars = s.chars();
4199 match chars.next() {
4200 None => String::new(),
4201 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4202 }
4203 })
4204 .collect::<String>();
4205
4206 operation_id.push_str(&pascal_case_part);
4207 }
4208
4209 operation_id
4210 }
4211
4212 fn analyze_single_operation(
4214 &mut self,
4215 operation_id: &str,
4216 method: &str,
4217 path: &str,
4218 operation: &crate::openapi::Operation,
4219 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4220 _analysis: &mut SchemaAnalysis,
4221 ) -> Result<OperationInfo> {
4222 let mut op_info = OperationInfo {
4223 operation_id: operation_id.to_string(),
4224 method: method.to_uppercase(),
4225 path: path.to_string(),
4226 summary: operation.summary.clone(),
4227 description: operation.description.clone(),
4228 request_body: None,
4229 request_body_required: operation
4231 .request_body
4232 .as_ref()
4233 .and_then(|rb| rb.required)
4234 .unwrap_or(false),
4235 response_schemas: BTreeMap::new(),
4236 parameters: Vec::new(),
4237 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4240 };
4241
4242 if let Some(request_body) = &operation.request_body
4244 && let Some((content_type, maybe_schema)) = request_body.best_content()
4245 {
4246 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
4247 op_info.request_body = if is_json_media_type(content_type) {
4248 maybe_schema
4249 .map(|s| {
4250 self.resolve_or_inline_schema(s, operation_id, "Request")
4251 .map(|name| RequestBodyContent::Json { schema_name: name })
4252 })
4253 .transpose()?
4254 } else if is_form_urlencoded_media_type(content_type) {
4255 maybe_schema
4256 .map(|s| {
4257 self.resolve_or_inline_schema(s, operation_id, "Request")
4258 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
4259 })
4260 .transpose()?
4261 } else {
4262 match content_type {
4263 "multipart/form-data" => Some(RequestBodyContent::Multipart),
4264 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
4265 "text/plain" => Some(RequestBodyContent::TextPlain),
4266 _ => None,
4267 }
4268 };
4269 }
4270
4271 if let Some(responses) = &operation.responses {
4273 for (status_code, response) in responses {
4274 if let Some(content) = response.content.as_ref() {
4280 if content.keys().any(|ct| ct.starts_with("text/event-stream")) {
4281 op_info.supports_streaming = true;
4282 }
4283 }
4284
4285 if let Some(schema) = response.json_schema() {
4286 if let Some(schema_ref) = schema.reference() {
4287 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4289 op_info
4290 .response_schemas
4291 .insert(status_code.clone(), schema_name.to_string());
4292 }
4293 } else {
4294 let synthetic_name =
4296 self.generate_inline_response_type_name(operation_id, status_code);
4297
4298 let mut deps = HashSet::new();
4300 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4301
4302 op_info
4303 .response_schemas
4304 .insert(status_code.clone(), synthetic_name);
4305 }
4306 }
4307 }
4308 }
4309
4310 if op_info.supports_streaming
4313 && let Some(parameters) = &operation.parameters
4314 {
4315 for param in parameters {
4316 if let Some(name) = param.name.as_deref() {
4317 if name.eq_ignore_ascii_case("stream") {
4318 op_info.stream_parameter = Some(name.to_string());
4319 break;
4320 }
4321 }
4322 }
4323 }
4324
4325 if let Some(parameters) = &operation.parameters {
4327 for param in parameters {
4328 let resolved = self.resolve_parameter(param);
4329 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4330 op_info.parameters.push(param_info);
4331 }
4332 }
4333 }
4334
4335 if let Some(path_params) = path_item_parameters {
4337 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4338 .parameters
4339 .iter()
4340 .map(|p| (p.name.clone(), p.location.clone()))
4341 .collect();
4342 for param in path_params {
4343 let resolved = self.resolve_parameter(param);
4344 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4345 if !existing_keys
4346 .contains(&(param_info.name.clone(), param_info.location.clone()))
4347 {
4348 op_info.parameters.push(param_info);
4349 }
4350 }
4351 }
4352 }
4353
4354 let mut declared_path_names: std::collections::HashSet<String> = op_info
4362 .parameters
4363 .iter()
4364 .filter(|p| p.location == "path")
4365 .map(|p| p.name.clone())
4366 .collect();
4367 let bytes = path.as_bytes().iter();
4368 let mut current = String::new();
4369 let mut in_brace = false;
4370 let mut synthesized: Vec<String> = Vec::new();
4371 for b in bytes {
4372 match *b {
4373 b'{' => {
4374 in_brace = true;
4375 current.clear();
4376 }
4377 b'}' if in_brace => {
4378 in_brace = false;
4379 if !current.is_empty() && !declared_path_names.contains(¤t) {
4380 synthesized.push(current.clone());
4381 declared_path_names.insert(current.clone());
4382 }
4383 }
4384 _ if in_brace => current.push(*b as char),
4385 _ => {}
4386 }
4387 }
4388 for name in synthesized {
4389 eprintln!(
4390 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
4391 path, name
4392 );
4393 op_info.parameters.push(ParameterInfo {
4394 name,
4395 location: "path".to_string(),
4396 required: true,
4397 schema_ref: None,
4398 rust_type: "String".to_string(),
4399 description: None,
4400 enum_values: None,
4401 rust_ident: None,
4402 });
4403 }
4404
4405 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
4413 for p in op_info.parameters.iter_mut() {
4414 let raw = base_param_ident(&p.name);
4415 let mut chosen = raw.clone();
4416 let mut suffix = 2;
4417 while !used.insert(chosen.clone()) {
4418 chosen = format!("{raw}_{suffix}");
4419 suffix += 1;
4420 }
4421 p.rust_ident = Some(chosen);
4422 }
4423
4424 Ok(op_info)
4425 }
4426
4427 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
4434 use heck::ToPascalCase;
4435 let base_name = operation_id.replace('.', "_").to_pascal_case();
4436 let suffix = Self::status_code_suffix(status_code);
4437 format!("{}Response{}", base_name, suffix)
4438 }
4439
4440 fn status_code_suffix(status_code: &str) -> String {
4447 match status_code {
4448 "" | "200" => String::new(),
4449 "default" | "Default" => "Default".to_string(),
4450 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
4451 other => other.to_ascii_lowercase(),
4452 }
4453 }
4454
4455 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
4457 use heck::ToPascalCase;
4458 let base_name = operation_id.replace('.', "_").to_pascal_case();
4462 format!("{}Request", base_name)
4463 }
4464
4465 fn resolve_or_inline_schema(
4468 &mut self,
4469 schema: &crate::openapi::Schema,
4470 operation_id: &str,
4471 suffix: &str,
4472 ) -> Result<String> {
4473 if let Some(schema_ref) = schema.reference()
4474 && let Some(schema_name) = self.extract_schema_name(schema_ref)
4475 {
4476 return Ok(schema_name.to_string());
4477 }
4478 let synthetic_name = if suffix == "Request" {
4480 self.generate_inline_request_type_name(operation_id)
4481 } else {
4482 self.generate_inline_response_type_name(operation_id, "")
4483 };
4484 let mut deps = HashSet::new();
4485 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4486 Ok(synthetic_name)
4487 }
4488
4489 fn resolve_parameter<'a>(
4492 &'a self,
4493 param: &'a crate::openapi::Parameter,
4494 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
4495 if let Some(ref_str) = param.reference.as_deref() {
4496 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
4497 if let Some(resolved) = self.component_parameters.get(param_name) {
4498 return std::borrow::Cow::Borrowed(resolved);
4499 }
4500 }
4501 }
4502 std::borrow::Cow::Borrowed(param)
4503 }
4504
4505 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
4517 let Some(schema_value) = self
4518 .openapi_spec
4519 .get("components")
4520 .and_then(|c| c.get("schemas"))
4521 .and_then(|s| s.get(name))
4522 else {
4523 return false;
4524 };
4525 let is_string_type = schema_value
4526 .get("type")
4527 .and_then(|v| v.as_str())
4528 .map(|s| s == "string")
4529 .unwrap_or(false);
4530 let has_enum_or_const =
4531 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
4532 is_string_type && has_enum_or_const
4533 }
4534
4535 fn analyze_parameter(
4536 &self,
4537 param: &crate::openapi::Parameter,
4538 operation_id: &str,
4539 ) -> Result<Option<ParameterInfo>> {
4540 use heck::ToPascalCase;
4541
4542 let name = param.name.as_deref().unwrap_or("");
4543 let location = param.location.as_deref().unwrap_or("");
4544 let required = param.required.unwrap_or(false);
4545
4546 let mut rust_type = "String".to_string();
4547 let mut schema_ref = None;
4548 let mut enum_values: Option<Vec<String>> = None;
4549
4550 if let Some(schema) = ¶m.schema {
4551 if let Some(ref_str) = schema.reference() {
4552 if let Some(name) = self.extract_schema_name(ref_str) {
4559 if self.referenced_schema_is_string_enum(name) {
4560 schema_ref = Some(name.to_string());
4561 }
4562 }
4563 } else if let Some(schema_type) = schema.schema_type() {
4564 let format = schema.details().format.clone();
4570 rust_type = match schema_type {
4571 crate::openapi::SchemaType::Boolean => "bool".to_string(),
4572 crate::openapi::SchemaType::Integer => {
4573 self.type_mapper.integer_format(format.as_deref()).rust_type
4574 }
4575 crate::openapi::SchemaType::Number => {
4576 self.type_mapper.number_format(format.as_deref()).rust_type
4577 }
4578 crate::openapi::SchemaType::String => "String".to_string(),
4579 _ => "String".to_string(),
4580 };
4581
4582 if matches!(schema_type, crate::openapi::SchemaType::String) {
4583 let details = schema.details();
4584 if details.is_string_enum() {
4585 if let Some(values) = details.string_enum_values() {
4586 if !values.is_empty() {
4587 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
4588 let param_pascal = name.to_pascal_case();
4589 rust_type = format!("{op_pascal}{param_pascal}");
4590 enum_values = Some(values);
4591 }
4592 }
4593 }
4594 }
4595 }
4596 }
4597
4598 Ok(Some(ParameterInfo {
4599 name: name.to_string(),
4600 location: location.to_string(),
4601 required,
4602 schema_ref,
4603 rust_type,
4604 description: param.description.clone(),
4605 enum_values,
4606 rust_ident: None,
4607 }))
4608 }
4609}