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 #[serde(skip_serializing_if = "Option::is_none")]
401 pub query_serialization: Option<QuerySerialization>,
402}
403
404#[derive(Debug, Clone, PartialEq, serde::Serialize)]
409pub enum QuerySerialization {
410 FormExplodedObject,
414 FormObject,
417 DeepObject,
420 FormExplodedArray { item_type: ArrayItemType },
423 FormArray { item_type: ArrayItemType },
426}
427
428#[derive(Debug, Clone, PartialEq, serde::Serialize)]
435pub enum ArrayItemType {
436 Scalar(String),
438 EnumRef(String),
440}
441
442impl Default for DependencyGraph {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448impl DependencyGraph {
449 pub fn new() -> Self {
450 Self {
451 edges: BTreeMap::new(),
452 recursive_schemas: HashSet::new(),
453 }
454 }
455
456 pub fn add_dependency(&mut self, from: String, to: String) {
457 self.edges.entry(from).or_default().insert(to);
458 }
459
460 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
462 self.detect_recursive_schemas();
464
465 let mut temp_edges = self.edges.clone();
467 for (schema, deps) in &mut temp_edges {
468 deps.remove(schema); }
470
471 let mut visited = HashSet::new();
472 let mut temp_visited = HashSet::new();
473 let mut result = Vec::new();
474
475 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
477 all_nodes.sort();
478 for node in all_nodes {
479 if !visited.contains(node) {
480 self.visit_node_recursive(
481 node,
482 &temp_edges,
483 &mut visited,
484 &mut temp_visited,
485 &mut result,
486 )?;
487 }
488 }
489
490 result.reverse();
491 Ok(result)
492 }
493
494 fn detect_recursive_schemas(&mut self) {
495 for (schema, deps) in &self.edges {
496 if deps.contains(schema) {
497 self.recursive_schemas.insert(schema.clone());
499 } else {
500 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
502 self.recursive_schemas.insert(schema.clone());
503 }
504 }
505 }
506
507 for (schema, deps) in &self.edges {
509 for dep in deps {
510 if let Some(dep_deps) = self.edges.get(dep) {
511 if dep_deps.contains(schema) {
512 self.recursive_schemas.insert(schema.clone());
514 self.recursive_schemas.insert(dep.clone());
515 }
516 }
517 }
518 }
519 }
520
521 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
522 if visited.contains(current) {
523 return false; }
525
526 visited.insert(current.to_string());
527
528 if let Some(deps) = self.edges.get(current) {
529 for dep in deps {
530 if dep == start {
531 return true; }
533 if self.has_cycle_from(start, dep, visited) {
534 return true;
535 }
536 }
537 }
538
539 false
540 }
541
542 #[allow(clippy::only_used_in_recursion)]
543 fn visit_node_recursive(
544 &self,
545 node: &str,
546 temp_edges: &BTreeMap<String, HashSet<String>>,
547 visited: &mut HashSet<String>,
548 temp_visited: &mut HashSet<String>,
549 result: &mut Vec<String>,
550 ) -> Result<()> {
551 if temp_visited.contains(node) {
552 return Ok(());
554 }
555
556 if visited.contains(node) {
557 return Ok(());
558 }
559
560 temp_visited.insert(node.to_string());
561
562 if let Some(dependencies) = temp_edges.get(node) {
563 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
565 sorted_deps.sort();
566 for dep in sorted_deps {
567 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
568 }
569 }
570
571 temp_visited.remove(node);
572 visited.insert(node.to_string());
573 result.push(node.to_string());
574
575 Ok(())
576 }
577}
578
579pub fn merge_schema_extensions(
582 main_spec: Value,
583 extension_paths: &[impl AsRef<Path>],
584) -> Result<Value> {
585 let mut result = main_spec;
586
587 for path in extension_paths {
588 let extension = load_extension_file(path.as_ref())?;
589 result = merge_json_objects_with_replacements(result, extension)?;
590 }
591
592 Ok(result)
593}
594
595fn load_extension_file(path: &Path) -> Result<Value> {
597 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
598 message: format!("Failed to read file {}: {}", path.display(), e),
599 })?;
600
601 serde_json::from_str(&content).map_err(GeneratorError::ParseError)
602}
603
604fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
606 let replacements = extract_replacement_rules(&extension);
608
609 Ok(merge_json_objects_with_rules(
611 main,
612 extension,
613 &replacements,
614 ))
615}
616
617fn extract_replacement_rules(
619 extension: &Value,
620) -> std::collections::HashMap<String, (String, String)> {
621 let mut rules = std::collections::HashMap::new();
622
623 if let Some(x_replacements) = extension.get("x-replacements") {
624 if let Some(x_replacements_obj) = x_replacements.as_object() {
625 for (schema_name, replacement_rule) in x_replacements_obj {
626 if let Some(rule_obj) = replacement_rule.as_object() {
627 if let (Some(replace), Some(with)) = (
628 rule_obj.get("replace").and_then(|v| v.as_str()),
629 rule_obj.get("with").and_then(|v| v.as_str()),
630 ) {
631 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
632 }
634 }
635 }
636 }
637 }
638
639 rules
640}
641
642fn should_replace_variant(
644 schema_name: &str,
645 extension_refs: &[String],
646 replacements: &std::collections::HashMap<String, (String, String)>,
647) -> bool {
648 for (replace_schema, with_schema) in replacements.values() {
650 if schema_name == replace_schema {
651 let replacement_exists = extension_refs.iter().any(|ext_ref| {
653 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
654 ext_schema_name == with_schema
655 });
656
657 if replacement_exists {
658 return true;
659 }
660 }
661 }
662
663 extension_refs.iter().any(|ext_ref| {
665 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
666 schema_name == ext_schema_name
667 })
668}
669
670fn merge_json_objects_with_rules(
675 main: Value,
676 extension: Value,
677 replacements: &std::collections::HashMap<String, (String, String)>,
678) -> Value {
679 match (main, extension) {
680 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
682 let main_union_keyword = if main_obj.contains_key("oneOf") {
685 Some("oneOf")
686 } else if main_obj.contains_key("anyOf") {
687 Some("anyOf")
688 } else {
689 None
690 };
691 if let (Some(main_variants), Some(ext_variants)) = (
692 extract_schema_variants(&Value::Object(main_obj.clone())),
693 extract_schema_variants(&Value::Object(ext_obj.clone())),
694 ) {
695 let union_key = main_union_keyword.unwrap_or("oneOf");
696 println!(
697 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
698 main_variants.len(),
699 ext_variants.len()
700 );
701 let mut merged_variants = Vec::new();
704 let extension_refs: Vec<String> = ext_variants
705 .iter()
706 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
707 .map(|s| s.to_string())
708 .collect();
709
710 for main_variant in main_variants {
712 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
713 let schema_name = main_ref.split('/').next_back().unwrap_or("");
715 let should_replace =
716 should_replace_variant(schema_name, &extension_refs, replacements);
717
718 if should_replace {
719 println!("🔄 REPLACING {} (explicit rule)", schema_name);
720 }
721
722 if !should_replace {
723 merged_variants.push(main_variant);
724 }
725 } else {
726 merged_variants.push(main_variant);
728 }
729 }
730
731 for ext_variant in ext_variants {
733 merged_variants.push(ext_variant);
734 }
735
736 main_obj.remove("oneOf");
738 main_obj.remove("anyOf");
739 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
740
741 for (key, ext_value) in ext_obj {
743 if key != "oneOf" && key != "anyOf" {
744 match main_obj.get(&key) {
745 Some(main_value) => {
746 let merged_value = merge_json_objects_with_rules(
747 main_value.clone(),
748 ext_value,
749 replacements,
750 );
751 main_obj.insert(key, merged_value);
752 }
753 None => {
754 main_obj.insert(key, ext_value);
755 }
756 }
757 }
758 }
759
760 return Value::Object(main_obj);
761 }
762
763 for (key, ext_value) in ext_obj {
765 match main_obj.get(&key) {
766 Some(main_value) => {
767 let merged_value = merge_json_objects_with_rules(
769 main_value.clone(),
770 ext_value,
771 replacements,
772 );
773 main_obj.insert(key, merged_value);
774 }
775 None => {
776 main_obj.insert(key, ext_value);
778 }
779 }
780 }
781 Value::Object(main_obj)
782 }
783
784 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
786 main_arr.extend(ext_arr);
787 Value::Array(main_arr)
788 }
789
790 (_, extension) => extension,
792 }
793}
794
795fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
797 if let Value::Object(map) = obj {
798 if let Some(Value::Array(variants)) = map.get("oneOf") {
799 return Some(variants.clone());
800 }
801 if let Some(Value::Array(variants)) = map.get("anyOf") {
802 return Some(variants.clone());
803 }
804 }
805 None
806}
807
808pub struct SchemaAnalyzer {
809 schemas: BTreeMap<String, Schema>,
810 resolved_cache: BTreeMap<String, AnalyzedSchema>,
811 openapi_spec: Value,
812 current_schema_name: Option<String>,
813 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
814 type_mapper: TypeMapper,
819}
820
821impl SchemaAnalyzer {
822 pub fn new(openapi_spec: Value) -> Result<Self> {
826 Self::with_type_mapper(openapi_spec, TypeMapper::default())
827 }
828
829 pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
833 let spec: OpenApiSpec =
834 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
835 let schemas = Self::extract_schemas(&spec)?;
836
837 let component_parameters = spec
838 .components
839 .as_ref()
840 .and_then(|c| c.parameters.as_ref())
841 .cloned()
842 .unwrap_or_default();
843
844 Ok(Self {
845 schemas,
846 resolved_cache: BTreeMap::new(),
847 openapi_spec,
848 current_schema_name: None,
849 component_parameters,
850 type_mapper,
851 })
852 }
853
854 pub fn new_with_extensions(
857 openapi_spec: Value,
858 extension_paths: &[std::path::PathBuf],
859 ) -> Result<Self> {
860 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
861 Self::new(merged_spec)
862 }
863
864 pub fn new_with_extensions_and_type_mapper(
867 openapi_spec: Value,
868 extension_paths: &[std::path::PathBuf],
869 type_mapper: TypeMapper,
870 ) -> Result<Self> {
871 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
872 Self::with_type_mapper(merged_spec, type_mapper)
873 }
874
875 pub fn type_mapper(&self) -> &TypeMapper {
879 &self.type_mapper
880 }
881
882 fn generate_context_aware_name(
885 &self,
886 base_context: &str,
887 type_hint: &str,
888 index: usize,
889 schema: Option<&Schema>,
890 ) -> String {
891 if let Some(schema) = schema {
893 if type_hint == "Array"
895 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
896 {
897 if let Some(items_schema) = &schema.details().items {
898 if let Some(item_type) = items_schema.schema_type() {
900 match item_type {
901 OpenApiSchemaType::Object => {
902 return format!("{base_context}ItemArray");
903 }
904 OpenApiSchemaType::String => {
905 return format!("{base_context}StringArray");
906 }
907 _ => {}
908 }
909 }
910 }
911 }
912 }
913
914 match type_hint {
916 "Array" => {
917 format!("{base_context}Array")
919 }
920 "Variant" | "InlineVariant" => {
921 if index == 0 {
923 format!("{base_context}{type_hint}")
924 } else {
925 format!("{}{}{}", base_context, type_hint, index + 1)
926 }
927 }
928 _ => {
929 format!("{base_context}{type_hint}{index}")
931 }
932 }
933 }
934
935 fn to_pascal_case(&self, s: &str) -> String {
937 s.split(['_', '-'])
938 .filter(|part| !part.is_empty())
939 .map(|part| {
940 let mut chars = part.chars();
941 match chars.next() {
942 None => String::new(),
943 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
944 }
945 })
946 .collect()
947 }
948
949 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
950 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
955 Ok(schemas
956 .map(|m| {
957 m.iter()
958 .map(|(k, v)| (k.clone(), v.clone()))
959 .collect::<BTreeMap<_, _>>()
960 })
961 .unwrap_or_default())
962 }
963
964 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
965 let mut analysis = SchemaAnalysis {
966 schemas: BTreeMap::new(),
967 dependencies: DependencyGraph::new(),
968 patterns: DetectedPatterns {
969 tagged_enum_schemas: HashSet::new(),
970 untagged_enum_schemas: HashSet::new(),
971 type_mappings: BTreeMap::new(),
972 },
973 operations: BTreeMap::new(),
974 used_type_features: crate::type_mapping::UsedFeatures::default(),
975 enum_extensions: BTreeMap::new(),
976 };
977
978 self.detect_patterns(&mut analysis.patterns)?;
980
981 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
983 for schema_name in schema_names {
984 let analyzed = self.analyze_schema(&schema_name)?;
985
986 for dep in &analyzed.dependencies {
988 analysis
989 .dependencies
990 .add_dependency(schema_name.clone(), dep.clone());
991 }
992
993 analysis.schemas.insert(schema_name, analyzed);
994 }
995
996 for (inline_name, inline_schema) in &self.resolved_cache {
999 if !analysis.schemas.contains_key(inline_name) {
1000 analysis
1002 .schemas
1003 .insert(inline_name.clone(), inline_schema.clone());
1004
1005 for dep in &inline_schema.dependencies {
1007 analysis
1008 .dependencies
1009 .add_dependency(inline_name.clone(), dep.clone());
1010 }
1011
1012 let mut schemas_to_update = Vec::new();
1017 for (schema_name, schema) in &analysis.schemas {
1018 if schema_name == inline_name {
1020 continue;
1021 }
1022
1023 if schema.dependencies.contains(inline_name) {
1024 schemas_to_update.push(schema_name.clone());
1026 }
1027 }
1028
1029 for schema_name in schemas_to_update {
1031 analysis
1032 .dependencies
1033 .add_dependency(schema_name, inline_name.clone());
1034 }
1035 }
1036 }
1037
1038 self.analyze_operations(&mut analysis)?;
1040
1041 for (inline_name, inline_schema) in &self.resolved_cache {
1044 if !analysis.schemas.contains_key(inline_name) {
1045 analysis
1046 .schemas
1047 .insert(inline_name.clone(), inline_schema.clone());
1048
1049 for dep in &inline_schema.dependencies {
1051 analysis
1052 .dependencies
1053 .add_dependency(inline_name.clone(), dep.clone());
1054 }
1055 }
1056 }
1057
1058 analysis.used_type_features = self.type_mapper.used_features();
1062
1063 for (name, analyzed) in &analysis.schemas {
1068 let enum_value_count = match &analyzed.schema_type {
1069 SchemaType::StringEnum { values } => values.len(),
1070 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1071 _ => continue,
1072 };
1073 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1074 analysis.enum_extensions.insert(name.clone(), ext);
1075 }
1076 }
1077
1078 Ok(analysis)
1079 }
1080
1081 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1082 for (schema_name, schema) in &self.schemas {
1083 if self.is_discriminated_union(schema) {
1085 patterns.tagged_enum_schemas.insert(schema_name.clone());
1086
1087 if let Some(mappings) = self.extract_type_mappings(schema)? {
1089 patterns.type_mappings.insert(schema_name.clone(), mappings);
1090 }
1091 }
1092 else if self.is_simple_union(schema) {
1094 patterns.untagged_enum_schemas.insert(schema_name.clone());
1095 }
1096 }
1097
1098 Ok(())
1099 }
1100
1101 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1102 if schema.is_discriminated_union() {
1104 return true;
1105 }
1106
1107 if let Some(variants) = schema.union_variants() {
1109 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1110 }
1111
1112 false
1113 }
1114
1115 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1116 variants.iter().all(|variant| {
1117 if let Some(ref_str) = variant.reference() {
1118 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1120 if let Some(schema) = self.schemas.get(schema_name) {
1121 return self.has_const_discriminator_field(schema, field_name);
1122 }
1123 }
1124 } else {
1125 return self.has_const_discriminator_field(variant, field_name);
1127 }
1128 false
1129 })
1130 }
1131
1132 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1141 if let Some(ref_str) = schema.reference() {
1143 return match self
1144 .extract_schema_name(ref_str)
1145 .and_then(|n| self.schemas.get(n))
1146 {
1147 Some(target) => self.branch_resolves_to_object(target),
1148 None => false,
1149 };
1150 }
1151 if matches!(
1154 schema,
1155 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1156 ) {
1157 return true;
1158 }
1159 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1160 return true;
1161 }
1162 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1163 return true;
1164 }
1165 false
1168 }
1169
1170 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1174 if variants.is_empty() {
1175 return None;
1176 }
1177
1178 let first_variant = &variants[0];
1180 let first_schema = if let Some(ref_str) = first_variant.reference() {
1181 let schema_name = self.extract_schema_name(ref_str)?;
1182 self.schemas.get(schema_name)?
1183 } else {
1184 first_variant
1185 };
1186
1187 let properties = first_schema.details().properties.as_ref()?;
1188 let mut candidates: Vec<String> = Vec::new();
1189
1190 for (field_name, field_schema) in properties {
1191 let details = field_schema.details();
1192 let is_const = details.const_value.is_some()
1193 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1194 || details.extra.contains_key("const");
1195 if is_const {
1196 candidates.push(field_name.clone());
1197 }
1198 }
1199
1200 if candidates.is_empty() {
1201 return None;
1202 }
1203
1204 candidates.sort_by(|a, b| {
1206 if a == "type" {
1207 std::cmp::Ordering::Less
1208 } else if b == "type" {
1209 std::cmp::Ordering::Greater
1210 } else {
1211 a.cmp(b)
1212 }
1213 });
1214
1215 for candidate in &candidates {
1217 if self.all_variants_have_const_field(variants, candidate) {
1218 return Some(candidate.clone());
1219 }
1220 }
1221
1222 None
1223 }
1224
1225 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1226 if let Some(properties) = &schema.details().properties {
1227 if let Some(field) = properties.get(field_name) {
1228 if field.details().const_value.is_some() {
1230 return true;
1231 }
1232 if let Some(enum_vals) = &field.details().enum_values {
1234 return enum_vals.len() == 1;
1235 }
1236 return field.details().extra.contains_key("const");
1238 }
1239 }
1240 false
1241 }
1242
1243 fn is_simple_union(&self, schema: &Schema) -> bool {
1244 if let Some(variants) = schema.union_variants() {
1245 if variants.len() > 1 && !schema.is_nullable_pattern() {
1247 let has_refs = variants.iter().any(|v| v.is_reference());
1248 return has_refs;
1249 }
1250 }
1251 false
1252 }
1253
1254 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1255 let variants = schema.union_variants().ok_or_else(|| {
1256 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1257 })?;
1258
1259 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1261 discriminator.property_name.clone()
1262 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1263 detected
1264 } else {
1265 "type".to_string() };
1267
1268 let mut mappings = BTreeMap::new();
1269
1270 for variant in variants {
1271 if let Some(ref_str) = variant.reference() {
1272 if let Some(type_name) = self.extract_schema_name(ref_str) {
1273 if let Some(variant_schema) = self.schemas.get(type_name) {
1274 if let Some(discriminator_value) = self
1275 .extract_discriminator_value_for_field(
1276 variant_schema,
1277 &discriminator_field,
1278 )
1279 {
1280 mappings.insert(type_name.to_string(), discriminator_value);
1281 }
1282 }
1283 }
1284 }
1285 }
1286
1287 if mappings.is_empty() {
1288 Ok(None)
1289 } else {
1290 Ok(Some(mappings))
1291 }
1292 }
1293
1294 #[allow(dead_code)]
1295 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1296 self.extract_discriminator_value_for_field(schema, "type")
1297 }
1298
1299 fn extract_discriminator_value_for_field(
1300 &self,
1301 schema: &Schema,
1302 field_name: &str,
1303 ) -> Option<String> {
1304 if let Some(properties) = &schema.details().properties {
1305 if let Some(type_field) = properties.get(field_name) {
1306 if let Some(const_value) = &type_field.details().const_value {
1308 if let Some(value) = const_value.as_str() {
1309 return Some(value.to_string());
1310 }
1311 }
1312 if let Some(enum_values) = &type_field.details().enum_values {
1314 if enum_values.len() == 1 {
1315 return enum_values[0].as_str().map(|s| s.to_string());
1316 }
1317 }
1318 if let Some(const_value) = type_field.details().extra.get("const") {
1320 return const_value.as_str().map(|s| s.to_string());
1321 }
1322 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1324 if stainless_const.as_bool() == Some(true) {
1325 if let Some(default_value) = &type_field.details().default {
1326 if let Some(value) = default_value.as_str() {
1327 return Some(value.to_string());
1328 }
1329 }
1330 }
1331 }
1332 }
1333 }
1334 None
1335 }
1336
1337 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1338 schema.reference().or_else(|| schema.recursive_reference())
1339 }
1340
1341 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1342 if ref_str == "#" {
1343 return None; }
1345
1346 let parts: Vec<&str> = ref_str.split('/').collect();
1347
1348 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1350 return Some(parts[3]);
1351 }
1352
1353 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1356 return Some(parts[2]);
1357 }
1358
1359 let last = parts.last()?;
1365 if last.is_empty()
1366 || last.chars().all(|c| c.is_ascii_digit())
1367 || matches!(
1368 *last,
1369 "schema" | "properties" | "items" | "additionalProperties"
1370 )
1371 {
1372 return None;
1373 }
1374 let first = last.chars().next().unwrap_or(' ');
1375 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1376 return None;
1377 }
1378 Some(last)
1379 }
1380
1381 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1382 if let Some(cached) = self.resolved_cache.get(schema_name) {
1384 return Ok(cached.clone());
1385 }
1386
1387 self.current_schema_name = Some(schema_name.to_string());
1389
1390 let schema = self
1391 .schemas
1392 .get(schema_name)
1393 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1394 .clone();
1395
1396 self.resolved_cache.insert(
1398 schema_name.to_string(),
1399 AnalyzedSchema {
1400 name: schema_name.to_string(),
1401 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1402 schema_type: SchemaType::Reference {
1403 target: "placeholder".to_string(),
1404 },
1405 dependencies: HashSet::new(),
1406 nullable: false,
1407 description: None,
1408 default: None,
1409 },
1410 );
1411
1412 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1413
1414 self.resolved_cache
1416 .insert(schema_name.to_string(), analyzed.clone());
1417
1418 Ok(analyzed)
1419 }
1420
1421 fn analyze_schema_value(
1422 &mut self,
1423 schema: &Schema,
1424 schema_name: &str,
1425 ) -> Result<AnalyzedSchema> {
1426 let details = schema.details();
1427 let description = details.description.clone();
1428 let nullable = details.is_nullable() || schema.type_array_contains_null();
1430 let mut dependencies = HashSet::new();
1431
1432 let schema_type = match schema {
1433 Schema::Reference { reference, .. } => {
1434 match self.extract_schema_name(reference) {
1439 Some(name) => {
1440 let target = name.to_string();
1441 dependencies.insert(target.clone());
1442 SchemaType::Reference { target }
1443 }
1444 None => {
1445 eprintln!(
1446 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1447 reference
1448 );
1449 SchemaType::Primitive {
1450 rust_type: "serde_json::Value".to_string(),
1451 serde_with: None,
1452 }
1453 }
1454 }
1455 }
1456 Schema::RecursiveRef { recursive_ref, .. }
1457 | Schema::DynamicRef {
1458 dynamic_ref: recursive_ref,
1459 ..
1460 } => {
1461 if recursive_ref == "#" {
1467 dependencies.insert(schema_name.to_string());
1468 SchemaType::Reference {
1469 target: schema_name.to_string(),
1470 }
1471 } else {
1472 let target = self
1473 .extract_schema_name(recursive_ref)
1474 .unwrap_or(schema_name)
1475 .to_string();
1476 dependencies.insert(target.clone());
1477 SchemaType::Reference { target }
1478 }
1479 }
1480 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1481 let primary = schema
1482 .schema_type()
1483 .cloned()
1484 .unwrap_or(OpenApiSchemaType::Object);
1485 let format = details.format.as_deref();
1486 match primary {
1487 OpenApiSchemaType::String => {
1488 if let Some(values) = details.string_enum_values() {
1489 SchemaType::StringEnum { values }
1490 } else {
1491 SchemaType::Primitive {
1492 rust_type: self.type_mapper.string_format(format).rust_type,
1493 serde_with: None,
1494 }
1495 }
1496 }
1497 OpenApiSchemaType::Integer => SchemaType::Primitive {
1498 rust_type: self.type_mapper.integer_format(format).rust_type,
1499 serde_with: None,
1500 },
1501 OpenApiSchemaType::Number => SchemaType::Primitive {
1502 rust_type: self.type_mapper.number_format(format).rust_type,
1503 serde_with: None,
1504 },
1505 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1506 rust_type: self.type_mapper.boolean().rust_type,
1507 serde_with: None,
1508 },
1509 OpenApiSchemaType::Array => {
1510 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1512 }
1513 OpenApiSchemaType::Object => {
1514 if self.should_use_dynamic_json(schema) {
1516 SchemaType::Primitive {
1517 rust_type: self.type_mapper.dynamic_json().rust_type,
1518 serde_with: None,
1519 }
1520 } else {
1521 self.analyze_object_schema(schema, &mut dependencies)?
1523 }
1524 }
1525 _ => SchemaType::Primitive {
1526 rust_type: self.type_mapper.dynamic_json().rust_type,
1527 serde_with: None,
1528 },
1529 }
1530 }
1531 Schema::AnyOf {
1532 any_of,
1533 discriminator,
1534 ..
1535 } => {
1536 self.analyze_anyof_union(
1538 any_of,
1539 discriminator.as_ref(),
1540 &mut dependencies,
1541 schema_name,
1542 )?
1543 }
1544 Schema::OneOf {
1545 one_of,
1546 discriminator,
1547 ..
1548 } => {
1549 self.analyze_oneof_union(
1551 one_of,
1552 discriminator.as_ref(),
1553 schema_name,
1554 &mut dependencies,
1555 )?
1556 }
1557 Schema::AllOf { all_of, .. } => {
1558 self.analyze_allof_composition(all_of, &mut dependencies)?
1560 }
1561 Schema::Untyped { .. } => {
1562 if let Some(inferred) = schema.inferred_type() {
1564 match inferred {
1565 OpenApiSchemaType::Object => {
1566 if self.should_use_dynamic_json(schema) {
1567 SchemaType::Primitive {
1568 rust_type: "serde_json::Value".to_string(),
1569 serde_with: None,
1570 }
1571 } else {
1572 self.analyze_object_schema(schema, &mut dependencies)?
1573 }
1574 }
1575 OpenApiSchemaType::String if details.is_string_enum() => {
1576 SchemaType::StringEnum {
1577 values: details.string_enum_values().unwrap_or_default(),
1578 }
1579 }
1580 _ => SchemaType::Primitive {
1581 rust_type: "serde_json::Value".to_string(),
1582 serde_with: None,
1583 },
1584 }
1585 } else {
1586 SchemaType::Primitive {
1587 rust_type: "serde_json::Value".to_string(),
1588 serde_with: None,
1589 }
1590 }
1591 }
1592 };
1593
1594 Ok(AnalyzedSchema {
1595 name: schema_name.to_string(),
1596 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1598 dependencies,
1599 nullable,
1600 description,
1601 default: details.default.clone(),
1602 })
1603 }
1604
1605 fn analyze_object_schema(
1606 &mut self,
1607 schema: &Schema,
1608 dependencies: &mut HashSet<String>,
1609 ) -> Result<SchemaType> {
1610 let details = schema.details();
1611 let properties = &details.properties;
1612 let required = details
1613 .required
1614 .as_ref()
1615 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1616 .unwrap_or_default();
1617
1618 let mut property_info = BTreeMap::new();
1619
1620 if let Some(props) = properties {
1621 for (prop_name, prop_schema) in props {
1622 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1624 if self.should_use_dynamic_json(prop_schema) {
1626 SchemaType::Primitive {
1628 rust_type: "serde_json::Value".to_string(),
1629 serde_with: None,
1630 }
1631 } else if prop_schema.is_nullable_pattern()
1632 && let Some(non_null) = prop_schema.non_null_variant()
1633 {
1634 self.analyze_property_schema_with_context(
1642 non_null,
1643 Some(prop_name),
1644 dependencies,
1645 )?
1646 } else {
1647 let context_name = self
1650 .current_schema_name
1651 .clone()
1652 .unwrap_or_else(|| "Unknown".to_string());
1653
1654 let prop_pascal = self.to_pascal_case(prop_name);
1656 let mut union_type_name = format!("{context_name}{prop_pascal}");
1657
1658 if self.schemas.contains_key(&union_type_name)
1661 || self.resolved_cache.contains_key(&union_type_name)
1662 {
1663 let mut suffix = 2;
1664 loop {
1665 let candidate = format!("{union_type_name}Union{suffix}");
1666 if !self.schemas.contains_key(&candidate)
1667 && !self.resolved_cache.contains_key(&candidate)
1668 {
1669 union_type_name = candidate;
1670 break;
1671 }
1672 suffix += 1;
1673 if suffix > 1000 {
1674 break;
1675 }
1676 }
1677 }
1678
1679 let union_schema_type = self.analyze_anyof_union(
1681 any_of,
1682 prop_schema.discriminator(),
1683 dependencies,
1684 &union_type_name,
1685 )?;
1686
1687 self.resolved_cache.insert(
1689 union_type_name.clone(),
1690 AnalyzedSchema {
1691 name: union_type_name.clone(),
1692 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1693 schema_type: union_schema_type,
1694 dependencies: HashSet::new(),
1695 nullable: false,
1696 description: prop_schema.details().description.clone(),
1697 default: None,
1698 },
1699 );
1700
1701 dependencies.insert(union_type_name.clone());
1703 SchemaType::Reference {
1704 target: union_type_name,
1705 }
1706 }
1707 } else if let Schema::OneOf {
1708 one_of,
1709 discriminator,
1710 ..
1711 } = prop_schema
1712 {
1713 if prop_schema.is_nullable_pattern()
1720 && let Some(non_null) = prop_schema.non_null_variant()
1721 {
1722 let unwrapped = self.analyze_property_schema_with_context(
1723 non_null,
1724 Some(prop_name),
1725 dependencies,
1726 )?;
1727 let prop_details = prop_schema.details();
1728 let prop_nullable = true;
1729 let prop_description = prop_details.description.clone();
1730 let prop_default = prop_details.default.clone();
1731 property_info.insert(
1732 prop_name.clone(),
1733 PropertyInfo {
1734 schema_type: unwrapped,
1735 nullable: prop_nullable,
1736 description: prop_description,
1737 default: prop_default,
1738 serde_attrs: Vec::new(),
1739 constraints: PropertyConstraints::from_schema_details(prop_details),
1740 },
1741 );
1742 continue;
1743 }
1744
1745 let context_name = self
1747 .current_schema_name
1748 .clone()
1749 .unwrap_or_else(|| "Unknown".to_string());
1750 let prop_pascal = self.to_pascal_case(prop_name);
1751 let mut union_type_name = format!("{context_name}{prop_pascal}");
1752 if self.schemas.contains_key(&union_type_name)
1754 || self.resolved_cache.contains_key(&union_type_name)
1755 {
1756 let mut suffix = 2;
1757 loop {
1758 let candidate = format!("{union_type_name}Union{suffix}");
1759 if !self.schemas.contains_key(&candidate)
1760 && !self.resolved_cache.contains_key(&candidate)
1761 {
1762 union_type_name = candidate;
1763 break;
1764 }
1765 suffix += 1;
1766 if suffix > 1000 {
1767 break;
1768 }
1769 }
1770 }
1771
1772 let union_schema_type = self.analyze_oneof_union(
1774 one_of,
1775 discriminator.as_ref(),
1776 &union_type_name,
1777 dependencies,
1778 )?;
1779
1780 self.resolved_cache.insert(
1782 union_type_name.clone(),
1783 AnalyzedSchema {
1784 name: union_type_name.clone(),
1785 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1786 schema_type: union_schema_type,
1787 dependencies: HashSet::new(),
1788 nullable: false,
1789 description: prop_schema.details().description.clone(),
1790 default: None,
1791 },
1792 );
1793
1794 dependencies.insert(union_type_name.clone());
1796 SchemaType::Reference {
1797 target: union_type_name,
1798 }
1799 } else {
1800 self.analyze_property_schema_with_context(
1802 prop_schema,
1803 Some(prop_name),
1804 dependencies,
1805 )?
1806 };
1807
1808 let prop_details = prop_schema.details();
1809 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1811 let prop_description = prop_details.description.clone();
1812 let prop_default = prop_details.default.clone();
1813
1814 property_info.insert(
1815 prop_name.clone(),
1816 PropertyInfo {
1817 schema_type: prop_type,
1818 nullable: prop_nullable,
1819 description: prop_description,
1820 default: prop_default,
1821 serde_attrs: Vec::new(),
1822 constraints: PropertyConstraints::from_schema_details(prop_details),
1823 },
1824 );
1825 }
1826 }
1827
1828 let typed_enabled = self
1836 .type_mapper
1837 .config()
1838 .shape
1839 .as_ref()
1840 .and_then(|s| s.additional_properties_typed)
1841 .unwrap_or(true);
1842
1843 let additional_properties = match &details.additional_properties {
1844 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
1845 ObjectAdditionalProperties::Untyped
1846 }
1847 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
1848 ObjectAdditionalProperties::Forbidden
1849 }
1850 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
1851 let analyzed =
1852 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
1853 ObjectAdditionalProperties::Typed {
1854 value_type: Box::new(analyzed),
1855 }
1856 }
1857 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1858 ObjectAdditionalProperties::Untyped
1860 }
1861 None => ObjectAdditionalProperties::Forbidden,
1862 };
1863
1864 Ok(SchemaType::Object {
1865 properties: property_info,
1866 required,
1867 additional_properties,
1868 })
1869 }
1870
1871 fn analyze_property_schema_with_context(
1872 &mut self,
1873 schema: &Schema,
1874 property_name: Option<&str>,
1875 dependencies: &mut HashSet<String>,
1876 ) -> Result<SchemaType> {
1877 if let Some(ref_str) = self.get_any_reference(schema) {
1878 let target_opt = if ref_str == "#" {
1879 Some(
1880 self.find_recursive_anchor_schema()
1881 .unwrap_or_else(|| "UnknownRecursive".to_string()),
1882 )
1883 } else {
1884 self.extract_schema_name(ref_str).map(|s| s.to_string())
1885 };
1886 match target_opt {
1887 Some(target) => {
1888 dependencies.insert(target.clone());
1889 return Ok(SchemaType::Reference { target });
1890 }
1891 None => {
1892 eprintln!(
1893 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1894 ref_str
1895 );
1896 return Ok(SchemaType::Primitive {
1897 rust_type: "serde_json::Value".to_string(),
1898 serde_with: None,
1899 });
1900 }
1901 }
1902 }
1903
1904 if let Some(schema_type) = schema.schema_type() {
1905 match schema_type {
1906 OpenApiSchemaType::String => {
1907 if let Some(enum_values) = schema.details().string_enum_values() {
1909 let context_name = self
1912 .current_schema_name
1913 .clone()
1914 .unwrap_or_else(|| "Unknown".to_string());
1915
1916 let primary_name = if let Some(prop_name) = property_name {
1918 let prop_pascal = self.to_pascal_case(prop_name);
1920 format!("{context_name}{prop_pascal}")
1921 } else {
1922 let suffix = if !enum_values.is_empty() {
1925 let first_value = self.to_pascal_case(&enum_values[0]);
1926 format!("{first_value}Enum")
1927 } else {
1928 "StringEnum".to_string()
1929 };
1930 format!("{context_name}{suffix}")
1931 };
1932
1933 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
1953 matches!(
1954 &existing.schema_type,
1955 SchemaType::StringEnum { values: existing_values }
1956 if existing_values == values
1957 )
1958 }
1959
1960 let mut enum_type_name = primary_name.clone();
1961 let mut should_insert = match self.resolved_cache.get(&enum_type_name) {
1962 None => true,
1963 Some(existing) if matches_values(existing, &enum_values) => false,
1964 Some(_) => {
1965 let suffix = enum_values
1968 .first()
1969 .map(|v| self.to_pascal_case(v))
1970 .unwrap_or_else(|| "Variant".to_string());
1971 let candidate = format!("{primary_name}{suffix}");
1972
1973 let resolved = match self.resolved_cache.get(&candidate) {
1974 None => Some((candidate.clone(), true)),
1975 Some(existing) if matches_values(existing, &enum_values) => {
1976 Some((candidate.clone(), false))
1977 }
1978 Some(_) => {
1979 let mut found = None;
1982 for n in 2..1000 {
1983 let numbered = format!("{candidate}_{n}");
1984 match self.resolved_cache.get(&numbered) {
1985 None => {
1986 found = Some((numbered, true));
1987 break;
1988 }
1989 Some(existing)
1990 if matches_values(existing, &enum_values) =>
1991 {
1992 found = Some((numbered, false));
1993 break;
1994 }
1995 Some(_) => continue,
1996 }
1997 }
1998 found
1999 }
2000 };
2001
2002 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
2003 enum_type_name = resolved_name;
2004 insert
2005 }
2006 };
2007
2008 if should_insert {
2011 self.resolved_cache.insert(
2012 enum_type_name.clone(),
2013 AnalyzedSchema {
2014 name: enum_type_name.clone(),
2015 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2016 schema_type: SchemaType::StringEnum {
2017 values: enum_values,
2018 },
2019 dependencies: HashSet::new(),
2020 nullable: false,
2021 description: schema.details().description.clone(),
2022 default: schema.details().default.clone(),
2023 },
2024 );
2025 let _ = &mut should_insert;
2028 }
2029
2030 dependencies.insert(enum_type_name.clone());
2032 return Ok(SchemaType::Reference {
2033 target: enum_type_name,
2034 });
2035 } else {
2036 let mapped = self
2042 .type_mapper
2043 .string_format(schema.details().format.as_deref());
2044 return Ok(SchemaType::Primitive {
2045 rust_type: mapped.rust_type,
2046 serde_with: mapped.serde_with,
2047 });
2048 }
2049 }
2050 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2051 let details = schema.details();
2052 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2053 return Ok(SchemaType::Primitive {
2054 rust_type,
2055 serde_with: None,
2056 });
2057 }
2058 OpenApiSchemaType::Boolean => {
2059 return Ok(SchemaType::Primitive {
2060 rust_type: "bool".to_string(),
2061 serde_with: None,
2062 });
2063 }
2064 OpenApiSchemaType::Array => {
2065 let context_name = if let Some(prop_name) = property_name {
2067 let prop_pascal = self.to_pascal_case(prop_name);
2069 format!(
2070 "{}{}",
2071 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2072 prop_pascal
2073 )
2074 } else {
2075 "ArrayItem".to_string()
2077 };
2078 return self.analyze_array_schema(schema, &context_name, dependencies);
2079 }
2080 OpenApiSchemaType::Object => {
2081 if self.should_use_dynamic_json(schema) {
2083 return Ok(SchemaType::Primitive {
2084 rust_type: "serde_json::Value".to_string(),
2085 serde_with: None,
2086 });
2087 }
2088 let object_type_name = if let Some(prop_name) = property_name {
2090 let prop_pascal = self.to_pascal_case(prop_name);
2092 format!(
2093 "{}{}",
2094 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2095 prop_pascal
2096 )
2097 } else {
2098 format!(
2100 "{}Object",
2101 self.current_schema_name.as_deref().unwrap_or("Unknown")
2102 )
2103 };
2104
2105 let object_type = self.analyze_object_schema(schema, dependencies)?;
2107
2108 let inline_schema = AnalyzedSchema {
2110 name: object_type_name.clone(),
2111 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2112 schema_type: object_type,
2113 dependencies: dependencies.clone(),
2114 nullable: false,
2115 description: schema.details().description.clone(),
2116 default: None,
2117 };
2118
2119 self.resolved_cache
2121 .insert(object_type_name.clone(), inline_schema);
2122 dependencies.insert(object_type_name.clone());
2123
2124 return Ok(SchemaType::Reference {
2126 target: object_type_name,
2127 });
2128 }
2129 _ => {
2130 return Ok(SchemaType::Primitive {
2131 rust_type: "serde_json::Value".to_string(),
2132 serde_with: None,
2133 });
2134 }
2135 }
2136 }
2137
2138 if schema.is_nullable_pattern() {
2140 if let Some(non_null) = schema.non_null_variant() {
2141 return self.analyze_property_schema_with_context(
2142 non_null,
2143 property_name,
2144 dependencies,
2145 );
2146 }
2147 }
2148
2149 if self.should_use_dynamic_json(schema) {
2151 return Ok(SchemaType::Primitive {
2152 rust_type: "serde_json::Value".to_string(),
2153 serde_with: None,
2154 });
2155 }
2156
2157 if let Schema::AllOf { all_of, .. } = schema {
2159 return self.analyze_allof_composition(all_of, dependencies);
2160 }
2161
2162 if let Some(variants) = schema.union_variants() {
2164 match variants.len().cmp(&1) {
2165 std::cmp::Ordering::Equal => {
2166 return self.analyze_property_schema_with_context(
2168 &variants[0],
2169 property_name,
2170 dependencies,
2171 );
2172 }
2173 std::cmp::Ordering::Greater => {
2174 let union_name = if let Some(prop_name) = property_name {
2177 let prop_pascal = self.to_pascal_case(prop_name);
2179 format!(
2180 "{}{}",
2181 self.current_schema_name.as_deref().unwrap_or(""),
2182 prop_pascal
2183 )
2184 } else {
2185 "UnionType".to_string()
2186 };
2187
2188 if let Schema::OneOf {
2190 one_of,
2191 discriminator,
2192 ..
2193 } = schema
2194 {
2195 let oneof_result = self.analyze_oneof_union(
2197 one_of,
2198 discriminator.as_ref(),
2199 &union_name,
2200 dependencies,
2201 )?;
2202
2203 if let SchemaType::Union {
2205 variants: _union_variants,
2206 } = &oneof_result
2207 {
2208 self.resolved_cache.insert(
2210 union_name.clone(),
2211 AnalyzedSchema {
2212 name: union_name.clone(),
2213 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2214 schema_type: oneof_result.clone(),
2215 dependencies: dependencies.clone(),
2216 nullable: false,
2217 description: schema.details().description.clone(),
2218 default: None,
2219 },
2220 );
2221
2222 dependencies.insert(union_name.clone());
2224 return Ok(SchemaType::Reference { target: union_name });
2225 }
2226
2227 return Ok(oneof_result);
2228 } else if let Schema::AnyOf {
2229 any_of,
2230 discriminator,
2231 ..
2232 } = schema
2233 {
2234 let union_analysis = self.analyze_anyof_union(
2236 any_of,
2237 discriminator.as_ref(),
2238 dependencies,
2239 &union_name,
2240 )?;
2241 return Ok(union_analysis);
2242 } else {
2243 let mut union_variants = Vec::new();
2246 for variant in variants {
2247 if let Some(ref_str) = variant.reference() {
2248 if let Some(target) = self.extract_schema_name(ref_str) {
2249 dependencies.insert(target.to_string());
2250 union_variants.push(SchemaRef {
2251 target: target.to_string(),
2252 nullable: false,
2253 });
2254 }
2255 }
2256 }
2257 return Ok(SchemaType::Union {
2258 variants: union_variants,
2259 });
2260 }
2261 }
2262 std::cmp::Ordering::Less => {}
2263 }
2264 }
2265
2266 if let Some(inferred_type) = schema.inferred_type() {
2268 match inferred_type {
2269 OpenApiSchemaType::Object => {
2270 if self.should_use_dynamic_json(schema) {
2272 return Ok(SchemaType::Primitive {
2273 rust_type: "serde_json::Value".to_string(),
2274 serde_with: None,
2275 });
2276 }
2277 return self.analyze_object_schema(schema, dependencies);
2278 }
2279 OpenApiSchemaType::Array => {
2280 let context_name = if let Some(prop_name) = property_name {
2281 let prop_pascal = self.to_pascal_case(prop_name);
2283 format!(
2284 "{}{}",
2285 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2286 prop_pascal
2287 )
2288 } else {
2289 "ArrayItem".to_string()
2291 };
2292 return self.analyze_array_schema(schema, &context_name, dependencies);
2293 }
2294 OpenApiSchemaType::String => {
2295 if let Some(enum_values) = schema.details().string_enum_values() {
2296 return Ok(SchemaType::StringEnum {
2297 values: enum_values,
2298 });
2299 } else {
2300 return Ok(SchemaType::Primitive {
2301 rust_type: "String".to_string(),
2302 serde_with: None,
2303 });
2304 }
2305 }
2306 _ => {
2307 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2309 return Ok(SchemaType::Primitive {
2310 rust_type,
2311 serde_with: None,
2312 });
2313 }
2314 }
2315 }
2316
2317 Ok(SchemaType::Primitive {
2318 rust_type: "serde_json::Value".to_string(),
2319 serde_with: None,
2320 })
2321 }
2322
2323 fn analyze_allof_composition(
2324 &mut self,
2325 all_of_schemas: &[Schema],
2326 dependencies: &mut HashSet<String>,
2327 ) -> Result<SchemaType> {
2328 if all_of_schemas.len() == 1 {
2331 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
2332 if let Some(target) = self.extract_schema_name(reference) {
2333 dependencies.insert(target.to_string());
2334 return Ok(SchemaType::Reference {
2335 target: target.to_string(),
2336 });
2337 }
2338 }
2339 }
2340
2341 let mut merged_properties = BTreeMap::new();
2343 let mut merged_required = HashSet::new();
2344 let mut descriptions = Vec::new();
2345
2346 let current_context = self.current_schema_name.clone();
2348
2349 for schema in all_of_schemas {
2350 match schema {
2351 Schema::Reference { reference, .. } => {
2352 if let Some(target) = self.extract_schema_name(reference) {
2354 dependencies.insert(target.to_string());
2355
2356 let analyzed_ref = self.analyze_schema(target)?;
2358
2359 match &analyzed_ref.schema_type {
2361 SchemaType::Object {
2362 properties,
2363 required,
2364 ..
2365 } => {
2366 for (prop_name, prop_info) in properties {
2368 merged_properties.insert(prop_name.clone(), prop_info.clone());
2369 }
2370 for req in required {
2372 merged_required.insert(req.clone());
2373 }
2374 }
2375 _ => {
2376 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2378 self.merge_schema_into_properties(
2379 &ref_schema,
2380 &mut merged_properties,
2381 &mut merged_required,
2382 dependencies,
2383 )?;
2384 }
2385 }
2386 }
2387 }
2388 }
2389 Schema::Typed {
2390 schema_type: OpenApiSchemaType::Object,
2391 ..
2392 }
2393 | Schema::Untyped { .. } => {
2394 let saved_context = self.current_schema_name.clone();
2396 self.current_schema_name = current_context.clone();
2397
2398 self.merge_schema_into_properties(
2400 schema,
2401 &mut merged_properties,
2402 &mut merged_required,
2403 dependencies,
2404 )?;
2405
2406 self.current_schema_name = saved_context;
2408 }
2409 _ => {
2410 self.merge_schema_into_properties(
2413 schema,
2414 &mut merged_properties,
2415 &mut merged_required,
2416 dependencies,
2417 )?;
2418 }
2419 }
2420
2421 if let Some(desc) = &schema.details().description {
2423 descriptions.push(desc.clone());
2424 }
2425 }
2426
2427 if !merged_properties.is_empty() {
2429 Ok(SchemaType::Object {
2430 properties: merged_properties,
2431 required: merged_required,
2432 additional_properties: ObjectAdditionalProperties::Forbidden,
2433 })
2434 } else {
2435 Ok(SchemaType::Composition {
2437 schemas: all_of_schemas
2438 .iter()
2439 .filter_map(|s| {
2440 if let Some(ref_str) = s.reference() {
2441 if let Some(target) = self.extract_schema_name(ref_str) {
2442 dependencies.insert(target.to_string());
2443 Some(SchemaRef {
2444 target: target.to_string(),
2445 nullable: false,
2446 })
2447 } else {
2448 None
2449 }
2450 } else {
2451 None
2452 }
2453 })
2454 .collect(),
2455 })
2456 }
2457 }
2458
2459 fn merge_schema_into_properties(
2460 &mut self,
2461 schema: &Schema,
2462 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2463 merged_required: &mut HashSet<String>,
2464 dependencies: &mut HashSet<String>,
2465 ) -> Result<()> {
2466 let details = schema.details();
2467
2468 if let Some(properties) = &details.properties {
2470 for (prop_name, prop_schema) in properties {
2471 let prop_type = self.analyze_property_schema_with_context(
2472 prop_schema,
2473 Some(prop_name),
2474 dependencies,
2475 )?;
2476 let prop_details = prop_schema.details();
2477
2478 let nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
2484 merged_properties.insert(
2485 prop_name.clone(),
2486 PropertyInfo {
2487 schema_type: prop_type,
2488 nullable,
2489 description: prop_details.description.clone(),
2490 default: prop_details.default.clone(),
2491 serde_attrs: Vec::new(),
2492 constraints: PropertyConstraints::from_schema_details(prop_details),
2493 },
2494 );
2495 }
2496 }
2497
2498 if let Some(required) = &details.required {
2500 for field in required {
2501 merged_required.insert(field.clone());
2502 }
2503 }
2504
2505 Ok(())
2506 }
2507
2508 fn analyze_oneof_union(
2509 &mut self,
2510 one_of_schemas: &[Schema],
2511 discriminator: Option<&crate::openapi::Discriminator>,
2512 parent_name: &str,
2513 dependencies: &mut HashSet<String>,
2514 ) -> Result<SchemaType> {
2515 if one_of_schemas.len() == 2 {
2518 let null_count = one_of_schemas
2519 .iter()
2520 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2521 .count();
2522 if null_count == 1 {
2523 if let Some(non_null) = one_of_schemas
2524 .iter()
2525 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2526 {
2527 return self
2528 .analyze_schema_value(non_null, parent_name)
2529 .map(|a| a.schema_type);
2530 }
2531 }
2532 }
2533
2534 if discriminator.is_none() {
2536 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2538 }
2539
2540 if one_of_schemas
2546 .iter()
2547 .any(|s| !self.branch_resolves_to_object(s))
2548 {
2549 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2550 }
2551
2552 let discriminator_field = discriminator
2554 .ok_or_else(|| {
2555 GeneratorError::InvalidDiscriminator(
2556 "expected discriminator after guard check".to_string(),
2557 )
2558 })?
2559 .property_name
2560 .clone();
2561
2562 let mut variants = Vec::new();
2563 let mut used_variant_names = std::collections::HashSet::new();
2564
2565 for variant_schema in one_of_schemas {
2566 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2568 Some((ref_str, false))
2569 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2570 Some((recursive_ref, true))
2571 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2572 if all_of.len() == 1 {
2574 if let Some(ref_str) = all_of[0].reference() {
2575 Some((ref_str, false))
2576 } else {
2577 all_of[0]
2578 .recursive_reference()
2579 .map(|recursive_ref| (recursive_ref, true))
2580 }
2581 } else {
2582 None
2583 }
2584 } else {
2585 None
2586 };
2587
2588 if let Some((ref_str, is_recursive)) = ref_info {
2589 let schema_name = if is_recursive && ref_str == "#" {
2590 self.find_recursive_anchor_schema()
2592 .or_else(|| self.current_schema_name.clone())
2593 .unwrap_or_else(|| "CompoundFilter".to_string())
2594 } else {
2595 self.extract_schema_name(ref_str)
2596 .map(|s| s.to_string())
2597 .unwrap_or_else(|| "UnknownRef".to_string())
2598 };
2599
2600 if !schema_name.is_empty() {
2601 dependencies.insert(schema_name.clone());
2602
2603 let discriminator_value = if let Some(disc) = discriminator {
2608 if let Some(mappings) = &disc.mapping {
2609 mappings
2612 .iter()
2613 .find(|(_, target_ref)| {
2614 target_ref.as_str() == ref_str
2616 || self
2617 .extract_schema_name(target_ref)
2618 .map(|s| s.to_string())
2619 == Some(schema_name.clone())
2620 })
2621 .map(|(key, _)| key.clone())
2622 .unwrap_or_else(|| {
2623 self.fallback_discriminator_value_for_field(
2624 &schema_name,
2625 &discriminator_field,
2626 )
2627 })
2628 } else {
2629 self.fallback_discriminator_value_for_field(
2630 &schema_name,
2631 &discriminator_field,
2632 )
2633 }
2634 } else {
2635 self.fallback_discriminator_value_for_field(
2636 &schema_name,
2637 &discriminator_field,
2638 )
2639 };
2640
2641 let base_name = self.to_rust_variant_name(&schema_name);
2643 let rust_name =
2644 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2645
2646 let final_discriminator_value = discriminator_value;
2648
2649 variants.push(UnionVariant {
2650 rust_name,
2651 type_name: schema_name,
2652 discriminator_value: final_discriminator_value,
2653 schema_ref: ref_str.to_string(),
2654 });
2655 }
2656 } else {
2657 let variant_index = variants.len();
2659 let inline_type_name =
2660 self.generate_inline_type_name(variant_schema, variant_index);
2661
2662 let discriminator_value = if let Some(disc) = discriminator {
2664 if let Some(mappings) = &disc.mapping {
2665 mappings
2667 .iter()
2668 .find(|(_, target_ref)| {
2669 target_ref.contains(&format!("variant_{variant_index}"))
2670 })
2671 .map(|(key, _)| key.clone())
2672 .unwrap_or_else(|| {
2673 self.extract_inline_discriminator_value(
2674 variant_schema,
2675 &discriminator_field,
2676 variant_index,
2677 )
2678 })
2679 } else {
2680 self.extract_inline_discriminator_value(
2681 variant_schema,
2682 &discriminator_field,
2683 variant_index,
2684 )
2685 }
2686 } else {
2687 self.extract_inline_discriminator_value(
2688 variant_schema,
2689 &discriminator_field,
2690 variant_index,
2691 )
2692 };
2693
2694 let base_name = if discriminator_value.starts_with("variant_") {
2696 format!("Variant{variant_index}")
2697 } else {
2698 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2700 self.to_rust_variant_name(&clean_name)
2701 };
2702 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2703
2704 let final_discriminator_value = discriminator_value;
2706
2707 variants.push(UnionVariant {
2708 rust_name,
2709 type_name: inline_type_name.clone(),
2710 discriminator_value: final_discriminator_value,
2711 schema_ref: format!("inline_{variant_index}"),
2712 });
2713
2714 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2716 }
2717 }
2718
2719 if variants.is_empty() {
2720 let mut union_variants = Vec::new();
2723
2724 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2725 if let Some(ref_str) = variant_schema.reference() {
2727 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2728 dependencies.insert(schema_name.to_string());
2729 union_variants.push(SchemaRef {
2730 target: schema_name.to_string(),
2731 nullable: false,
2732 });
2733 }
2734 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2735 let schema_name = if recursive_ref == "#" {
2736 self.find_recursive_anchor_schema()
2738 .or_else(|| self.current_schema_name.clone())
2739 .unwrap_or_else(|| "CompoundFilter".to_string())
2740 } else {
2741 self.extract_schema_name(recursive_ref)
2742 .map(|s| s.to_string())
2743 .unwrap_or_else(|| "RecursiveType".to_string())
2744 };
2745 dependencies.insert(schema_name.clone());
2746 union_variants.push(SchemaRef {
2747 target: schema_name,
2748 nullable: false,
2749 });
2750 } else {
2751 let inline_name = self.generate_context_aware_name(
2753 parent_name,
2754 "InlineVariant",
2755 variant_index,
2756 Some(variant_schema),
2757 );
2758 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2759 let variant_type = analyzed.schema_type;
2760
2761 for dep in &analyzed.dependencies {
2763 dependencies.insert(dep.clone());
2764 }
2765
2766 match &variant_type {
2767 SchemaType::Primitive { rust_type, .. } => {
2769 union_variants.push(SchemaRef {
2770 target: rust_type.clone(),
2771 nullable: false,
2772 });
2773 }
2774 SchemaType::Array { item_type } => {
2776 match item_type.as_ref() {
2777 SchemaType::Primitive { rust_type, .. } => {
2778 let type_name = format!("Vec<{rust_type}>");
2779 union_variants.push(SchemaRef {
2780 target: type_name,
2781 nullable: false,
2782 });
2783 }
2784 SchemaType::Reference { target } => {
2785 let type_name = format!("Vec<{target}>");
2786 union_variants.push(SchemaRef {
2787 target: type_name,
2788 nullable: false,
2789 });
2790 }
2791 _ => {
2792 let inline_type_name = self.generate_context_aware_name(
2794 parent_name,
2795 "Variant",
2796 variant_index,
2797 None,
2798 );
2799 self.add_inline_schema(
2800 &inline_type_name,
2801 variant_schema,
2802 dependencies,
2803 )?;
2804 union_variants.push(SchemaRef {
2805 target: inline_type_name,
2806 nullable: false,
2807 });
2808 }
2809 }
2810 }
2811 SchemaType::Reference { target } => {
2813 union_variants.push(SchemaRef {
2814 target: target.clone(),
2815 nullable: false,
2816 });
2817 }
2818 _ => {
2820 let inline_type_name =
2821 format!("{}Variant{}", parent_name, variant_index + 1);
2822 self.add_inline_schema(
2823 &inline_type_name,
2824 variant_schema,
2825 dependencies,
2826 )?;
2827 union_variants.push(SchemaRef {
2828 target: inline_type_name,
2829 nullable: false,
2830 });
2831 }
2832 }
2833 }
2834 }
2835
2836 if !union_variants.is_empty() {
2837 return Ok(SchemaType::Union {
2838 variants: union_variants,
2839 });
2840 }
2841
2842 return Ok(SchemaType::Primitive {
2844 rust_type: "serde_json::Value".to_string(),
2845 serde_with: None,
2846 });
2847 }
2848
2849 Ok(SchemaType::DiscriminatedUnion {
2850 discriminator_field,
2851 variants,
2852 })
2853 }
2854
2855 fn analyze_untagged_oneof_union(
2856 &mut self,
2857 one_of_schemas: &[Schema],
2858 parent_name: &str,
2859 dependencies: &mut HashSet<String>,
2860 ) -> Result<SchemaType> {
2861 let filtered: Vec<&Schema> = one_of_schemas
2865 .iter()
2866 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2867 .collect();
2868
2869 if filtered.len() == 1 {
2871 return self
2872 .analyze_schema_value(filtered[0], parent_name)
2873 .map(|a| a.schema_type);
2874 }
2875
2876 let mut union_variants = Vec::new();
2877
2878 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2879 if let Some(ref_str) = variant_schema.reference() {
2881 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2882 dependencies.insert(schema_name.to_string());
2883 union_variants.push(SchemaRef {
2884 target: schema_name.to_string(),
2885 nullable: false,
2886 });
2887 }
2888 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2889 let schema_name = if recursive_ref == "#" {
2890 self.find_recursive_anchor_schema()
2892 .or_else(|| self.current_schema_name.clone())
2893 .unwrap_or_else(|| "CompoundFilter".to_string())
2894 } else {
2895 self.extract_schema_name(recursive_ref)
2896 .map(|s| s.to_string())
2897 .unwrap_or_else(|| "RecursiveType".to_string())
2898 };
2899 dependencies.insert(schema_name.clone());
2900 union_variants.push(SchemaRef {
2901 target: schema_name,
2902 nullable: false,
2903 });
2904 } else {
2905 let inline_name = self.generate_context_aware_name(
2907 parent_name,
2908 "InlineVariant",
2909 variant_index,
2910 Some(variant_schema),
2911 );
2912 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2913 let variant_type = analyzed.schema_type;
2914
2915 for dep in &analyzed.dependencies {
2917 dependencies.insert(dep.clone());
2918 }
2919
2920 match &variant_type {
2921 SchemaType::Primitive { rust_type, .. } => {
2923 union_variants.push(SchemaRef {
2924 target: rust_type.clone(),
2925 nullable: false,
2926 });
2927 }
2928 SchemaType::Array { item_type } => {
2930 match item_type.as_ref() {
2931 SchemaType::Primitive { rust_type, .. } => {
2932 let type_name = format!("Vec<{rust_type}>");
2933 union_variants.push(SchemaRef {
2934 target: type_name,
2935 nullable: false,
2936 });
2937 }
2938 SchemaType::Reference { target } => {
2939 let type_name = format!("Vec<{target}>");
2940 union_variants.push(SchemaRef {
2941 target: type_name,
2942 nullable: false,
2943 });
2944 }
2945 SchemaType::Array {
2947 item_type: inner_item_type,
2948 } => {
2949 match inner_item_type.as_ref() {
2950 SchemaType::Primitive { rust_type, .. } => {
2951 let type_name = format!("Vec<Vec<{rust_type}>>");
2952 union_variants.push(SchemaRef {
2953 target: type_name,
2954 nullable: false,
2955 });
2956 }
2957 SchemaType::Reference { target } => {
2958 let type_name = format!("Vec<Vec<{target}>>");
2959 union_variants.push(SchemaRef {
2960 target: type_name,
2961 nullable: false,
2962 });
2963 }
2964 _ => {
2965 let inline_type_name = self.generate_context_aware_name(
2967 parent_name,
2968 "Variant",
2969 variant_index,
2970 None,
2971 );
2972 self.add_inline_schema(
2973 &inline_type_name,
2974 variant_schema,
2975 dependencies,
2976 )?;
2977 union_variants.push(SchemaRef {
2978 target: inline_type_name,
2979 nullable: false,
2980 });
2981 }
2982 }
2983 }
2984 _ => {
2985 let inline_type_name = self.generate_context_aware_name(
2987 parent_name,
2988 "Variant",
2989 variant_index,
2990 None,
2991 );
2992 self.add_inline_schema(
2993 &inline_type_name,
2994 variant_schema,
2995 dependencies,
2996 )?;
2997 union_variants.push(SchemaRef {
2998 target: inline_type_name,
2999 nullable: false,
3000 });
3001 }
3002 }
3003 }
3004 SchemaType::Reference { target } => {
3006 union_variants.push(SchemaRef {
3007 target: target.clone(),
3008 nullable: false,
3009 });
3010 }
3011 _ => {
3013 let inline_type_name = self.generate_context_aware_name(
3014 parent_name,
3015 "Variant",
3016 variant_index,
3017 None,
3018 );
3019 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3020 union_variants.push(SchemaRef {
3021 target: inline_type_name,
3022 nullable: false,
3023 });
3024 }
3025 }
3026 }
3027 }
3028
3029 if !union_variants.is_empty() {
3030 return Ok(SchemaType::Union {
3031 variants: union_variants,
3032 });
3033 }
3034
3035 Ok(SchemaType::Primitive {
3037 rust_type: "serde_json::Value".to_string(),
3038 serde_with: None,
3039 })
3040 }
3041
3042 fn add_inline_schema(
3043 &mut self,
3044 type_name: &str,
3045 schema: &Schema,
3046 dependencies: &mut HashSet<String>,
3047 ) -> Result<()> {
3048 if let Some(schema_type) = schema.schema_type() {
3050 match schema_type {
3051 OpenApiSchemaType::String
3052 | OpenApiSchemaType::Integer
3053 | OpenApiSchemaType::Number
3054 | OpenApiSchemaType::Boolean => {
3055 let rust_type =
3056 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3057
3058 self.resolved_cache.insert(
3060 type_name.to_string(),
3061 AnalyzedSchema {
3062 name: type_name.to_string(),
3063 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3064 schema_type: SchemaType::Primitive {
3065 rust_type,
3066 serde_with: None,
3067 },
3068 dependencies: HashSet::new(),
3069 nullable: false,
3070 description: schema.details().description.clone(),
3071 default: None,
3072 },
3073 );
3074 return Ok(());
3075 }
3076 _ => {}
3077 }
3078 }
3079
3080 let previous_schema_name = self.current_schema_name.take();
3084 self.current_schema_name = Some(type_name.to_string());
3085 let analyzed = self.analyze_schema_value(schema, type_name)?;
3086 self.current_schema_name = previous_schema_name;
3087
3088 self.resolved_cache.insert(type_name.to_string(), analyzed);
3090
3091 if let Some(cached) = self.resolved_cache.get(type_name) {
3093 for dep in &cached.dependencies {
3094 dependencies.insert(dep.clone());
3095 }
3096 }
3097
3098 Ok(())
3099 }
3100
3101 fn extract_inline_discriminator_value(
3102 &self,
3103 schema: &Schema,
3104 discriminator_field: &str,
3105 variant_index: usize,
3106 ) -> String {
3107 if let Some(properties) = &schema.details().properties {
3109 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3110 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3112 if enum_values.len() == 1 {
3113 if let Some(value) = enum_values[0].as_str() {
3114 return value.to_string();
3115 }
3116 }
3117 }
3118 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3120 if let Some(value) = const_value.as_str() {
3121 return value.to_string();
3122 }
3123 }
3124 if let Some(const_value) = &discriminator_prop.details().const_value {
3126 if let Some(value) = const_value.as_str() {
3127 return value.to_string();
3128 }
3129 }
3130 }
3131 }
3132
3133 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3135 return inferred_name;
3136 }
3137
3138 format!("variant_{variant_index}")
3140 }
3141
3142 fn infer_variant_name_from_structure(
3143 &self,
3144 schema: &Schema,
3145 _variant_index: usize,
3146 ) -> Option<String> {
3147 let details = schema.details();
3148
3149 if let Some(properties) = &details.properties {
3151 if properties.contains_key("text") && properties.len() <= 3 {
3153 return Some("text".to_string());
3154 }
3155 if properties.contains_key("image") || properties.contains_key("source") {
3156 return Some("image".to_string());
3157 }
3158 if properties.contains_key("document") {
3159 return Some("document".to_string());
3160 }
3161 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3162 return Some("tool_result".to_string());
3163 }
3164 if properties.contains_key("content") && properties.contains_key("is_error") {
3165 return Some("tool_result".to_string());
3166 }
3167 if properties.contains_key("partial_json") {
3168 return Some("partial_json".to_string());
3169 }
3170
3171 let property_names: Vec<&String> = properties.keys().collect();
3173
3174 for prop_name in &property_names {
3176 if prop_name.contains("result") {
3177 return Some("result".to_string());
3178 }
3179 if prop_name.contains("error") {
3180 return Some("error".to_string());
3181 }
3182 if prop_name.contains("content") && property_names.len() <= 2 {
3183 return Some("content".to_string());
3184 }
3185 }
3186
3187 let significant_props = property_names
3189 .iter()
3190 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3191 .collect::<Vec<_>>();
3192
3193 if significant_props.len() == 1 {
3194 return Some((*significant_props[0]).clone());
3195 }
3196 }
3197
3198 if let Some(description) = &details.description {
3200 let desc_lower = description.to_lowercase();
3201 if desc_lower.contains("text") && desc_lower.len() < 100 {
3202 return Some("text".to_string());
3203 }
3204 if desc_lower.contains("image") {
3205 return Some("image".to_string());
3206 }
3207 if desc_lower.contains("document") {
3208 return Some("document".to_string());
3209 }
3210 if desc_lower.contains("tool") && desc_lower.contains("result") {
3211 return Some("tool_result".to_string());
3212 }
3213 }
3214
3215 None
3216 }
3217
3218 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3219 if discriminator.is_empty() {
3221 return "Variant".to_string();
3222 }
3223
3224 let mut result = String::new();
3225 let mut next_upper = true;
3226
3227 for c in discriminator.chars() {
3228 match c {
3229 'a'..='z' => {
3230 if next_upper {
3231 result.push(c.to_ascii_uppercase());
3232 next_upper = false;
3233 } else {
3234 result.push(c);
3235 }
3236 }
3237 'A'..='Z' => {
3238 result.push(c);
3239 next_upper = false;
3240 }
3241 '0'..='9' => {
3242 result.push(c);
3243 next_upper = false;
3244 }
3245 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3246 next_upper = true;
3248 }
3249 _ => {
3250 next_upper = true;
3252 }
3253 }
3254 }
3255
3256 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3258 result = format!("Variant{result}");
3259 }
3260
3261 result
3262 }
3263
3264 fn ensure_unique_variant_name(
3265 &self,
3266 base_name: String,
3267 used_names: &mut std::collections::HashSet<String>,
3268 ) -> String {
3269 let mut candidate = base_name.clone();
3270 let mut counter = 1;
3271
3272 while used_names.contains(&candidate) {
3273 counter += 1;
3274 candidate = format!("{base_name}{counter}");
3275 }
3276
3277 used_names.insert(candidate.clone());
3278 candidate
3279 }
3280
3281 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3282 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3284 return meaningful_name;
3285 }
3286
3287 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3289 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3290 }
3291
3292 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3293 let details = schema.details();
3294
3295 if let Some(description) = &details.description {
3297 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3298 return Some(name_from_desc);
3299 }
3300 }
3301
3302 if let Some(properties) = &details.properties {
3304 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3305 return Some(format!("{name_from_props}Block"));
3306 }
3307 }
3308
3309 None
3310 }
3311
3312 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3313 if description.len() > 100 || description.contains('\n') {
3315 return None;
3316 }
3317
3318 let words: Vec<&str> = description
3320 .split_whitespace()
3321 .take(2) .filter(|word| {
3323 let w = word.to_lowercase();
3324 word.len() > 2
3325 && ![
3326 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3327 ]
3328 .contains(&w.as_str())
3329 })
3330 .collect();
3331
3332 if words.is_empty() {
3333 return None;
3334 }
3335
3336 let combined = words.join("_");
3338 let pascal_name = self.discriminator_to_variant_name(&combined);
3339
3340 if !pascal_name.ends_with("Content")
3342 && !pascal_name.ends_with("Block")
3343 && !pascal_name.ends_with("Type")
3344 {
3345 Some(format!("{pascal_name}Content"))
3346 } else {
3347 Some(pascal_name)
3348 }
3349 }
3350
3351 fn extract_type_name_from_properties(
3352 &self,
3353 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3354 ) -> Option<String> {
3355 let significant_props: Vec<&String> = properties
3357 .keys()
3358 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3359 .collect();
3360
3361 if significant_props.is_empty() {
3362 return None;
3363 }
3364
3365 if significant_props.len() == 1 {
3367 let prop_name = significant_props[0];
3368 return Some(self.discriminator_to_variant_name(prop_name));
3369 }
3370
3371 let mut sorted_props = significant_props.clone();
3374 sorted_props.sort();
3375 if let Some(first_prop) = sorted_props.first() {
3376 return Some(self.discriminator_to_variant_name(first_prop));
3377 }
3378
3379 None
3380 }
3381
3382 fn openapi_type_to_rust_type(
3383 &self,
3384 openapi_type: OpenApiSchemaType,
3385 details: &crate::openapi::SchemaDetails,
3386 ) -> String {
3387 self.type_mapper.map(openapi_type, details).rust_type
3392 }
3393
3394 #[allow(dead_code)]
3395 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3396 self.fallback_discriminator_value_for_field(schema_name, "type")
3397 }
3398
3399 fn fallback_discriminator_value_for_field(
3400 &self,
3401 schema_name: &str,
3402 field_name: &str,
3403 ) -> String {
3404 if let Some(ref_schema) = self.schemas.get(schema_name) {
3406 if let Some(extracted) =
3407 self.extract_discriminator_value_for_field(ref_schema, field_name)
3408 {
3409 return extracted;
3410 }
3411 }
3412
3413 self.generate_discriminator_value_from_name(schema_name)
3415 }
3416
3417 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3418 let mut result = String::new();
3420 let mut chars = schema_name.chars().peekable();
3421 let mut first = true;
3422
3423 while let Some(c) = chars.next() {
3424 if c.is_uppercase()
3425 && !first
3426 && chars
3427 .peek()
3428 .map(|&next| next.is_lowercase())
3429 .unwrap_or(false)
3430 {
3431 result.push('.');
3432 }
3433 result.push(c.to_ascii_lowercase());
3434 first = false;
3435 }
3436
3437 if result.ends_with("event") {
3439 result = result[..result.len() - 5].to_string();
3440 }
3441
3442 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3444 result = format!("response.{}", result.trim_start_matches("response"));
3445 }
3446
3447 result
3448 }
3449
3450 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3451 let mut name = schema_name;
3453
3454 if name.starts_with("Response") && name.len() > 8 {
3456 name = &name[8..]; }
3458
3459 if name.ends_with("Event") && name.len() > 5 {
3461 name = &name[..name.len() - 5]; }
3463
3464 name = name.trim_matches('_');
3466
3467 if name.is_empty() {
3469 schema_name.to_string()
3470 } else {
3471 self.discriminator_to_variant_name(name)
3473 }
3474 }
3475
3476 fn analyze_array_schema(
3477 &mut self,
3478 schema: &Schema,
3479 parent_schema_name: &str,
3480 dependencies: &mut HashSet<String>,
3481 ) -> Result<SchemaType> {
3482 let details = schema.details();
3483
3484 if let Some(items_schema) = &details.items {
3486 let item_type = match items_schema.as_ref() {
3488 Schema::Reference { reference, .. } => {
3489 let target = self
3491 .extract_schema_name(reference)
3492 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3493 .to_string();
3494 dependencies.insert(target.clone());
3495 SchemaType::Reference { target }
3496 }
3497 Schema::RecursiveRef { recursive_ref, .. } => {
3498 if recursive_ref == "#" {
3500 let target = self
3502 .find_recursive_anchor_schema()
3503 .unwrap_or_else(|| parent_schema_name.to_string());
3504 dependencies.insert(target.clone());
3505 SchemaType::Reference { target }
3506 } else {
3507 let target = self
3508 .extract_schema_name(recursive_ref)
3509 .unwrap_or("RecursiveType")
3510 .to_string();
3511 dependencies.insert(target.clone());
3512 SchemaType::Reference { target }
3513 }
3514 }
3515 Schema::Typed { schema_type, .. } => {
3516 match schema_type {
3518 OpenApiSchemaType::String => SchemaType::Primitive {
3519 rust_type: "String".to_string(),
3520 serde_with: None,
3521 },
3522 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3523 let details = items_schema.details();
3524 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3525 SchemaType::Primitive {
3526 rust_type,
3527 serde_with: None,
3528 }
3529 }
3530 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3531 rust_type: "bool".to_string(),
3532 serde_with: None,
3533 },
3534 OpenApiSchemaType::Object => {
3535 let object_type_name = format!("{parent_schema_name}Item");
3537
3538 let object_type =
3540 self.analyze_object_schema(items_schema, dependencies)?;
3541
3542 let inline_schema = AnalyzedSchema {
3544 name: object_type_name.clone(),
3545 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3546 schema_type: object_type,
3547 dependencies: dependencies.clone(),
3548 nullable: false,
3549 description: items_schema.details().description.clone(),
3550 default: None,
3551 };
3552
3553 self.resolved_cache
3555 .insert(object_type_name.clone(), inline_schema);
3556 dependencies.insert(object_type_name.clone());
3557
3558 SchemaType::Reference {
3560 target: object_type_name,
3561 }
3562 }
3563 OpenApiSchemaType::Array => {
3564 self.analyze_array_schema(
3566 items_schema,
3567 parent_schema_name,
3568 dependencies,
3569 )?
3570 }
3571 _ => SchemaType::Primitive {
3572 rust_type: "serde_json::Value".to_string(),
3573 serde_with: None,
3574 },
3575 }
3576 }
3577 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3578 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3580
3581 match &analyzed.schema_type {
3583 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3584 let union_name = format!("{parent_schema_name}ItemUnion");
3587
3588 let mut union_schema = analyzed;
3590 union_schema.name = union_name.clone();
3591
3592 self.resolved_cache.insert(union_name.clone(), union_schema);
3594
3595 dependencies.insert(union_name.clone());
3597
3598 SchemaType::Reference { target: union_name }
3600 }
3601 _ => analyzed.schema_type,
3602 }
3603 }
3604 Schema::Untyped { .. } => {
3605 if let Some(inferred) = items_schema.inferred_type() {
3607 match inferred {
3608 OpenApiSchemaType::Object => {
3609 let object_type_name = format!("{parent_schema_name}Item");
3611
3612 let object_type =
3614 self.analyze_object_schema(items_schema, dependencies)?;
3615
3616 let inline_schema = AnalyzedSchema {
3618 name: object_type_name.clone(),
3619 original: serde_json::to_value(items_schema)
3620 .unwrap_or(Value::Null),
3621 schema_type: object_type,
3622 dependencies: dependencies.clone(),
3623 nullable: false,
3624 description: items_schema.details().description.clone(),
3625 default: None,
3626 };
3627
3628 self.resolved_cache
3630 .insert(object_type_name.clone(), inline_schema);
3631 dependencies.insert(object_type_name.clone());
3632
3633 SchemaType::Reference {
3635 target: object_type_name,
3636 }
3637 }
3638 OpenApiSchemaType::String => SchemaType::Primitive {
3639 rust_type: "String".to_string(),
3640 serde_with: None,
3641 },
3642 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3643 let details = items_schema.details();
3644 let rust_type = self.get_number_rust_type(inferred, details);
3645 SchemaType::Primitive {
3646 rust_type,
3647 serde_with: None,
3648 }
3649 }
3650 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3651 rust_type: "bool".to_string(),
3652 serde_with: None,
3653 },
3654 _ => SchemaType::Primitive {
3655 rust_type: "serde_json::Value".to_string(),
3656 serde_with: None,
3657 },
3658 }
3659 } else {
3660 SchemaType::Primitive {
3661 rust_type: "serde_json::Value".to_string(),
3662 serde_with: None,
3663 }
3664 }
3665 }
3666 _ => SchemaType::Primitive {
3667 rust_type: "serde_json::Value".to_string(),
3668 serde_with: None,
3669 },
3670 };
3671
3672 Ok(SchemaType::Array {
3673 item_type: Box::new(item_type),
3674 })
3675 } else {
3676 Ok(SchemaType::Primitive {
3678 rust_type: "Vec<serde_json::Value>".to_string(),
3679 serde_with: None,
3680 })
3681 }
3682 }
3683
3684 fn get_number_rust_type(
3685 &self,
3686 schema_type: OpenApiSchemaType,
3687 details: &crate::openapi::SchemaDetails,
3688 ) -> String {
3689 let format = details.format.as_deref();
3693 match schema_type {
3694 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
3695 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
3696 _ => self.type_mapper.dynamic_json().rust_type,
3697 }
3698 }
3699
3700 fn analyze_anyof_union(
3701 &mut self,
3702 any_of_schemas: &[Schema],
3703 discriminator: Option<&Discriminator>,
3704 dependencies: &mut HashSet<String>,
3705 context_name: &str,
3706 ) -> Result<SchemaType> {
3707 let filtered_owned: Vec<Schema>;
3712 let any_of_schemas: &[Schema] = if any_of_schemas
3713 .iter()
3714 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3715 {
3716 filtered_owned = any_of_schemas
3717 .iter()
3718 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3719 .cloned()
3720 .collect();
3721 if filtered_owned.is_empty() {
3722 return Ok(SchemaType::Primitive {
3723 rust_type: "serde_json::Value".to_string(),
3724 serde_with: None,
3725 });
3726 }
3727 if filtered_owned.len() == 1 {
3728 return self
3729 .analyze_schema_value(&filtered_owned[0], context_name)
3730 .map(|a| a.schema_type);
3731 }
3732 &filtered_owned
3733 } else {
3734 any_of_schemas
3735 };
3736
3737 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3739 let has_objects = any_of_schemas.iter().any(|s| {
3740 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3741 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3742 });
3743 let has_arrays = any_of_schemas
3744 .iter()
3745 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3746
3747 let all_string_like = any_of_schemas.iter().all(|s| {
3750 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3751 || s.details().const_value.is_some()
3752 });
3753
3754 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3755 if let Some(disc) = discriminator {
3757 return self.analyze_oneof_union(
3759 any_of_schemas,
3760 Some(disc),
3761 context_name,
3762 dependencies,
3763 );
3764 }
3765
3766 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3768 return self.analyze_oneof_union(
3769 any_of_schemas,
3770 Some(&Discriminator {
3771 property_name: disc_field,
3772 mapping: None,
3773 default_mapping: None,
3774 extensions: crate::extensions::Extensions::default(),
3775 }),
3776 context_name,
3777 dependencies,
3778 );
3779 }
3780
3781 let mut variants = Vec::new();
3783
3784 for schema in any_of_schemas {
3785 if let Some(ref_str) = schema.reference() {
3786 if let Some(target) = self.extract_schema_name(ref_str) {
3787 dependencies.insert(target.to_string());
3788 variants.push(SchemaRef {
3789 target: target.to_string(),
3790 nullable: false,
3791 });
3792 }
3793 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3794 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3795 {
3796 let inline_index = variants.len();
3798 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3799
3800 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3802
3803 variants.push(SchemaRef {
3804 target: inline_type_name,
3805 nullable: false,
3806 });
3807 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3808 let array_type =
3810 self.analyze_array_schema(schema, context_name, dependencies)?;
3811
3812 let array_type_name = if let Some(items_schema) = &schema.details().items {
3814 if let Some(ref_str) = items_schema.reference() {
3815 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3816 dependencies.insert(item_type_name.to_string());
3817 format!("{item_type_name}Array")
3818 } else {
3819 self.generate_context_aware_name(
3820 context_name,
3821 "Array",
3822 variants.len(),
3823 Some(schema),
3824 )
3825 }
3826 } else {
3827 self.generate_context_aware_name(
3828 context_name,
3829 "Array",
3830 variants.len(),
3831 Some(schema),
3832 )
3833 }
3834 } else {
3835 self.generate_context_aware_name(
3836 context_name,
3837 "Array",
3838 variants.len(),
3839 Some(schema),
3840 )
3841 };
3842
3843 self.resolved_cache.insert(
3845 array_type_name.clone(),
3846 AnalyzedSchema {
3847 name: array_type_name.clone(),
3848 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3849 schema_type: array_type,
3850 dependencies: HashSet::new(),
3851 nullable: false,
3852 description: Some("Array variant in union".to_string()),
3853 default: None,
3854 },
3855 );
3856
3857 dependencies.insert(array_type_name.clone());
3859
3860 variants.push(SchemaRef {
3861 target: array_type_name,
3862 nullable: false,
3863 });
3864 } else if let Some(schema_type) = schema.schema_type() {
3865 let primitive_unions = self
3875 .type_mapper
3876 .config_shape_primitive_unions()
3877 .unwrap_or(true);
3878
3879 if primitive_unions {
3880 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
3881 variants.push(SchemaRef {
3882 target: mapped.rust_type,
3883 nullable: false,
3884 });
3885 } else {
3886 let inline_index = variants.len();
3887 let inline_type_name = match schema_type {
3888 OpenApiSchemaType::String => {
3889 if inline_index == 0 {
3890 format!("{context_name}String")
3891 } else {
3892 format!("{context_name}StringVariant{inline_index}")
3893 }
3894 }
3895 OpenApiSchemaType::Number => {
3896 if inline_index == 0 {
3897 format!("{context_name}Number")
3898 } else {
3899 format!("{context_name}NumberVariant{inline_index}")
3900 }
3901 }
3902 OpenApiSchemaType::Integer => {
3903 if inline_index == 0 {
3904 format!("{context_name}Integer")
3905 } else {
3906 format!("{context_name}IntegerVariant{inline_index}")
3907 }
3908 }
3909 OpenApiSchemaType::Boolean => {
3910 if inline_index == 0 {
3911 format!("{context_name}Boolean")
3912 } else {
3913 format!("{context_name}BooleanVariant{inline_index}")
3914 }
3915 }
3916 _ => format!("{context_name}Variant{inline_index}"),
3917 };
3918
3919 let rust_type =
3920 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3921
3922 self.resolved_cache.insert(
3923 inline_type_name.clone(),
3924 AnalyzedSchema {
3925 name: inline_type_name.clone(),
3926 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3927 schema_type: SchemaType::Primitive {
3928 rust_type,
3929 serde_with: None,
3930 },
3931 dependencies: HashSet::new(),
3932 nullable: false,
3933 description: schema.details().description.clone(),
3934 default: None,
3935 },
3936 );
3937
3938 dependencies.insert(inline_type_name.clone());
3939
3940 variants.push(SchemaRef {
3941 target: inline_type_name,
3942 nullable: false,
3943 });
3944 }
3945 }
3946 }
3947
3948 if !variants.is_empty() {
3949 return Ok(SchemaType::Union { variants });
3950 }
3951 }
3952
3953 let all_strings = any_of_schemas.iter().all(|schema| {
3955 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
3956 || schema.details().const_value.is_some()
3957 });
3958
3959 if all_strings {
3960 let mut enum_values = Vec::new();
3962 let mut has_open_string = false;
3963
3964 for schema in any_of_schemas {
3965 if let Some(const_val) = &schema.details().const_value {
3966 if let Some(const_str) = const_val.as_str() {
3967 enum_values.push(const_str.to_string());
3968 }
3969 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
3970 has_open_string = true;
3971 }
3972 }
3973
3974 if !enum_values.is_empty() {
3975 if has_open_string {
3976 return Ok(SchemaType::ExtensibleEnum {
3979 known_values: enum_values,
3980 });
3981 } else {
3982 return Ok(SchemaType::StringEnum {
3984 values: enum_values,
3985 });
3986 }
3987 }
3988 }
3989
3990 Ok(SchemaType::Primitive {
3992 rust_type: "serde_json::Value".to_string(),
3993 serde_with: None,
3994 })
3995 }
3996
3997 fn find_recursive_anchor_schema(&self) -> Option<String> {
3999 for (schema_name, schema) in &self.schemas {
4001 let details = schema.details();
4002 if details.recursive_anchor == Some(true) {
4003 return Some(schema_name.clone());
4004 }
4005 }
4006
4007 None
4011 }
4012
4013 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4016 if let Schema::AnyOf { any_of, .. } = schema {
4018 if any_of.len() == 2 {
4019 let has_null = any_of
4020 .iter()
4021 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4022 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4023
4024 if has_null && has_empty_object {
4025 return true;
4026 }
4027 }
4028 }
4029
4030 self.is_dynamic_object_pattern(schema)
4032 }
4033
4034 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4036 let is_object = match schema.schema_type() {
4038 Some(OpenApiSchemaType::Object) => true,
4039 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4040 _ => false,
4041 };
4042
4043 if !is_object {
4044 return false;
4045 }
4046
4047 let details = schema.details();
4048
4049 if self.has_explicit_additional_properties(schema) {
4052 return false;
4053 }
4054
4055 let no_properties = details
4057 .properties
4058 .as_ref()
4059 .map(|props| props.is_empty())
4060 .unwrap_or(true);
4061
4062 if no_properties {
4063 let has_structural_constraints = details
4066 .required
4067 .as_ref()
4068 .map(|req| req.iter().any(|r| r != "type"))
4069 .unwrap_or(false)
4070 || details.pattern_properties.is_some()
4071 || details.property_names.is_some()
4072 || details.min_properties.is_some()
4073 || details.max_properties.is_some()
4074 || details.dependent_required.is_some()
4075 || details.dependent_schemas.is_some()
4076 || details.if_schema.is_some()
4077 || details.then_schema.is_some()
4078 || details.else_schema.is_some();
4079
4080 return !has_structural_constraints;
4081 }
4082
4083 false
4084 }
4085
4086 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4088 let details = schema.details();
4089
4090 matches!(
4092 &details.additional_properties,
4093 Some(crate::openapi::AdditionalProperties::Boolean(true))
4094 | Some(crate::openapi::AdditionalProperties::Schema(_))
4095 )
4096 }
4097
4098 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4100 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4101 .map_err(GeneratorError::ParseError)?;
4102
4103 if let Some(paths) = &spec.paths {
4104 for (path, path_item) in paths {
4105 let resolved = self.resolve_path_item(path_item, &spec)?;
4107 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4108 self.ingest_path_item_operations(path, pi, analysis)?;
4109 }
4110 }
4111 if let Some(webhooks) = &spec.webhooks {
4118 for (name, path_item) in webhooks {
4119 let synthetic_path = format!("__webhook__/{name}");
4120 self.ingest_path_item_operations(&synthetic_path, path_item, analysis)?;
4121 }
4122 }
4123 Ok(())
4124 }
4125
4126 fn resolve_path_item(
4130 &self,
4131 path_item: &crate::openapi::PathItem,
4132 spec: &crate::openapi::OpenApiSpec,
4133 ) -> Result<Option<crate::openapi::PathItem>> {
4134 let Some(reference) = &path_item.reference else {
4135 return Ok(None);
4136 };
4137 let target_name = reference
4138 .strip_prefix("#/components/pathItems/")
4139 .ok_or_else(|| {
4140 GeneratorError::UnresolvedReference(format!(
4141 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4142 ))
4143 })?;
4144 let pi = spec
4145 .components
4146 .as_ref()
4147 .and_then(|c| c.path_items.as_ref())
4148 .and_then(|map| map.get(target_name))
4149 .ok_or_else(|| {
4150 GeneratorError::UnresolvedReference(format!(
4151 "Path Item ref {reference} not found in components/pathItems"
4152 ))
4153 })?;
4154 Ok(Some(pi.clone()))
4155 }
4156
4157 fn ingest_path_item_operations(
4158 &mut self,
4159 path: &str,
4160 path_item: &crate::openapi::PathItem,
4161 analysis: &mut SchemaAnalysis,
4162 ) -> Result<()> {
4163 for (method, operation) in path_item.operations() {
4164 let raw_operation_id = operation
4166 .operation_id
4167 .clone()
4168 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4169
4170 use heck::ToPascalCase;
4181 let canon = |s: &str| s.replace('.', "_").to_pascal_case();
4182 let key_collides = |id: &str| -> bool {
4183 let target = canon(id);
4184 analysis
4185 .operations
4186 .keys()
4187 .any(|existing| canon(existing) == target)
4188 };
4189 let operation_id = if key_collides(&raw_operation_id) {
4190 let method_lower = method.to_lowercase();
4191 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4192 let mut suffix = 2;
4193 while key_collides(&candidate) {
4194 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4195 suffix += 1;
4196 }
4197 eprintln!(
4198 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4199 raw_operation_id, method, path, candidate
4200 );
4201 candidate
4202 } else {
4203 raw_operation_id
4204 };
4205
4206 let op_info = self.analyze_single_operation(
4207 &operation_id,
4208 method,
4209 path,
4210 operation,
4211 path_item.parameters.as_ref(),
4212 analysis,
4213 )?;
4214 analysis.operations.insert(operation_id, op_info);
4215 }
4216 Ok(())
4217 }
4218
4219 fn generate_operation_id(method: &str, path: &str) -> String {
4222 let mut operation_id = method.to_lowercase();
4224
4225 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4227
4228 for part in path_parts {
4229 if part.is_empty() {
4230 continue;
4231 }
4232
4233 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4235 &part[1..part.len() - 1]
4236 } else {
4237 part
4238 };
4239
4240 let pascal_case_part = cleaned_part
4242 .split(&['-', '_'][..])
4243 .map(|s| {
4244 let mut chars = s.chars();
4245 match chars.next() {
4246 None => String::new(),
4247 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4248 }
4249 })
4250 .collect::<String>();
4251
4252 operation_id.push_str(&pascal_case_part);
4253 }
4254
4255 operation_id
4256 }
4257
4258 fn analyze_single_operation(
4260 &mut self,
4261 operation_id: &str,
4262 method: &str,
4263 path: &str,
4264 operation: &crate::openapi::Operation,
4265 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4266 _analysis: &mut SchemaAnalysis,
4267 ) -> Result<OperationInfo> {
4268 let mut op_info = OperationInfo {
4269 operation_id: operation_id.to_string(),
4270 method: method.to_uppercase(),
4271 path: path.to_string(),
4272 summary: operation.summary.clone(),
4273 description: operation.description.clone(),
4274 request_body: None,
4275 request_body_required: operation
4277 .request_body
4278 .as_ref()
4279 .and_then(|rb| rb.required)
4280 .unwrap_or(false),
4281 response_schemas: BTreeMap::new(),
4282 parameters: Vec::new(),
4283 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4286 };
4287
4288 if let Some(request_body) = &operation.request_body
4290 && let Some((content_type, maybe_schema)) = request_body.best_content()
4291 {
4292 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
4293 op_info.request_body = if is_json_media_type(content_type) {
4294 maybe_schema
4295 .map(|s| {
4296 self.resolve_or_inline_schema(s, operation_id, "Request")
4297 .map(|name| RequestBodyContent::Json { schema_name: name })
4298 })
4299 .transpose()?
4300 } else if is_form_urlencoded_media_type(content_type) {
4301 maybe_schema
4302 .map(|s| {
4303 self.resolve_or_inline_schema(s, operation_id, "Request")
4304 .map(|name| RequestBodyContent::FormUrlEncoded { schema_name: name })
4305 })
4306 .transpose()?
4307 } else {
4308 match content_type {
4309 "multipart/form-data" => Some(RequestBodyContent::Multipart),
4310 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
4311 "text/plain" => Some(RequestBodyContent::TextPlain),
4312 _ => None,
4313 }
4314 };
4315 }
4316
4317 if let Some(responses) = &operation.responses {
4319 for (status_code, response) in responses {
4320 if let Some(content) = response.content.as_ref() {
4326 if content.keys().any(|ct| ct.starts_with("text/event-stream")) {
4327 op_info.supports_streaming = true;
4328 }
4329 }
4330
4331 if let Some(schema) = response.json_schema() {
4332 if let Some(schema_ref) = schema.reference() {
4333 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4335 op_info
4336 .response_schemas
4337 .insert(status_code.clone(), schema_name.to_string());
4338 }
4339 } else {
4340 let synthetic_name =
4342 self.generate_inline_response_type_name(operation_id, status_code);
4343
4344 let mut deps = HashSet::new();
4346 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4347
4348 op_info
4349 .response_schemas
4350 .insert(status_code.clone(), synthetic_name);
4351 }
4352 }
4353 }
4354 }
4355
4356 if op_info.supports_streaming
4359 && let Some(parameters) = &operation.parameters
4360 {
4361 for param in parameters {
4362 if let Some(name) = param.name.as_deref() {
4363 if name.eq_ignore_ascii_case("stream") {
4364 op_info.stream_parameter = Some(name.to_string());
4365 break;
4366 }
4367 }
4368 }
4369 }
4370
4371 if let Some(parameters) = &operation.parameters {
4373 for param in parameters {
4374 let resolved = self.resolve_parameter(param).into_owned();
4378 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4379 op_info.parameters.push(param_info);
4380 }
4381 }
4382 }
4383
4384 if let Some(path_params) = path_item_parameters {
4386 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4387 .parameters
4388 .iter()
4389 .map(|p| (p.name.clone(), p.location.clone()))
4390 .collect();
4391 for param in path_params {
4392 let resolved = self.resolve_parameter(param).into_owned();
4393 if let Some(param_info) = self.analyze_parameter(&resolved, operation_id)? {
4394 if !existing_keys
4395 .contains(&(param_info.name.clone(), param_info.location.clone()))
4396 {
4397 op_info.parameters.push(param_info);
4398 }
4399 }
4400 }
4401 }
4402
4403 let mut declared_path_names: std::collections::HashSet<String> = op_info
4411 .parameters
4412 .iter()
4413 .filter(|p| p.location == "path")
4414 .map(|p| p.name.clone())
4415 .collect();
4416 let bytes = path.as_bytes().iter();
4417 let mut current = String::new();
4418 let mut in_brace = false;
4419 let mut synthesized: Vec<String> = Vec::new();
4420 for b in bytes {
4421 match *b {
4422 b'{' => {
4423 in_brace = true;
4424 current.clear();
4425 }
4426 b'}' if in_brace => {
4427 in_brace = false;
4428 if !current.is_empty() && !declared_path_names.contains(¤t) {
4429 synthesized.push(current.clone());
4430 declared_path_names.insert(current.clone());
4431 }
4432 }
4433 _ if in_brace => current.push(*b as char),
4434 _ => {}
4435 }
4436 }
4437 for name in synthesized {
4438 eprintln!(
4439 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
4440 path, name
4441 );
4442 op_info.parameters.push(ParameterInfo {
4443 name,
4444 location: "path".to_string(),
4445 required: true,
4446 schema_ref: None,
4447 rust_type: "String".to_string(),
4448 description: None,
4449 enum_values: None,
4450 rust_ident: None,
4451 query_serialization: None,
4452 });
4453 }
4454
4455 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
4463 for p in op_info.parameters.iter_mut() {
4464 let raw = base_param_ident(&p.name);
4465 let mut chosen = raw.clone();
4466 let mut suffix = 2;
4467 while !used.insert(chosen.clone()) {
4468 chosen = format!("{raw}_{suffix}");
4469 suffix += 1;
4470 }
4471 p.rust_ident = Some(chosen);
4472 }
4473
4474 Ok(op_info)
4475 }
4476
4477 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
4484 use heck::ToPascalCase;
4485 let base_name = operation_id.replace('.', "_").to_pascal_case();
4486 let suffix = Self::status_code_suffix(status_code);
4487 format!("{}Response{}", base_name, suffix)
4488 }
4489
4490 fn status_code_suffix(status_code: &str) -> String {
4497 match status_code {
4498 "" | "200" => String::new(),
4499 "default" | "Default" => "Default".to_string(),
4500 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
4501 other => other.to_ascii_lowercase(),
4502 }
4503 }
4504
4505 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
4507 use heck::ToPascalCase;
4508 let base_name = operation_id.replace('.', "_").to_pascal_case();
4512 format!("{}Request", base_name)
4513 }
4514
4515 fn resolve_or_inline_schema(
4518 &mut self,
4519 schema: &crate::openapi::Schema,
4520 operation_id: &str,
4521 suffix: &str,
4522 ) -> Result<String> {
4523 if let Some(schema_ref) = schema.reference()
4524 && let Some(schema_name) = self.extract_schema_name(schema_ref)
4525 {
4526 return Ok(schema_name.to_string());
4527 }
4528 let synthetic_name = if suffix == "Request" {
4530 self.generate_inline_request_type_name(operation_id)
4531 } else {
4532 self.generate_inline_response_type_name(operation_id, "")
4533 };
4534 let mut deps = HashSet::new();
4535 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4536 Ok(synthetic_name)
4537 }
4538
4539 fn resolve_parameter<'a>(
4542 &'a self,
4543 param: &'a crate::openapi::Parameter,
4544 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
4545 if let Some(ref_str) = param.reference.as_deref() {
4546 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
4547 if let Some(resolved) = self.component_parameters.get(param_name) {
4548 return std::borrow::Cow::Borrowed(resolved);
4549 }
4550 }
4551 }
4552 std::borrow::Cow::Borrowed(param)
4553 }
4554
4555 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
4568 let Some(schema_value) = self
4569 .openapi_spec
4570 .get("components")
4571 .and_then(|c| c.get("schemas"))
4572 .and_then(|s| s.get(name))
4573 else {
4574 return false;
4575 };
4576 let is_string_type = schema_value
4577 .get("type")
4578 .and_then(|v| v.as_str())
4579 .map(|s| s == "string")
4580 .unwrap_or(false);
4581 let has_enum_or_const =
4582 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
4583 is_string_type && has_enum_or_const
4584 }
4585
4586 fn analyze_parameter(
4587 &mut self,
4588 param: &crate::openapi::Parameter,
4589 operation_id: &str,
4590 ) -> Result<Option<ParameterInfo>> {
4591 use heck::ToPascalCase;
4592
4593 let name = param.name.as_deref().unwrap_or("");
4594 let location = param.location.as_deref().unwrap_or("");
4595 let required = param.required.unwrap_or(false);
4596
4597 let mut rust_type = "String".to_string();
4598 let mut schema_ref = None;
4599 let mut enum_values: Option<Vec<String>> = None;
4600 let mut query_serialization: Option<QuerySerialization> = None;
4601
4602 let is_query = location == "query";
4608 let form_style = matches!(param.style.as_deref(), None | Some("form"));
4609 let form_exploded = form_style && param.explode.unwrap_or(true);
4610 let deep_object =
4611 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
4612
4613 let object_serialization = if !is_query {
4614 None
4615 } else if deep_object {
4616 Some(QuerySerialization::DeepObject)
4617 } else if form_exploded {
4618 Some(QuerySerialization::FormExplodedObject)
4619 } else if form_style {
4620 Some(QuerySerialization::FormObject)
4621 } else {
4622 None
4623 };
4624
4625 if let Some(schema) = ¶m.schema {
4626 if let Some(ref_str) = schema.reference() {
4627 if let Some(name) = self.extract_schema_name(ref_str) {
4633 if self.referenced_schema_is_string_enum(name) {
4634 schema_ref = Some(name.to_string());
4635 } else if object_serialization.is_some()
4636 && self.referenced_schema_is_object(name)
4637 {
4638 schema_ref = Some(name.to_string());
4639 query_serialization = object_serialization.clone();
4640 }
4641 }
4642 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
4643 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
4648 let param_pascal = name.to_pascal_case();
4649 let synthetic_name = format!("{op_pascal}{param_pascal}");
4650 let mut deps = HashSet::new();
4651 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4652 schema_ref = Some(synthetic_name);
4653 query_serialization = object_serialization.clone();
4654 } else if is_query
4655 && form_style
4656 && matches!(
4657 schema.schema_type(),
4658 Some(crate::openapi::SchemaType::Array)
4659 )
4660 && let Some(item_type) = self.array_param_item_type(schema)
4661 {
4662 query_serialization = Some(if form_exploded {
4669 QuerySerialization::FormExplodedArray { item_type }
4670 } else {
4671 QuerySerialization::FormArray { item_type }
4672 });
4673 } else if let Some(schema_type) = schema.schema_type() {
4674 let format = schema.details().format.clone();
4680 rust_type = match schema_type {
4681 crate::openapi::SchemaType::Boolean => "bool".to_string(),
4682 crate::openapi::SchemaType::Integer => {
4683 self.type_mapper.integer_format(format.as_deref()).rust_type
4684 }
4685 crate::openapi::SchemaType::Number => {
4686 self.type_mapper.number_format(format.as_deref()).rust_type
4687 }
4688 crate::openapi::SchemaType::String => "String".to_string(),
4689 _ => "String".to_string(),
4690 };
4691
4692 if matches!(schema_type, crate::openapi::SchemaType::String) {
4693 let details = schema.details();
4694 if details.is_string_enum() {
4695 if let Some(values) = details.string_enum_values() {
4696 if !values.is_empty() {
4697 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
4698 let param_pascal = name.to_pascal_case();
4699 rust_type = format!("{op_pascal}{param_pascal}");
4700 enum_values = Some(values);
4701 }
4702 }
4703 }
4704 }
4705 }
4706 }
4707
4708 Ok(Some(ParameterInfo {
4709 name: name.to_string(),
4710 location: location.to_string(),
4711 required,
4712 schema_ref,
4713 rust_type,
4714 description: param.description.clone(),
4715 enum_values,
4716 rust_ident: None,
4717 query_serialization,
4718 }))
4719 }
4720
4721 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
4730 let items = schema.details().items.as_deref()?;
4731 if let Some(ref_str) = items.reference() {
4732 let name = self.extract_schema_name(ref_str)?;
4733 return self
4734 .referenced_schema_is_string_enum(name)
4735 .then(|| ArrayItemType::EnumRef(name.to_string()));
4736 }
4737 let format = items.details().format.clone();
4738 let scalar = match items.schema_type()? {
4739 crate::openapi::SchemaType::String => "String".to_string(),
4740 crate::openapi::SchemaType::Integer => {
4741 self.type_mapper.integer_format(format.as_deref()).rust_type
4742 }
4743 crate::openapi::SchemaType::Number => {
4744 self.type_mapper.number_format(format.as_deref()).rust_type
4745 }
4746 crate::openapi::SchemaType::Boolean => "bool".to_string(),
4747 _ => return None,
4748 };
4749 Some(ArrayItemType::Scalar(scalar))
4750 }
4751
4752 fn referenced_schema_is_object(&self, name: &str) -> bool {
4756 let Some(schema_value) = self
4757 .openapi_spec
4758 .get("components")
4759 .and_then(|c| c.get("schemas"))
4760 .and_then(|s| s.get(name))
4761 else {
4762 return false;
4763 };
4764 match schema_value.get("type").and_then(|v| v.as_str()) {
4765 Some(t) => t == "object",
4766 None => schema_value.get("properties").is_some(),
4767 }
4768 }
4769
4770 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
4772 match schema.schema_type() {
4773 Some(crate::openapi::SchemaType::Object) => true,
4774 None => schema.details().properties.is_some(),
4775 _ => false,
4776 }
4777 }
4778}