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 operation_responses: BTreeMap<String, BTreeMap<String, OperationResponse>>,
78 pub operation_id_aliases: BTreeMap<String, Vec<String>>,
82 pub used_type_features: crate::type_mapping::UsedFeatures,
91 pub enum_extensions: BTreeMap<String, EnumExtensions>,
99 pub validation_context: ValidationContext,
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
107pub struct OperationResponse {
108 pub schema_name: Option<String>,
110 pub media_type: Option<String>,
112 pub supports_streaming: bool,
114 pub has_content: bool,
116 pub unsupported_media_types: Vec<String>,
118}
119
120#[derive(Debug, Clone, Default)]
121pub struct ValidationContext {
122 pub openapi_version: String,
123 pub json_schema_dialect: Option<String>,
124 pub component_schemas: BTreeMap<String, Value>,
125}
126
127#[derive(Debug, Clone, Default)]
132pub struct EnumExtensions {
133 pub varnames: Vec<String>,
138 pub descriptions: Vec<String>,
140}
141
142#[derive(Debug, Clone)]
143pub struct AnalyzedSchema {
144 pub name: String,
145 pub original: Value,
146 pub schema_type: SchemaType,
147 pub dependencies: HashSet<String>,
148 pub nullable: bool,
149 pub description: Option<String>,
150 pub default: Option<serde_json::Value>,
151}
152
153#[derive(Debug, Clone)]
154pub enum SchemaType {
155 Primitive {
161 rust_type: String,
162 serde_with: Option<String>,
163 },
164 Object {
166 properties: BTreeMap<String, PropertyInfo>,
167 required: HashSet<String>,
168 additional_properties: ObjectAdditionalProperties,
169 },
170 DiscriminatedUnion {
172 discriminator_field: String,
173 variants: Vec<UnionVariant>,
174 },
175 Union { variants: Vec<SchemaRef> },
177 Array { item_type: Box<SchemaType> },
179 StringEnum { values: Vec<String> },
181 ExtensibleEnum { known_values: Vec<String> },
183 Composition { schemas: Vec<SchemaRef> },
185 Reference { target: String },
187}
188
189#[derive(Debug, Clone)]
194pub enum ObjectAdditionalProperties {
195 Forbidden,
198 Untyped,
201 Typed { value_type: Box<SchemaType> },
204}
205
206impl ObjectAdditionalProperties {
207 pub fn is_open(&self) -> bool {
210 !matches!(self, Self::Forbidden)
211 }
212}
213
214#[derive(Debug, Clone)]
215pub struct PropertyInfo {
216 pub schema_type: SchemaType,
217 pub nullable: bool,
218 pub description: Option<String>,
219 pub default: Option<serde_json::Value>,
220 pub serde_attrs: Vec<String>,
221 pub constraints: PropertyConstraints,
226}
227
228#[derive(Debug, Clone, Default)]
233pub struct PropertyConstraints {
234 pub minimum: Option<f64>,
235 pub maximum: Option<f64>,
236 pub exclusive_minimum: Option<f64>,
237 pub exclusive_maximum: Option<f64>,
238 pub multiple_of: Option<f64>,
239 pub min_length: Option<u64>,
240 pub max_length: Option<u64>,
241 pub pattern: Option<String>,
242 pub min_items: Option<u64>,
243 pub max_items: Option<u64>,
244 pub unique_items: Option<bool>,
245}
246
247impl PropertyConstraints {
248 pub fn is_empty(&self) -> bool {
249 self.minimum.is_none()
250 && self.maximum.is_none()
251 && self.exclusive_minimum.is_none()
252 && self.exclusive_maximum.is_none()
253 && self.multiple_of.is_none()
254 && self.min_length.is_none()
255 && self.max_length.is_none()
256 && self.pattern.is_none()
257 && self.min_items.is_none()
258 && self.max_items.is_none()
259 && self.unique_items.is_none()
260 }
261
262 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
267 use crate::openapi::ExclusiveBound;
268 let exclusive_minimum = match &details.exclusive_minimum {
269 Some(ExclusiveBound::Number(v)) => Some(*v),
270 _ => None,
271 };
272 let exclusive_maximum = match &details.exclusive_maximum {
273 Some(ExclusiveBound::Number(v)) => Some(*v),
274 _ => None,
275 };
276 Self {
277 minimum: details.minimum,
278 maximum: details.maximum,
279 exclusive_minimum,
280 exclusive_maximum,
281 multiple_of: details.multiple_of,
282 min_length: details.min_length,
283 max_length: details.max_length,
284 pattern: details.pattern.clone(),
285 min_items: details.min_items,
286 max_items: details.max_items,
287 unique_items: details.unique_items,
288 }
289 }
290}
291
292#[derive(Debug, Clone)]
293pub struct UnionVariant {
294 pub rust_name: String,
295 pub type_name: String,
296 pub discriminator_value: String,
297 pub schema_ref: String,
298}
299
300#[derive(Debug, Clone)]
301pub struct SchemaRef {
302 pub target: String,
303 pub nullable: bool,
304}
305
306#[derive(Debug, Clone)]
307pub struct DependencyGraph {
308 pub edges: BTreeMap<String, HashSet<String>>,
309 pub recursive_schemas: HashSet<String>,
311}
312
313#[derive(Debug, Clone)]
314pub struct DetectedPatterns {
315 pub tagged_enum_schemas: HashSet<String>,
317 pub untagged_enum_schemas: HashSet<String>,
319 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
321}
322
323#[derive(Debug, Clone, Default, serde::Serialize)]
325pub struct OperationInfo {
326 pub operation_id: String,
328 pub method: String,
330 pub path: String,
332 pub summary: Option<String>,
334 pub description: Option<String>,
336 pub request_body: Option<RequestBodyContent>,
338 pub request_body_required: bool,
341 pub response_schemas: BTreeMap<String, String>,
343 pub parameters: Vec<ParameterInfo>,
345 pub supports_streaming: bool,
347 pub stream_parameter: Option<String>,
349 pub tags: Vec<String>,
353}
354
355#[derive(Debug, Clone, serde::Serialize)]
357#[serde(tag = "kind")]
358pub enum RequestBodyContent {
359 Json {
360 schema_name: String,
361 media_type: String,
362 #[serde(skip)]
363 validation_schema: Value,
364 },
365 FormUrlEncoded {
366 schema_name: String,
367 media_type: String,
368 #[serde(skip)]
369 validation_schema: Value,
370 },
371 Multipart,
372 OctetStream,
373 TextPlain,
374 SchemaLess {
378 media_type: String,
379 },
380 Unsupported {
381 media_types: Vec<String>,
382 },
383}
384
385impl RequestBodyContent {
386 pub fn schema_name(&self) -> Option<&str> {
388 match self {
389 Self::Json { schema_name, .. } | Self::FormUrlEncoded { schema_name, .. } => {
390 Some(schema_name)
391 }
392 Self::Multipart
393 | Self::OctetStream
394 | Self::TextPlain
395 | Self::SchemaLess { .. }
396 | Self::Unsupported { .. } => None,
397 }
398 }
399}
400
401fn base_param_ident(name: &str) -> String {
405 use heck::ToSnakeCase;
406 let suffix = if name.ends_with("<=") {
407 "_lte"
408 } else if name.ends_with(">=") {
409 "_gte"
410 } else if name.ends_with('<') {
411 "_lt"
412 } else if name.ends_with('>') {
413 "_gt"
414 } else {
415 ""
416 };
417 let stripped = name.trim_end_matches(['<', '>', '=']);
418 let mut snake = stripped.to_snake_case();
419 if snake.is_empty() {
420 snake.push_str("parameter");
421 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
422 snake.insert(0, '_');
423 }
424 snake.push_str(suffix);
425 snake
426}
427
428#[derive(Debug, Clone, serde::Serialize)]
430pub struct ParameterInfo {
431 pub name: String,
433 pub location: String,
435 pub required: bool,
437 pub schema_ref: Option<String>,
439 pub rust_type: String,
441 pub description: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
449 pub enum_values: Option<Vec<String>>,
450 #[serde(skip_serializing_if = "Option::is_none")]
456 pub enum_varnames: Option<Vec<String>>,
457 #[serde(skip_serializing_if = "Option::is_none")]
465 pub rust_ident: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
475 pub query_serialization: Option<QuerySerialization>,
476 #[serde(skip)]
479 pub validation_schema: Option<Value>,
480}
481
482#[derive(Debug, Clone, PartialEq, serde::Serialize)]
485pub enum QuerySerialization {
486 FormExplodedObject,
490 FormObject,
493 DeepObject,
496 FormExplodedArray { item_type: ArrayItemType },
499 FormArray { item_type: ArrayItemType },
502 Unsupported { reason: String },
507}
508
509#[derive(Debug, Clone, PartialEq, serde::Serialize)]
516pub enum ArrayItemType {
517 Scalar(String),
519 EnumRef(String),
521}
522
523impl Default for DependencyGraph {
524 fn default() -> Self {
525 Self::new()
526 }
527}
528
529impl DependencyGraph {
530 pub fn new() -> Self {
531 Self {
532 edges: BTreeMap::new(),
533 recursive_schemas: HashSet::new(),
534 }
535 }
536
537 pub fn add_dependency(&mut self, from: String, to: String) {
538 self.edges.entry(from).or_default().insert(to);
539 }
540
541 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
543 self.detect_recursive_schemas();
545
546 let mut temp_edges = self.edges.clone();
548 for (schema, deps) in &mut temp_edges {
549 deps.remove(schema); }
551
552 let mut visited = HashSet::new();
553 let mut temp_visited = HashSet::new();
554 let mut result = Vec::new();
555
556 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
558 all_nodes.sort();
559 for node in all_nodes {
560 if !visited.contains(node) {
561 self.visit_node_recursive(
562 node,
563 &temp_edges,
564 &mut visited,
565 &mut temp_visited,
566 &mut result,
567 )?;
568 }
569 }
570
571 result.reverse();
572 Ok(result)
573 }
574
575 fn detect_recursive_schemas(&mut self) {
576 for (schema, deps) in &self.edges {
577 if deps.contains(schema) {
578 self.recursive_schemas.insert(schema.clone());
580 } else {
581 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
583 self.recursive_schemas.insert(schema.clone());
584 }
585 }
586 }
587
588 for (schema, deps) in &self.edges {
590 for dep in deps {
591 if let Some(dep_deps) = self.edges.get(dep) {
592 if dep_deps.contains(schema) {
593 self.recursive_schemas.insert(schema.clone());
595 self.recursive_schemas.insert(dep.clone());
596 }
597 }
598 }
599 }
600 }
601
602 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
603 if visited.contains(current) {
604 return false; }
606
607 visited.insert(current.to_string());
608
609 if let Some(deps) = self.edges.get(current) {
610 for dep in deps {
611 if dep == start {
612 return true; }
614 if self.has_cycle_from(start, dep, visited) {
615 return true;
616 }
617 }
618 }
619
620 false
621 }
622
623 #[allow(clippy::only_used_in_recursion)]
624 fn visit_node_recursive(
625 &self,
626 node: &str,
627 temp_edges: &BTreeMap<String, HashSet<String>>,
628 visited: &mut HashSet<String>,
629 temp_visited: &mut HashSet<String>,
630 result: &mut Vec<String>,
631 ) -> Result<()> {
632 if temp_visited.contains(node) {
633 return Ok(());
635 }
636
637 if visited.contains(node) {
638 return Ok(());
639 }
640
641 temp_visited.insert(node.to_string());
642
643 if let Some(dependencies) = temp_edges.get(node) {
644 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
646 sorted_deps.sort();
647 for dep in sorted_deps {
648 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
649 }
650 }
651
652 temp_visited.remove(node);
653 visited.insert(node.to_string());
654 result.push(node.to_string());
655
656 Ok(())
657 }
658}
659
660pub fn merge_schema_extensions(
663 main_spec: Value,
664 extension_paths: &[impl AsRef<Path>],
665) -> Result<Value> {
666 let mut result = main_spec;
667
668 for path in extension_paths {
669 let extension = load_extension_file(path.as_ref())?;
670 result = merge_json_objects_with_replacements(result, extension)?;
671 }
672
673 Ok(result)
674}
675
676fn load_extension_file(path: &Path) -> Result<Value> {
680 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
681 message: format!("Failed to read file {}: {}", path.display(), e),
682 })?;
683
684 let is_yaml = path
685 .extension()
686 .and_then(|extension| extension.to_str())
687 .is_some_and(|extension| {
688 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
689 });
690
691 if is_yaml {
692 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
693 GeneratorError::FileError {
694 message: format!(
695 "Failed to parse schema extension {} as YAML: {}",
696 path.display(),
697 error
698 ),
699 }
700 })
701 } else {
702 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
703 message: format!(
704 "Failed to parse schema extension {} as JSON: {}",
705 path.display(),
706 error
707 ),
708 })
709 }
710}
711
712fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
714 let replacements = extract_replacement_rules(&extension);
716
717 Ok(merge_json_objects_with_rules(
719 main,
720 extension,
721 &replacements,
722 ))
723}
724
725fn extract_replacement_rules(
727 extension: &Value,
728) -> std::collections::HashMap<String, (String, String)> {
729 let mut rules = std::collections::HashMap::new();
730
731 if let Some(x_replacements) = extension.get("x-replacements") {
732 if let Some(x_replacements_obj) = x_replacements.as_object() {
733 for (schema_name, replacement_rule) in x_replacements_obj {
734 if let Some(rule_obj) = replacement_rule.as_object() {
735 if let (Some(replace), Some(with)) = (
736 rule_obj.get("replace").and_then(|v| v.as_str()),
737 rule_obj.get("with").and_then(|v| v.as_str()),
738 ) {
739 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
740 }
742 }
743 }
744 }
745 }
746
747 rules
748}
749
750fn should_replace_variant(
752 schema_name: &str,
753 extension_refs: &[String],
754 replacements: &std::collections::HashMap<String, (String, String)>,
755) -> bool {
756 for (replace_schema, with_schema) in replacements.values() {
758 if schema_name == replace_schema {
759 let replacement_exists = extension_refs.iter().any(|ext_ref| {
761 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
762 ext_schema_name == with_schema
763 });
764
765 if replacement_exists {
766 return true;
767 }
768 }
769 }
770
771 extension_refs.iter().any(|ext_ref| {
773 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
774 schema_name == ext_schema_name
775 })
776}
777
778fn merge_json_objects_with_rules(
783 main: Value,
784 extension: Value,
785 replacements: &std::collections::HashMap<String, (String, String)>,
786) -> Value {
787 match (main, extension) {
788 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
790 let main_union_keyword = if main_obj.contains_key("oneOf") {
793 Some("oneOf")
794 } else if main_obj.contains_key("anyOf") {
795 Some("anyOf")
796 } else {
797 None
798 };
799 if let (Some(main_variants), Some(ext_variants)) = (
800 extract_schema_variants(&Value::Object(main_obj.clone())),
801 extract_schema_variants(&Value::Object(ext_obj.clone())),
802 ) {
803 let union_key = main_union_keyword.unwrap_or("oneOf");
804 println!(
805 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
806 main_variants.len(),
807 ext_variants.len()
808 );
809 let mut merged_variants = Vec::new();
812 let extension_refs: Vec<String> = ext_variants
813 .iter()
814 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
815 .map(|s| s.to_string())
816 .collect();
817
818 for main_variant in main_variants {
820 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
821 let schema_name = main_ref.split('/').next_back().unwrap_or("");
823 let should_replace =
824 should_replace_variant(schema_name, &extension_refs, replacements);
825
826 if should_replace {
827 println!("🔄 REPLACING {} (explicit rule)", schema_name);
828 }
829
830 if !should_replace {
831 merged_variants.push(main_variant);
832 }
833 } else {
834 merged_variants.push(main_variant);
836 }
837 }
838
839 for ext_variant in ext_variants {
841 merged_variants.push(ext_variant);
842 }
843
844 main_obj.remove("oneOf");
846 main_obj.remove("anyOf");
847 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
848
849 for (key, ext_value) in ext_obj {
851 if key != "oneOf" && key != "anyOf" {
852 match main_obj.get(&key) {
853 Some(main_value) => {
854 let merged_value = merge_json_objects_with_rules(
855 main_value.clone(),
856 ext_value,
857 replacements,
858 );
859 main_obj.insert(key, merged_value);
860 }
861 None => {
862 main_obj.insert(key, ext_value);
863 }
864 }
865 }
866 }
867
868 return Value::Object(main_obj);
869 }
870
871 for (key, ext_value) in ext_obj {
873 match main_obj.get(&key) {
874 Some(main_value) => {
875 let merged_value = merge_json_objects_with_rules(
877 main_value.clone(),
878 ext_value,
879 replacements,
880 );
881 main_obj.insert(key, merged_value);
882 }
883 None => {
884 main_obj.insert(key, ext_value);
886 }
887 }
888 }
889 Value::Object(main_obj)
890 }
891
892 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
894 main_arr.extend(ext_arr);
895 Value::Array(main_arr)
896 }
897
898 (_, extension) => extension,
900 }
901}
902
903fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
905 if let Value::Object(map) = obj {
906 if let Some(Value::Array(variants)) = map.get("oneOf") {
907 return Some(variants.clone());
908 }
909 if let Some(Value::Array(variants)) = map.get("anyOf") {
910 return Some(variants.clone());
911 }
912 }
913 None
914}
915
916pub struct SchemaAnalyzer {
917 schemas: BTreeMap<String, Schema>,
918 resolved_cache: BTreeMap<String, AnalyzedSchema>,
919 openapi_spec: Value,
920 current_schema_name: Option<String>,
921 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
922 type_mapper: TypeMapper,
927}
928
929impl SchemaAnalyzer {
930 pub fn new(openapi_spec: Value) -> Result<Self> {
934 Self::with_type_mapper(openapi_spec, TypeMapper::default())
935 }
936
937 pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
941 let spec: OpenApiSpec =
942 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
943 let schemas = Self::extract_schemas(&spec)?;
944
945 let component_parameters = spec
946 .components
947 .as_ref()
948 .and_then(|c| c.parameters.as_ref())
949 .cloned()
950 .unwrap_or_default();
951 Ok(Self {
952 schemas,
953 resolved_cache: BTreeMap::new(),
954 openapi_spec,
955 current_schema_name: None,
956 component_parameters,
957 type_mapper,
958 })
959 }
960
961 pub fn new_with_extensions(
964 openapi_spec: Value,
965 extension_paths: &[std::path::PathBuf],
966 ) -> Result<Self> {
967 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
968 Self::new(merged_spec)
969 }
970
971 pub fn new_with_extensions_and_type_mapper(
974 openapi_spec: Value,
975 extension_paths: &[std::path::PathBuf],
976 type_mapper: TypeMapper,
977 ) -> Result<Self> {
978 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
979 Self::with_type_mapper(merged_spec, type_mapper)
980 }
981
982 pub fn type_mapper(&self) -> &TypeMapper {
986 &self.type_mapper
987 }
988
989 fn generate_context_aware_name(
992 &self,
993 base_context: &str,
994 type_hint: &str,
995 index: usize,
996 schema: Option<&Schema>,
997 ) -> String {
998 if let Some(schema) = schema {
1000 if type_hint == "Array"
1002 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
1003 {
1004 if let Some(items_schema) = &schema.details().items {
1005 if let Some(item_type) = items_schema.schema_type() {
1007 match item_type {
1008 OpenApiSchemaType::Object => {
1009 return format!("{base_context}ItemArray");
1010 }
1011 OpenApiSchemaType::String => {
1012 return format!("{base_context}StringArray");
1013 }
1014 _ => {}
1015 }
1016 }
1017 }
1018 }
1019 }
1020
1021 match type_hint {
1023 "Array" => {
1024 format!("{base_context}Array")
1026 }
1027 "Variant" | "InlineVariant" => {
1028 if index == 0 {
1030 format!("{base_context}{type_hint}")
1031 } else {
1032 format!("{}{}{}", base_context, type_hint, index + 1)
1033 }
1034 }
1035 _ => {
1036 format!("{base_context}{type_hint}{index}")
1038 }
1039 }
1040 }
1041
1042 fn to_pascal_case(&self, s: &str) -> String {
1044 s.split(['_', '-'])
1045 .filter(|part| !part.is_empty())
1046 .map(|part| {
1047 let mut chars = part.chars();
1048 match chars.next() {
1049 None => String::new(),
1050 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1051 }
1052 })
1053 .collect()
1054 }
1055
1056 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
1057 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
1062 Ok(schemas
1063 .map(|m| {
1064 m.iter()
1065 .map(|(k, v)| (k.clone(), v.clone()))
1066 .collect::<BTreeMap<_, _>>()
1067 })
1068 .unwrap_or_default())
1069 }
1070
1071 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
1072 let validation_context = ValidationContext {
1073 openapi_version: self
1074 .openapi_spec
1075 .get("openapi")
1076 .and_then(Value::as_str)
1077 .unwrap_or_default()
1078 .to_string(),
1079 json_schema_dialect: self
1080 .openapi_spec
1081 .get("jsonSchemaDialect")
1082 .and_then(Value::as_str)
1083 .map(str::to_string),
1084 component_schemas: self
1085 .openapi_spec
1086 .pointer("/components/schemas")
1087 .and_then(Value::as_object)
1088 .map(|schemas| {
1089 schemas
1090 .iter()
1091 .map(|(name, schema)| (name.clone(), schema.clone()))
1092 .collect()
1093 })
1094 .unwrap_or_default(),
1095 };
1096 let mut analysis = SchemaAnalysis {
1097 schemas: BTreeMap::new(),
1098 dependencies: DependencyGraph::new(),
1099 patterns: DetectedPatterns {
1100 tagged_enum_schemas: HashSet::new(),
1101 untagged_enum_schemas: HashSet::new(),
1102 type_mappings: BTreeMap::new(),
1103 },
1104 operations: BTreeMap::new(),
1105 operation_responses: BTreeMap::new(),
1106 operation_id_aliases: BTreeMap::new(),
1107 used_type_features: crate::type_mapping::UsedFeatures::default(),
1108 enum_extensions: BTreeMap::new(),
1109 validation_context,
1110 };
1111
1112 self.detect_patterns(&mut analysis.patterns)?;
1114
1115 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
1117 for schema_name in schema_names {
1118 let analyzed = self.analyze_schema(&schema_name)?;
1119
1120 for dep in &analyzed.dependencies {
1122 analysis
1123 .dependencies
1124 .add_dependency(schema_name.clone(), dep.clone());
1125 }
1126
1127 analysis.schemas.insert(schema_name, analyzed);
1128 }
1129
1130 for (inline_name, inline_schema) in &self.resolved_cache {
1133 if !analysis.schemas.contains_key(inline_name) {
1134 analysis
1136 .schemas
1137 .insert(inline_name.clone(), inline_schema.clone());
1138
1139 for dep in &inline_schema.dependencies {
1141 analysis
1142 .dependencies
1143 .add_dependency(inline_name.clone(), dep.clone());
1144 }
1145
1146 let mut schemas_to_update = Vec::new();
1151 for (schema_name, schema) in &analysis.schemas {
1152 if schema_name == inline_name {
1154 continue;
1155 }
1156
1157 if schema.dependencies.contains(inline_name) {
1158 schemas_to_update.push(schema_name.clone());
1160 }
1161 }
1162
1163 for schema_name in schemas_to_update {
1165 analysis
1166 .dependencies
1167 .add_dependency(schema_name, inline_name.clone());
1168 }
1169 }
1170 }
1171
1172 self.analyze_operations(&mut analysis)?;
1174
1175 for (inline_name, inline_schema) in &self.resolved_cache {
1178 if !analysis.schemas.contains_key(inline_name) {
1179 analysis
1180 .schemas
1181 .insert(inline_name.clone(), inline_schema.clone());
1182
1183 for dep in &inline_schema.dependencies {
1185 analysis
1186 .dependencies
1187 .add_dependency(inline_name.clone(), dep.clone());
1188 }
1189 }
1190 }
1191
1192 analysis.used_type_features = self.type_mapper.used_features();
1196
1197 for (name, analyzed) in &analysis.schemas {
1202 let enum_value_count = match &analyzed.schema_type {
1203 SchemaType::StringEnum { values } => values.len(),
1204 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1205 _ => continue,
1206 };
1207 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1208 analysis.enum_extensions.insert(name.clone(), ext);
1209 }
1210 }
1211
1212 Ok(analysis)
1213 }
1214
1215 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1216 for (schema_name, schema) in &self.schemas {
1217 if self.is_discriminated_union(schema) {
1219 patterns.tagged_enum_schemas.insert(schema_name.clone());
1220
1221 if let Some(mappings) = self.extract_type_mappings(schema)? {
1223 patterns.type_mappings.insert(schema_name.clone(), mappings);
1224 }
1225 }
1226 else if self.is_simple_union(schema) {
1228 patterns.untagged_enum_schemas.insert(schema_name.clone());
1229 }
1230 }
1231
1232 Ok(())
1233 }
1234
1235 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1236 if schema.is_discriminated_union() {
1238 return true;
1239 }
1240
1241 if let Some(variants) = schema.union_variants() {
1243 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1244 }
1245
1246 false
1247 }
1248
1249 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1250 variants.iter().all(|variant| {
1251 if let Some(ref_str) = variant.reference() {
1252 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1254 if let Some(schema) = self.schemas.get(schema_name) {
1255 return self.has_const_discriminator_field(schema, field_name);
1256 }
1257 }
1258 } else {
1259 return self.has_const_discriminator_field(variant, field_name);
1261 }
1262 false
1263 })
1264 }
1265
1266 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1275 if let Some(ref_str) = schema.reference() {
1277 return match self
1278 .extract_schema_name(ref_str)
1279 .and_then(|n| self.schemas.get(n))
1280 {
1281 Some(target) => self.branch_resolves_to_object(target),
1282 None => false,
1283 };
1284 }
1285 if matches!(
1288 schema,
1289 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1290 ) {
1291 return true;
1292 }
1293 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1294 return true;
1295 }
1296 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1297 return true;
1298 }
1299 false
1302 }
1303
1304 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1308 if variants.is_empty() {
1309 return None;
1310 }
1311
1312 let first_variant = &variants[0];
1314 let first_schema = if let Some(ref_str) = first_variant.reference() {
1315 let schema_name = self.extract_schema_name(ref_str)?;
1316 self.schemas.get(schema_name)?
1317 } else {
1318 first_variant
1319 };
1320
1321 let properties = first_schema.details().properties.as_ref()?;
1322 let mut candidates: Vec<String> = Vec::new();
1323
1324 for (field_name, field_schema) in properties {
1325 let details = field_schema.details();
1326 let is_const = details.const_value.is_some()
1327 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1328 || details.extra.contains_key("const");
1329 if is_const {
1330 candidates.push(field_name.clone());
1331 }
1332 }
1333
1334 if candidates.is_empty() {
1335 return None;
1336 }
1337
1338 candidates.sort_by(|a, b| {
1340 if a == "type" {
1341 std::cmp::Ordering::Less
1342 } else if b == "type" {
1343 std::cmp::Ordering::Greater
1344 } else {
1345 a.cmp(b)
1346 }
1347 });
1348
1349 for candidate in &candidates {
1351 if self.all_variants_have_const_field(variants, candidate) {
1352 return Some(candidate.clone());
1353 }
1354 }
1355
1356 None
1357 }
1358
1359 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1360 if let Some(properties) = &schema.details().properties {
1361 if let Some(field) = properties.get(field_name) {
1362 if field.details().const_value.is_some() {
1364 return true;
1365 }
1366 if let Some(enum_vals) = &field.details().enum_values {
1368 return enum_vals.len() == 1;
1369 }
1370 return field.details().extra.contains_key("const");
1372 }
1373 }
1374 false
1375 }
1376
1377 fn is_simple_union(&self, schema: &Schema) -> bool {
1378 if let Some(variants) = schema.union_variants() {
1379 if variants.len() > 1 && !schema.is_nullable_pattern() {
1381 let has_refs = variants.iter().any(|v| v.is_reference());
1382 return has_refs;
1383 }
1384 }
1385 false
1386 }
1387
1388 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1389 let variants = schema.union_variants().ok_or_else(|| {
1390 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1391 })?;
1392
1393 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1395 discriminator.property_name.clone()
1396 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1397 detected
1398 } else {
1399 "type".to_string() };
1401
1402 let mut mappings = BTreeMap::new();
1403
1404 for variant in variants {
1405 if let Some(ref_str) = variant.reference() {
1406 if let Some(type_name) = self.extract_schema_name(ref_str) {
1407 if let Some(variant_schema) = self.schemas.get(type_name) {
1408 if let Some(discriminator_value) = self
1409 .extract_discriminator_value_for_field(
1410 variant_schema,
1411 &discriminator_field,
1412 )
1413 {
1414 mappings.insert(type_name.to_string(), discriminator_value);
1415 }
1416 }
1417 }
1418 }
1419 }
1420
1421 if mappings.is_empty() {
1422 Ok(None)
1423 } else {
1424 Ok(Some(mappings))
1425 }
1426 }
1427
1428 #[allow(dead_code)]
1429 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1430 self.extract_discriminator_value_for_field(schema, "type")
1431 }
1432
1433 fn extract_discriminator_value_for_field(
1434 &self,
1435 schema: &Schema,
1436 field_name: &str,
1437 ) -> Option<String> {
1438 if let Some(properties) = &schema.details().properties {
1439 if let Some(type_field) = properties.get(field_name) {
1440 if let Some(const_value) = &type_field.details().const_value {
1442 if let Some(value) = const_value.as_str() {
1443 return Some(value.to_string());
1444 }
1445 }
1446 if let Some(enum_values) = &type_field.details().enum_values {
1448 if enum_values.len() == 1 {
1449 return enum_values[0].as_str().map(|s| s.to_string());
1450 }
1451 }
1452 if let Some(const_value) = type_field.details().extra.get("const") {
1454 return const_value.as_str().map(|s| s.to_string());
1455 }
1456 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1458 if stainless_const.as_bool() == Some(true) {
1459 if let Some(default_value) = &type_field.details().default {
1460 if let Some(value) = default_value.as_str() {
1461 return Some(value.to_string());
1462 }
1463 }
1464 }
1465 }
1466 }
1467 }
1468 None
1469 }
1470
1471 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1472 schema.reference().or_else(|| schema.recursive_reference())
1473 }
1474
1475 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1476 if ref_str == "#" {
1477 return None; }
1479
1480 let parts: Vec<&str> = ref_str.split('/').collect();
1481
1482 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1484 return Some(parts[3]);
1485 }
1486
1487 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1490 return Some(parts[2]);
1491 }
1492
1493 let last = parts.last()?;
1499 if last.is_empty()
1500 || last.chars().all(|c| c.is_ascii_digit())
1501 || matches!(
1502 *last,
1503 "schema" | "properties" | "items" | "additionalProperties"
1504 )
1505 {
1506 return None;
1507 }
1508 let first = last.chars().next().unwrap_or(' ');
1509 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1510 return None;
1511 }
1512 Some(last)
1513 }
1514
1515 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1516 if let Some(cached) = self.resolved_cache.get(schema_name) {
1518 return Ok(cached.clone());
1519 }
1520
1521 self.current_schema_name = Some(schema_name.to_string());
1523
1524 let schema = self
1525 .schemas
1526 .get(schema_name)
1527 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1528 .clone();
1529
1530 self.resolved_cache.insert(
1532 schema_name.to_string(),
1533 AnalyzedSchema {
1534 name: schema_name.to_string(),
1535 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1536 schema_type: SchemaType::Reference {
1537 target: "placeholder".to_string(),
1538 },
1539 dependencies: HashSet::new(),
1540 nullable: false,
1541 description: None,
1542 default: None,
1543 },
1544 );
1545
1546 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1547
1548 self.resolved_cache
1550 .insert(schema_name.to_string(), analyzed.clone());
1551
1552 Ok(analyzed)
1553 }
1554
1555 fn analyze_schema_value(
1556 &mut self,
1557 schema: &Schema,
1558 schema_name: &str,
1559 ) -> Result<AnalyzedSchema> {
1560 let details = schema.details();
1561 let description = details.description.clone();
1562 let nullable = details.is_nullable() || schema.type_array_contains_null();
1564 let mut dependencies = HashSet::new();
1565
1566 let schema_type = match schema {
1567 Schema::Reference { reference, .. } => {
1568 match self.extract_schema_name(reference) {
1573 Some(name) => {
1574 let target = name.to_string();
1575 dependencies.insert(target.clone());
1576 SchemaType::Reference { target }
1577 }
1578 None => {
1579 eprintln!(
1580 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1581 reference
1582 );
1583 SchemaType::Primitive {
1584 rust_type: "serde_json::Value".to_string(),
1585 serde_with: None,
1586 }
1587 }
1588 }
1589 }
1590 Schema::RecursiveRef { recursive_ref, .. }
1591 | Schema::DynamicRef {
1592 dynamic_ref: recursive_ref,
1593 ..
1594 } => {
1595 if recursive_ref == "#" {
1601 dependencies.insert(schema_name.to_string());
1602 SchemaType::Reference {
1603 target: schema_name.to_string(),
1604 }
1605 } else {
1606 let target = self
1607 .extract_schema_name(recursive_ref)
1608 .unwrap_or(schema_name)
1609 .to_string();
1610 dependencies.insert(target.clone());
1611 SchemaType::Reference { target }
1612 }
1613 }
1614 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1615 let primary = schema
1616 .schema_type()
1617 .cloned()
1618 .unwrap_or(OpenApiSchemaType::Object);
1619 let format = details.format.as_deref();
1620 match primary {
1621 OpenApiSchemaType::String => {
1622 if let Some(values) = details.string_enum_values() {
1623 SchemaType::StringEnum { values }
1624 } else {
1625 SchemaType::Primitive {
1626 rust_type: self.type_mapper.string_format(format).rust_type,
1627 serde_with: None,
1628 }
1629 }
1630 }
1631 OpenApiSchemaType::Integer => SchemaType::Primitive {
1632 rust_type: self.type_mapper.integer_format(format).rust_type,
1633 serde_with: None,
1634 },
1635 OpenApiSchemaType::Number => SchemaType::Primitive {
1636 rust_type: self.type_mapper.number_format(format).rust_type,
1637 serde_with: None,
1638 },
1639 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1640 rust_type: self.type_mapper.boolean().rust_type,
1641 serde_with: None,
1642 },
1643 OpenApiSchemaType::Array => {
1644 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1646 }
1647 OpenApiSchemaType::Object => {
1648 if self.should_use_dynamic_json(schema) {
1650 SchemaType::Primitive {
1651 rust_type: self.type_mapper.dynamic_json().rust_type,
1652 serde_with: None,
1653 }
1654 } else {
1655 self.analyze_object_schema(schema, &mut dependencies)?
1657 }
1658 }
1659 _ => SchemaType::Primitive {
1660 rust_type: self.type_mapper.dynamic_json().rust_type,
1661 serde_with: None,
1662 },
1663 }
1664 }
1665 Schema::AnyOf {
1666 any_of,
1667 discriminator,
1668 ..
1669 } => {
1670 self.analyze_anyof_union(
1672 any_of,
1673 discriminator.as_ref(),
1674 &mut dependencies,
1675 schema_name,
1676 )?
1677 }
1678 Schema::OneOf {
1679 one_of,
1680 discriminator,
1681 ..
1682 } => {
1683 self.analyze_oneof_union(
1685 one_of,
1686 discriminator.as_ref(),
1687 schema_name,
1688 &mut dependencies,
1689 )?
1690 }
1691 Schema::AllOf { all_of, .. } => {
1692 self.analyze_allof_composition(all_of, &mut dependencies)?
1694 }
1695 Schema::Untyped { .. } => {
1696 if let Some(inferred) = schema.inferred_type() {
1698 match inferred {
1699 OpenApiSchemaType::Object => {
1700 if self.should_use_dynamic_json(schema) {
1701 SchemaType::Primitive {
1702 rust_type: "serde_json::Value".to_string(),
1703 serde_with: None,
1704 }
1705 } else {
1706 self.analyze_object_schema(schema, &mut dependencies)?
1707 }
1708 }
1709 OpenApiSchemaType::String if details.is_string_enum() => {
1710 SchemaType::StringEnum {
1711 values: details.string_enum_values().unwrap_or_default(),
1712 }
1713 }
1714 _ => SchemaType::Primitive {
1715 rust_type: "serde_json::Value".to_string(),
1716 serde_with: None,
1717 },
1718 }
1719 } else {
1720 SchemaType::Primitive {
1721 rust_type: "serde_json::Value".to_string(),
1722 serde_with: None,
1723 }
1724 }
1725 }
1726 };
1727
1728 Ok(AnalyzedSchema {
1729 name: schema_name.to_string(),
1730 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1732 dependencies,
1733 nullable,
1734 description,
1735 default: details.default.clone(),
1736 })
1737 }
1738
1739 fn analyze_object_schema(
1740 &mut self,
1741 schema: &Schema,
1742 dependencies: &mut HashSet<String>,
1743 ) -> Result<SchemaType> {
1744 let details = schema.details();
1745 let properties = &details.properties;
1746 let required = details
1747 .required
1748 .as_ref()
1749 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1750 .unwrap_or_default();
1751
1752 let mut property_info = BTreeMap::new();
1753
1754 if let Some(props) = properties {
1755 for (prop_name, prop_schema) in props {
1756 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1758 if self.should_use_dynamic_json(prop_schema) {
1760 SchemaType::Primitive {
1762 rust_type: "serde_json::Value".to_string(),
1763 serde_with: None,
1764 }
1765 } else if prop_schema.is_nullable_pattern()
1766 && let Some(non_null) = prop_schema.non_null_variant()
1767 {
1768 self.analyze_property_schema_with_context(
1776 non_null,
1777 Some(prop_name),
1778 dependencies,
1779 )?
1780 } else {
1781 let context_name = self
1784 .current_schema_name
1785 .clone()
1786 .unwrap_or_else(|| "Unknown".to_string());
1787
1788 let prop_pascal = self.to_pascal_case(prop_name);
1790 let mut union_type_name = format!("{context_name}{prop_pascal}");
1791
1792 if self.schemas.contains_key(&union_type_name)
1795 || self.resolved_cache.contains_key(&union_type_name)
1796 {
1797 let mut suffix = 2;
1798 loop {
1799 let candidate = format!("{union_type_name}Union{suffix}");
1800 if !self.schemas.contains_key(&candidate)
1801 && !self.resolved_cache.contains_key(&candidate)
1802 {
1803 union_type_name = candidate;
1804 break;
1805 }
1806 suffix += 1;
1807 if suffix > 1000 {
1808 break;
1809 }
1810 }
1811 }
1812
1813 let union_schema_type = self.analyze_anyof_union(
1815 any_of,
1816 prop_schema.discriminator(),
1817 dependencies,
1818 &union_type_name,
1819 )?;
1820
1821 self.resolved_cache.insert(
1823 union_type_name.clone(),
1824 AnalyzedSchema {
1825 name: union_type_name.clone(),
1826 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1827 schema_type: union_schema_type,
1828 dependencies: HashSet::new(),
1829 nullable: false,
1830 description: prop_schema.details().description.clone(),
1831 default: None,
1832 },
1833 );
1834
1835 dependencies.insert(union_type_name.clone());
1837 SchemaType::Reference {
1838 target: union_type_name,
1839 }
1840 }
1841 } else if let Schema::OneOf {
1842 one_of,
1843 discriminator,
1844 ..
1845 } = prop_schema
1846 {
1847 if prop_schema.is_nullable_pattern()
1854 && let Some(non_null) = prop_schema.non_null_variant()
1855 {
1856 let unwrapped = self.analyze_property_schema_with_context(
1857 non_null,
1858 Some(prop_name),
1859 dependencies,
1860 )?;
1861 let prop_details = prop_schema.details();
1862 let prop_nullable = true;
1863 let prop_description = prop_details.description.clone();
1864 let prop_default = prop_details.default.clone();
1865 property_info.insert(
1866 prop_name.clone(),
1867 PropertyInfo {
1868 schema_type: unwrapped,
1869 nullable: prop_nullable,
1870 description: prop_description,
1871 default: prop_default,
1872 serde_attrs: Vec::new(),
1873 constraints: PropertyConstraints::from_schema_details(prop_details),
1874 },
1875 );
1876 continue;
1877 }
1878
1879 let context_name = self
1881 .current_schema_name
1882 .clone()
1883 .unwrap_or_else(|| "Unknown".to_string());
1884 let prop_pascal = self.to_pascal_case(prop_name);
1885 let mut union_type_name = format!("{context_name}{prop_pascal}");
1886 if self.schemas.contains_key(&union_type_name)
1888 || self.resolved_cache.contains_key(&union_type_name)
1889 {
1890 let mut suffix = 2;
1891 loop {
1892 let candidate = format!("{union_type_name}Union{suffix}");
1893 if !self.schemas.contains_key(&candidate)
1894 && !self.resolved_cache.contains_key(&candidate)
1895 {
1896 union_type_name = candidate;
1897 break;
1898 }
1899 suffix += 1;
1900 if suffix > 1000 {
1901 break;
1902 }
1903 }
1904 }
1905
1906 let union_schema_type = self.analyze_oneof_union(
1908 one_of,
1909 discriminator.as_ref(),
1910 &union_type_name,
1911 dependencies,
1912 )?;
1913
1914 self.resolved_cache.insert(
1916 union_type_name.clone(),
1917 AnalyzedSchema {
1918 name: union_type_name.clone(),
1919 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1920 schema_type: union_schema_type,
1921 dependencies: HashSet::new(),
1922 nullable: false,
1923 description: prop_schema.details().description.clone(),
1924 default: None,
1925 },
1926 );
1927
1928 dependencies.insert(union_type_name.clone());
1930 SchemaType::Reference {
1931 target: union_type_name,
1932 }
1933 } else {
1934 self.analyze_property_schema_with_context(
1936 prop_schema,
1937 Some(prop_name),
1938 dependencies,
1939 )?
1940 };
1941
1942 let prop_details = prop_schema.details();
1943 let prop_nullable = prop_schema.is_nullable_any();
1945 let prop_description = prop_details.description.clone();
1946 let prop_default = prop_details.default.clone();
1947
1948 property_info.insert(
1949 prop_name.clone(),
1950 PropertyInfo {
1951 schema_type: prop_type,
1952 nullable: prop_nullable,
1953 description: prop_description,
1954 default: prop_default,
1955 serde_attrs: Vec::new(),
1956 constraints: PropertyConstraints::from_schema_details(prop_details),
1957 },
1958 );
1959 }
1960 }
1961
1962 let typed_enabled = self
1970 .type_mapper
1971 .config()
1972 .shape
1973 .as_ref()
1974 .and_then(|s| s.additional_properties_typed)
1975 .unwrap_or(true);
1976
1977 let additional_properties = match &details.additional_properties {
1978 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
1979 ObjectAdditionalProperties::Untyped
1980 }
1981 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
1982 ObjectAdditionalProperties::Forbidden
1983 }
1984 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
1985 let analyzed =
1986 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
1987 ObjectAdditionalProperties::Typed {
1988 value_type: Box::new(analyzed),
1989 }
1990 }
1991 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1992 ObjectAdditionalProperties::Untyped
1994 }
1995 None => ObjectAdditionalProperties::Forbidden,
1996 };
1997
1998 Ok(SchemaType::Object {
1999 properties: property_info,
2000 required,
2001 additional_properties,
2002 })
2003 }
2004
2005 fn analyze_property_schema_with_context(
2006 &mut self,
2007 schema: &Schema,
2008 property_name: Option<&str>,
2009 dependencies: &mut HashSet<String>,
2010 ) -> Result<SchemaType> {
2011 if let Some(ref_str) = self.get_any_reference(schema) {
2012 let target_opt = if ref_str == "#" {
2013 Some(
2014 self.find_recursive_anchor_schema()
2015 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2016 )
2017 } else {
2018 self.extract_schema_name(ref_str).map(|s| s.to_string())
2019 };
2020 match target_opt {
2021 Some(target) => {
2022 dependencies.insert(target.clone());
2023 return Ok(SchemaType::Reference { target });
2024 }
2025 None => {
2026 eprintln!(
2027 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2028 ref_str
2029 );
2030 return Ok(SchemaType::Primitive {
2031 rust_type: "serde_json::Value".to_string(),
2032 serde_with: None,
2033 });
2034 }
2035 }
2036 }
2037
2038 if let Some(schema_type) = schema.schema_type() {
2039 match schema_type {
2040 OpenApiSchemaType::String => {
2041 if let Some(enum_values) = schema.details().string_enum_values() {
2043 let context_name = self
2046 .current_schema_name
2047 .clone()
2048 .unwrap_or_else(|| "Unknown".to_string());
2049
2050 let primary_name = if let Some(prop_name) = property_name {
2052 let prop_pascal = self.to_pascal_case(prop_name);
2054 format!("{context_name}{prop_pascal}")
2055 } else {
2056 let suffix = if !enum_values.is_empty() {
2059 let first_value = self.to_pascal_case(&enum_values[0]);
2060 format!("{first_value}Enum")
2061 } else {
2062 "StringEnum".to_string()
2063 };
2064 format!("{context_name}{suffix}")
2065 };
2066
2067 return Ok(self.hoist_inline_string_enum(
2068 schema,
2069 enum_values,
2070 primary_name,
2071 dependencies,
2072 ));
2073 } else {
2074 let mapped = self
2080 .type_mapper
2081 .string_format(schema.details().format.as_deref());
2082 return Ok(SchemaType::Primitive {
2083 rust_type: mapped.rust_type,
2084 serde_with: mapped.serde_with,
2085 });
2086 }
2087 }
2088 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2089 let details = schema.details();
2090 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2091 return Ok(SchemaType::Primitive {
2092 rust_type,
2093 serde_with: None,
2094 });
2095 }
2096 OpenApiSchemaType::Boolean => {
2097 return Ok(SchemaType::Primitive {
2098 rust_type: "bool".to_string(),
2099 serde_with: None,
2100 });
2101 }
2102 OpenApiSchemaType::Array => {
2103 let context_name = if let Some(prop_name) = property_name {
2105 let prop_pascal = self.to_pascal_case(prop_name);
2107 format!(
2108 "{}{}",
2109 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2110 prop_pascal
2111 )
2112 } else {
2113 "ArrayItem".to_string()
2115 };
2116 return self.analyze_array_schema(schema, &context_name, dependencies);
2117 }
2118 OpenApiSchemaType::Object => {
2119 if self.should_use_dynamic_json(schema) {
2121 return Ok(SchemaType::Primitive {
2122 rust_type: "serde_json::Value".to_string(),
2123 serde_with: None,
2124 });
2125 }
2126 let object_type_name = if let Some(prop_name) = property_name {
2128 let prop_pascal = self.to_pascal_case(prop_name);
2130 format!(
2131 "{}{}",
2132 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2133 prop_pascal
2134 )
2135 } else {
2136 format!(
2138 "{}Object",
2139 self.current_schema_name.as_deref().unwrap_or("Unknown")
2140 )
2141 };
2142
2143 let object_type = self.analyze_object_schema(schema, dependencies)?;
2145
2146 let inline_schema = AnalyzedSchema {
2148 name: object_type_name.clone(),
2149 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2150 schema_type: object_type,
2151 dependencies: dependencies.clone(),
2152 nullable: false,
2153 description: schema.details().description.clone(),
2154 default: None,
2155 };
2156
2157 self.resolved_cache
2159 .insert(object_type_name.clone(), inline_schema);
2160 dependencies.insert(object_type_name.clone());
2161
2162 return Ok(SchemaType::Reference {
2164 target: object_type_name,
2165 });
2166 }
2167 _ => {
2168 return Ok(SchemaType::Primitive {
2169 rust_type: "serde_json::Value".to_string(),
2170 serde_with: None,
2171 });
2172 }
2173 }
2174 }
2175
2176 if schema.is_nullable_pattern() {
2178 if let Some(non_null) = schema.non_null_variant() {
2179 return self.analyze_property_schema_with_context(
2180 non_null,
2181 property_name,
2182 dependencies,
2183 );
2184 }
2185 }
2186
2187 if self.should_use_dynamic_json(schema) {
2189 return Ok(SchemaType::Primitive {
2190 rust_type: "serde_json::Value".to_string(),
2191 serde_with: None,
2192 });
2193 }
2194
2195 if let Schema::AllOf { all_of, .. } = schema {
2197 return self.analyze_allof_composition(all_of, dependencies);
2198 }
2199
2200 if let Some(variants) = schema.union_variants() {
2202 match variants.len().cmp(&1) {
2203 std::cmp::Ordering::Equal => {
2204 return self.analyze_property_schema_with_context(
2206 &variants[0],
2207 property_name,
2208 dependencies,
2209 );
2210 }
2211 std::cmp::Ordering::Greater => {
2212 let union_name = if let Some(prop_name) = property_name {
2215 let prop_pascal = self.to_pascal_case(prop_name);
2217 format!(
2218 "{}{}",
2219 self.current_schema_name.as_deref().unwrap_or(""),
2220 prop_pascal
2221 )
2222 } else {
2223 "UnionType".to_string()
2224 };
2225
2226 if let Schema::OneOf {
2228 one_of,
2229 discriminator,
2230 ..
2231 } = schema
2232 {
2233 let oneof_result = self.analyze_oneof_union(
2235 one_of,
2236 discriminator.as_ref(),
2237 &union_name,
2238 dependencies,
2239 )?;
2240
2241 if let SchemaType::Union {
2243 variants: _union_variants,
2244 } = &oneof_result
2245 {
2246 self.resolved_cache.insert(
2248 union_name.clone(),
2249 AnalyzedSchema {
2250 name: union_name.clone(),
2251 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2252 schema_type: oneof_result.clone(),
2253 dependencies: dependencies.clone(),
2254 nullable: false,
2255 description: schema.details().description.clone(),
2256 default: None,
2257 },
2258 );
2259
2260 dependencies.insert(union_name.clone());
2262 return Ok(SchemaType::Reference { target: union_name });
2263 }
2264
2265 return Ok(oneof_result);
2266 } else if let Schema::AnyOf {
2267 any_of,
2268 discriminator,
2269 ..
2270 } = schema
2271 {
2272 let union_analysis = self.analyze_anyof_union(
2274 any_of,
2275 discriminator.as_ref(),
2276 dependencies,
2277 &union_name,
2278 )?;
2279 return Ok(union_analysis);
2280 } else {
2281 let mut union_variants = Vec::new();
2284 for variant in variants {
2285 if let Some(ref_str) = variant.reference() {
2286 if let Some(target) = self.extract_schema_name(ref_str) {
2287 dependencies.insert(target.to_string());
2288 union_variants.push(SchemaRef {
2289 target: target.to_string(),
2290 nullable: false,
2291 });
2292 }
2293 }
2294 }
2295 return Ok(SchemaType::Union {
2296 variants: union_variants,
2297 });
2298 }
2299 }
2300 std::cmp::Ordering::Less => {}
2301 }
2302 }
2303
2304 if let Some(inferred_type) = schema.inferred_type() {
2306 match inferred_type {
2307 OpenApiSchemaType::Object => {
2308 if self.should_use_dynamic_json(schema) {
2310 return Ok(SchemaType::Primitive {
2311 rust_type: "serde_json::Value".to_string(),
2312 serde_with: None,
2313 });
2314 }
2315 return self.analyze_object_schema(schema, dependencies);
2316 }
2317 OpenApiSchemaType::Array => {
2318 let context_name = if let Some(prop_name) = property_name {
2319 let prop_pascal = self.to_pascal_case(prop_name);
2321 format!(
2322 "{}{}",
2323 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2324 prop_pascal
2325 )
2326 } else {
2327 "ArrayItem".to_string()
2329 };
2330 return self.analyze_array_schema(schema, &context_name, dependencies);
2331 }
2332 OpenApiSchemaType::String => {
2333 if let Some(enum_values) = schema.details().string_enum_values() {
2334 return Ok(SchemaType::StringEnum {
2335 values: enum_values,
2336 });
2337 } else {
2338 return Ok(SchemaType::Primitive {
2339 rust_type: "String".to_string(),
2340 serde_with: None,
2341 });
2342 }
2343 }
2344 _ => {
2345 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2347 return Ok(SchemaType::Primitive {
2348 rust_type,
2349 serde_with: None,
2350 });
2351 }
2352 }
2353 }
2354
2355 Ok(SchemaType::Primitive {
2356 rust_type: "serde_json::Value".to_string(),
2357 serde_with: None,
2358 })
2359 }
2360
2361 fn analyze_allof_composition(
2362 &mut self,
2363 all_of_schemas: &[Schema],
2364 dependencies: &mut HashSet<String>,
2365 ) -> Result<SchemaType> {
2366 if all_of_schemas.len() == 1 {
2369 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
2370 if let Some(target) = self.extract_schema_name(reference) {
2371 dependencies.insert(target.to_string());
2372 return Ok(SchemaType::Reference {
2373 target: target.to_string(),
2374 });
2375 }
2376 }
2377 }
2378
2379 let mut merged_properties = BTreeMap::new();
2381 let mut merged_required = HashSet::new();
2382 let mut descriptions = Vec::new();
2383
2384 let current_context = self.current_schema_name.clone();
2386
2387 for schema in all_of_schemas {
2388 match schema {
2389 Schema::Reference { reference, .. } => {
2390 if let Some(target) = self.extract_schema_name(reference) {
2392 dependencies.insert(target.to_string());
2393
2394 let analyzed_ref = self.analyze_schema(target)?;
2396
2397 match &analyzed_ref.schema_type {
2399 SchemaType::Object {
2400 properties,
2401 required,
2402 ..
2403 } => {
2404 for (prop_name, prop_info) in properties {
2406 merged_properties.insert(prop_name.clone(), prop_info.clone());
2407 }
2408 for req in required {
2410 merged_required.insert(req.clone());
2411 }
2412 }
2413 _ => {
2414 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2416 self.merge_schema_into_properties(
2417 &ref_schema,
2418 &mut merged_properties,
2419 &mut merged_required,
2420 dependencies,
2421 )?;
2422 }
2423 }
2424 }
2425 }
2426 }
2427 Schema::Typed {
2428 schema_type: OpenApiSchemaType::Object,
2429 ..
2430 }
2431 | Schema::Untyped { .. } => {
2432 let saved_context = self.current_schema_name.clone();
2434 self.current_schema_name = current_context.clone();
2435
2436 self.merge_schema_into_properties(
2438 schema,
2439 &mut merged_properties,
2440 &mut merged_required,
2441 dependencies,
2442 )?;
2443
2444 self.current_schema_name = saved_context;
2446 }
2447 _ => {
2448 self.merge_schema_into_properties(
2451 schema,
2452 &mut merged_properties,
2453 &mut merged_required,
2454 dependencies,
2455 )?;
2456 }
2457 }
2458
2459 if let Some(desc) = &schema.details().description {
2461 descriptions.push(desc.clone());
2462 }
2463 }
2464
2465 if !merged_properties.is_empty() {
2467 Ok(SchemaType::Object {
2468 properties: merged_properties,
2469 required: merged_required,
2470 additional_properties: ObjectAdditionalProperties::Forbidden,
2471 })
2472 } else {
2473 Ok(SchemaType::Composition {
2475 schemas: all_of_schemas
2476 .iter()
2477 .filter_map(|s| {
2478 if let Some(ref_str) = s.reference() {
2479 if let Some(target) = self.extract_schema_name(ref_str) {
2480 dependencies.insert(target.to_string());
2481 Some(SchemaRef {
2482 target: target.to_string(),
2483 nullable: false,
2484 })
2485 } else {
2486 None
2487 }
2488 } else {
2489 None
2490 }
2491 })
2492 .collect(),
2493 })
2494 }
2495 }
2496
2497 fn merge_schema_into_properties(
2498 &mut self,
2499 schema: &Schema,
2500 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2501 merged_required: &mut HashSet<String>,
2502 dependencies: &mut HashSet<String>,
2503 ) -> Result<()> {
2504 let details = schema.details();
2505
2506 if let Some(properties) = &details.properties {
2508 for (prop_name, prop_schema) in properties {
2509 let prop_type = self.analyze_property_schema_with_context(
2510 prop_schema,
2511 Some(prop_name),
2512 dependencies,
2513 )?;
2514 let prop_details = prop_schema.details();
2515
2516 let nullable = prop_schema.is_nullable_any();
2523 merged_properties.insert(
2524 prop_name.clone(),
2525 PropertyInfo {
2526 schema_type: prop_type,
2527 nullable,
2528 description: prop_details.description.clone(),
2529 default: prop_details.default.clone(),
2530 serde_attrs: Vec::new(),
2531 constraints: PropertyConstraints::from_schema_details(prop_details),
2532 },
2533 );
2534 }
2535 }
2536
2537 if let Some(required) = &details.required {
2539 for field in required {
2540 merged_required.insert(field.clone());
2541 }
2542 }
2543
2544 Ok(())
2545 }
2546
2547 fn analyze_oneof_union(
2548 &mut self,
2549 one_of_schemas: &[Schema],
2550 discriminator: Option<&crate::openapi::Discriminator>,
2551 parent_name: &str,
2552 dependencies: &mut HashSet<String>,
2553 ) -> Result<SchemaType> {
2554 if one_of_schemas.len() == 2 {
2557 let null_count = one_of_schemas
2558 .iter()
2559 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2560 .count();
2561 if null_count == 1 {
2562 if let Some(non_null) = one_of_schemas
2563 .iter()
2564 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2565 {
2566 return self
2567 .analyze_schema_value(non_null, parent_name)
2568 .map(|a| a.schema_type);
2569 }
2570 }
2571 }
2572
2573 if discriminator.is_none() {
2575 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2577 }
2578
2579 if one_of_schemas
2585 .iter()
2586 .any(|s| !self.branch_resolves_to_object(s))
2587 {
2588 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2589 }
2590
2591 let discriminator_field = discriminator
2593 .ok_or_else(|| {
2594 GeneratorError::InvalidDiscriminator(
2595 "expected discriminator after guard check".to_string(),
2596 )
2597 })?
2598 .property_name
2599 .clone();
2600
2601 let mut variants = Vec::new();
2602 let mut used_variant_names = std::collections::HashSet::new();
2603
2604 for variant_schema in one_of_schemas {
2605 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2607 Some((ref_str, false))
2608 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2609 Some((recursive_ref, true))
2610 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2611 if all_of.len() == 1 {
2613 if let Some(ref_str) = all_of[0].reference() {
2614 Some((ref_str, false))
2615 } else {
2616 all_of[0]
2617 .recursive_reference()
2618 .map(|recursive_ref| (recursive_ref, true))
2619 }
2620 } else {
2621 None
2622 }
2623 } else {
2624 None
2625 };
2626
2627 if let Some((ref_str, is_recursive)) = ref_info {
2628 let schema_name = if is_recursive && ref_str == "#" {
2629 self.find_recursive_anchor_schema()
2631 .or_else(|| self.current_schema_name.clone())
2632 .unwrap_or_else(|| "CompoundFilter".to_string())
2633 } else {
2634 self.extract_schema_name(ref_str)
2635 .map(|s| s.to_string())
2636 .unwrap_or_else(|| "UnknownRef".to_string())
2637 };
2638
2639 if !schema_name.is_empty() {
2640 dependencies.insert(schema_name.clone());
2641
2642 let discriminator_value = if let Some(disc) = discriminator {
2647 if let Some(mappings) = &disc.mapping {
2648 mappings
2651 .iter()
2652 .find(|(_, target_ref)| {
2653 target_ref.as_str() == ref_str
2655 || self
2656 .extract_schema_name(target_ref)
2657 .map(|s| s.to_string())
2658 == Some(schema_name.clone())
2659 })
2660 .map(|(key, _)| key.clone())
2661 .unwrap_or_else(|| {
2662 self.fallback_discriminator_value_for_field(
2663 &schema_name,
2664 &discriminator_field,
2665 )
2666 })
2667 } else {
2668 self.fallback_discriminator_value_for_field(
2669 &schema_name,
2670 &discriminator_field,
2671 )
2672 }
2673 } else {
2674 self.fallback_discriminator_value_for_field(
2675 &schema_name,
2676 &discriminator_field,
2677 )
2678 };
2679
2680 let base_name = self.to_rust_variant_name(&schema_name);
2682 let rust_name =
2683 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2684
2685 let final_discriminator_value = discriminator_value;
2687
2688 variants.push(UnionVariant {
2689 rust_name,
2690 type_name: schema_name,
2691 discriminator_value: final_discriminator_value,
2692 schema_ref: ref_str.to_string(),
2693 });
2694 }
2695 } else {
2696 let variant_index = variants.len();
2698 let inline_type_name =
2699 self.generate_inline_type_name(variant_schema, variant_index);
2700
2701 let discriminator_value = if let Some(disc) = discriminator {
2703 if let Some(mappings) = &disc.mapping {
2704 mappings
2706 .iter()
2707 .find(|(_, target_ref)| {
2708 target_ref.contains(&format!("variant_{variant_index}"))
2709 })
2710 .map(|(key, _)| key.clone())
2711 .unwrap_or_else(|| {
2712 self.extract_inline_discriminator_value(
2713 variant_schema,
2714 &discriminator_field,
2715 variant_index,
2716 )
2717 })
2718 } else {
2719 self.extract_inline_discriminator_value(
2720 variant_schema,
2721 &discriminator_field,
2722 variant_index,
2723 )
2724 }
2725 } else {
2726 self.extract_inline_discriminator_value(
2727 variant_schema,
2728 &discriminator_field,
2729 variant_index,
2730 )
2731 };
2732
2733 let base_name = if discriminator_value.starts_with("variant_") {
2735 format!("Variant{variant_index}")
2736 } else {
2737 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2739 self.to_rust_variant_name(&clean_name)
2740 };
2741 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2742
2743 let final_discriminator_value = discriminator_value;
2745
2746 variants.push(UnionVariant {
2747 rust_name,
2748 type_name: inline_type_name.clone(),
2749 discriminator_value: final_discriminator_value,
2750 schema_ref: format!("inline_{variant_index}"),
2751 });
2752
2753 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2755 }
2756 }
2757
2758 if variants.is_empty() {
2759 let mut union_variants = Vec::new();
2762
2763 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2764 if let Some(ref_str) = variant_schema.reference() {
2766 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2767 dependencies.insert(schema_name.to_string());
2768 union_variants.push(SchemaRef {
2769 target: schema_name.to_string(),
2770 nullable: false,
2771 });
2772 }
2773 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2774 let schema_name = if recursive_ref == "#" {
2775 self.find_recursive_anchor_schema()
2777 .or_else(|| self.current_schema_name.clone())
2778 .unwrap_or_else(|| "CompoundFilter".to_string())
2779 } else {
2780 self.extract_schema_name(recursive_ref)
2781 .map(|s| s.to_string())
2782 .unwrap_or_else(|| "RecursiveType".to_string())
2783 };
2784 dependencies.insert(schema_name.clone());
2785 union_variants.push(SchemaRef {
2786 target: schema_name,
2787 nullable: false,
2788 });
2789 } else {
2790 let inline_name = self.generate_context_aware_name(
2792 parent_name,
2793 "InlineVariant",
2794 variant_index,
2795 Some(variant_schema),
2796 );
2797 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2798 let variant_type = analyzed.schema_type;
2799
2800 for dep in &analyzed.dependencies {
2802 dependencies.insert(dep.clone());
2803 }
2804
2805 match &variant_type {
2806 SchemaType::Primitive { rust_type, .. } => {
2808 union_variants.push(SchemaRef {
2809 target: rust_type.clone(),
2810 nullable: false,
2811 });
2812 }
2813 SchemaType::Array { item_type } => {
2815 match item_type.as_ref() {
2816 SchemaType::Primitive { rust_type, .. } => {
2817 let type_name = format!("Vec<{rust_type}>");
2818 union_variants.push(SchemaRef {
2819 target: type_name,
2820 nullable: false,
2821 });
2822 }
2823 SchemaType::Reference { target } => {
2824 let type_name = format!("Vec<{target}>");
2825 union_variants.push(SchemaRef {
2826 target: type_name,
2827 nullable: false,
2828 });
2829 }
2830 _ => {
2831 let inline_type_name = self.generate_context_aware_name(
2833 parent_name,
2834 "Variant",
2835 variant_index,
2836 None,
2837 );
2838 self.add_inline_schema(
2839 &inline_type_name,
2840 variant_schema,
2841 dependencies,
2842 )?;
2843 union_variants.push(SchemaRef {
2844 target: inline_type_name,
2845 nullable: false,
2846 });
2847 }
2848 }
2849 }
2850 SchemaType::Reference { target } => {
2852 union_variants.push(SchemaRef {
2853 target: target.clone(),
2854 nullable: false,
2855 });
2856 }
2857 _ => {
2859 let inline_type_name =
2860 format!("{}Variant{}", parent_name, variant_index + 1);
2861 self.add_inline_schema(
2862 &inline_type_name,
2863 variant_schema,
2864 dependencies,
2865 )?;
2866 union_variants.push(SchemaRef {
2867 target: inline_type_name,
2868 nullable: false,
2869 });
2870 }
2871 }
2872 }
2873 }
2874
2875 if !union_variants.is_empty() {
2876 return Ok(SchemaType::Union {
2877 variants: union_variants,
2878 });
2879 }
2880
2881 return Ok(SchemaType::Primitive {
2883 rust_type: "serde_json::Value".to_string(),
2884 serde_with: None,
2885 });
2886 }
2887
2888 Ok(SchemaType::DiscriminatedUnion {
2889 discriminator_field,
2890 variants,
2891 })
2892 }
2893
2894 fn analyze_untagged_oneof_union(
2895 &mut self,
2896 one_of_schemas: &[Schema],
2897 parent_name: &str,
2898 dependencies: &mut HashSet<String>,
2899 ) -> Result<SchemaType> {
2900 let filtered: Vec<&Schema> = one_of_schemas
2904 .iter()
2905 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2906 .collect();
2907
2908 if filtered.len() == 1 {
2910 return self
2911 .analyze_schema_value(filtered[0], parent_name)
2912 .map(|a| a.schema_type);
2913 }
2914
2915 let mut union_variants = Vec::new();
2916
2917 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2918 if let Some(ref_str) = variant_schema.reference() {
2920 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2921 dependencies.insert(schema_name.to_string());
2922 union_variants.push(SchemaRef {
2923 target: schema_name.to_string(),
2924 nullable: false,
2925 });
2926 }
2927 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2928 let schema_name = if recursive_ref == "#" {
2929 self.find_recursive_anchor_schema()
2931 .or_else(|| self.current_schema_name.clone())
2932 .unwrap_or_else(|| "CompoundFilter".to_string())
2933 } else {
2934 self.extract_schema_name(recursive_ref)
2935 .map(|s| s.to_string())
2936 .unwrap_or_else(|| "RecursiveType".to_string())
2937 };
2938 dependencies.insert(schema_name.clone());
2939 union_variants.push(SchemaRef {
2940 target: schema_name,
2941 nullable: false,
2942 });
2943 } else {
2944 let inline_name = self.generate_context_aware_name(
2946 parent_name,
2947 "InlineVariant",
2948 variant_index,
2949 Some(variant_schema),
2950 );
2951 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2952 let variant_type = analyzed.schema_type;
2953
2954 for dep in &analyzed.dependencies {
2956 dependencies.insert(dep.clone());
2957 }
2958
2959 match &variant_type {
2960 SchemaType::Primitive { rust_type, .. } => {
2962 union_variants.push(SchemaRef {
2963 target: rust_type.clone(),
2964 nullable: false,
2965 });
2966 }
2967 SchemaType::Array { item_type } => {
2969 match item_type.as_ref() {
2970 SchemaType::Primitive { rust_type, .. } => {
2971 let type_name = format!("Vec<{rust_type}>");
2972 union_variants.push(SchemaRef {
2973 target: type_name,
2974 nullable: false,
2975 });
2976 }
2977 SchemaType::Reference { target } => {
2978 let type_name = format!("Vec<{target}>");
2979 union_variants.push(SchemaRef {
2980 target: type_name,
2981 nullable: false,
2982 });
2983 }
2984 SchemaType::Array {
2986 item_type: inner_item_type,
2987 } => {
2988 match inner_item_type.as_ref() {
2989 SchemaType::Primitive { rust_type, .. } => {
2990 let type_name = format!("Vec<Vec<{rust_type}>>");
2991 union_variants.push(SchemaRef {
2992 target: type_name,
2993 nullable: false,
2994 });
2995 }
2996 SchemaType::Reference { target } => {
2997 let type_name = format!("Vec<Vec<{target}>>");
2998 union_variants.push(SchemaRef {
2999 target: type_name,
3000 nullable: false,
3001 });
3002 }
3003 _ => {
3004 let inline_type_name = self.generate_context_aware_name(
3006 parent_name,
3007 "Variant",
3008 variant_index,
3009 None,
3010 );
3011 self.add_inline_schema(
3012 &inline_type_name,
3013 variant_schema,
3014 dependencies,
3015 )?;
3016 union_variants.push(SchemaRef {
3017 target: inline_type_name,
3018 nullable: false,
3019 });
3020 }
3021 }
3022 }
3023 _ => {
3024 let inline_type_name = self.generate_context_aware_name(
3026 parent_name,
3027 "Variant",
3028 variant_index,
3029 None,
3030 );
3031 self.add_inline_schema(
3032 &inline_type_name,
3033 variant_schema,
3034 dependencies,
3035 )?;
3036 union_variants.push(SchemaRef {
3037 target: inline_type_name,
3038 nullable: false,
3039 });
3040 }
3041 }
3042 }
3043 SchemaType::Reference { target } => {
3045 union_variants.push(SchemaRef {
3046 target: target.clone(),
3047 nullable: false,
3048 });
3049 }
3050 _ => {
3052 let inline_type_name = self.generate_context_aware_name(
3053 parent_name,
3054 "Variant",
3055 variant_index,
3056 None,
3057 );
3058 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3059 union_variants.push(SchemaRef {
3060 target: inline_type_name,
3061 nullable: false,
3062 });
3063 }
3064 }
3065 }
3066 }
3067
3068 if !union_variants.is_empty() {
3069 return Ok(SchemaType::Union {
3070 variants: union_variants,
3071 });
3072 }
3073
3074 Ok(SchemaType::Primitive {
3076 rust_type: "serde_json::Value".to_string(),
3077 serde_with: None,
3078 })
3079 }
3080
3081 fn add_inline_schema(
3082 &mut self,
3083 type_name: &str,
3084 schema: &Schema,
3085 dependencies: &mut HashSet<String>,
3086 ) -> Result<()> {
3087 if let Some(schema_type) = schema.schema_type() {
3089 match schema_type {
3090 OpenApiSchemaType::String
3091 | OpenApiSchemaType::Integer
3092 | OpenApiSchemaType::Number
3093 | OpenApiSchemaType::Boolean => {
3094 let rust_type =
3095 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3096
3097 self.resolved_cache.insert(
3099 type_name.to_string(),
3100 AnalyzedSchema {
3101 name: type_name.to_string(),
3102 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3103 schema_type: SchemaType::Primitive {
3104 rust_type,
3105 serde_with: None,
3106 },
3107 dependencies: HashSet::new(),
3108 nullable: false,
3109 description: schema.details().description.clone(),
3110 default: None,
3111 },
3112 );
3113 return Ok(());
3114 }
3115 _ => {}
3116 }
3117 }
3118
3119 let previous_schema_name = self.current_schema_name.take();
3123 self.current_schema_name = Some(type_name.to_string());
3124 let analyzed = self.analyze_schema_value(schema, type_name)?;
3125 self.current_schema_name = previous_schema_name;
3126
3127 self.resolved_cache.insert(type_name.to_string(), analyzed);
3129
3130 if let Some(cached) = self.resolved_cache.get(type_name) {
3132 for dep in &cached.dependencies {
3133 dependencies.insert(dep.clone());
3134 }
3135 }
3136
3137 Ok(())
3138 }
3139
3140 fn extract_inline_discriminator_value(
3141 &self,
3142 schema: &Schema,
3143 discriminator_field: &str,
3144 variant_index: usize,
3145 ) -> String {
3146 if let Some(properties) = &schema.details().properties {
3148 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3149 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3151 if enum_values.len() == 1 {
3152 if let Some(value) = enum_values[0].as_str() {
3153 return value.to_string();
3154 }
3155 }
3156 }
3157 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3159 if let Some(value) = const_value.as_str() {
3160 return value.to_string();
3161 }
3162 }
3163 if let Some(const_value) = &discriminator_prop.details().const_value {
3165 if let Some(value) = const_value.as_str() {
3166 return value.to_string();
3167 }
3168 }
3169 }
3170 }
3171
3172 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3174 return inferred_name;
3175 }
3176
3177 format!("variant_{variant_index}")
3179 }
3180
3181 fn infer_variant_name_from_structure(
3182 &self,
3183 schema: &Schema,
3184 _variant_index: usize,
3185 ) -> Option<String> {
3186 let details = schema.details();
3187
3188 if let Some(properties) = &details.properties {
3190 if properties.contains_key("text") && properties.len() <= 3 {
3192 return Some("text".to_string());
3193 }
3194 if properties.contains_key("image") || properties.contains_key("source") {
3195 return Some("image".to_string());
3196 }
3197 if properties.contains_key("document") {
3198 return Some("document".to_string());
3199 }
3200 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3201 return Some("tool_result".to_string());
3202 }
3203 if properties.contains_key("content") && properties.contains_key("is_error") {
3204 return Some("tool_result".to_string());
3205 }
3206 if properties.contains_key("partial_json") {
3207 return Some("partial_json".to_string());
3208 }
3209
3210 let property_names: Vec<&String> = properties.keys().collect();
3212
3213 for prop_name in &property_names {
3215 if prop_name.contains("result") {
3216 return Some("result".to_string());
3217 }
3218 if prop_name.contains("error") {
3219 return Some("error".to_string());
3220 }
3221 if prop_name.contains("content") && property_names.len() <= 2 {
3222 return Some("content".to_string());
3223 }
3224 }
3225
3226 let significant_props = property_names
3228 .iter()
3229 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3230 .collect::<Vec<_>>();
3231
3232 if significant_props.len() == 1 {
3233 return Some((*significant_props[0]).clone());
3234 }
3235 }
3236
3237 if let Some(description) = &details.description {
3239 let desc_lower = description.to_lowercase();
3240 if desc_lower.contains("text") && desc_lower.len() < 100 {
3241 return Some("text".to_string());
3242 }
3243 if desc_lower.contains("image") {
3244 return Some("image".to_string());
3245 }
3246 if desc_lower.contains("document") {
3247 return Some("document".to_string());
3248 }
3249 if desc_lower.contains("tool") && desc_lower.contains("result") {
3250 return Some("tool_result".to_string());
3251 }
3252 }
3253
3254 None
3255 }
3256
3257 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3258 if discriminator.is_empty() {
3260 return "Variant".to_string();
3261 }
3262
3263 let mut result = String::new();
3264 let mut next_upper = true;
3265
3266 for c in discriminator.chars() {
3267 match c {
3268 'a'..='z' => {
3269 if next_upper {
3270 result.push(c.to_ascii_uppercase());
3271 next_upper = false;
3272 } else {
3273 result.push(c);
3274 }
3275 }
3276 'A'..='Z' => {
3277 result.push(c);
3278 next_upper = false;
3279 }
3280 '0'..='9' => {
3281 result.push(c);
3282 next_upper = false;
3283 }
3284 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3285 next_upper = true;
3287 }
3288 _ => {
3289 next_upper = true;
3291 }
3292 }
3293 }
3294
3295 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3297 result = format!("Variant{result}");
3298 }
3299
3300 result
3301 }
3302
3303 fn ensure_unique_variant_name(
3304 &self,
3305 base_name: String,
3306 used_names: &mut std::collections::HashSet<String>,
3307 ) -> String {
3308 let mut candidate = base_name.clone();
3309 let mut counter = 1;
3310
3311 while used_names.contains(&candidate) {
3312 counter += 1;
3313 candidate = format!("{base_name}{counter}");
3314 }
3315
3316 used_names.insert(candidate.clone());
3317 candidate
3318 }
3319
3320 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3321 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3323 return meaningful_name;
3324 }
3325
3326 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3328 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3329 }
3330
3331 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3332 let details = schema.details();
3333
3334 if let Some(description) = &details.description {
3336 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3337 return Some(name_from_desc);
3338 }
3339 }
3340
3341 if let Some(properties) = &details.properties {
3343 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3344 return Some(format!("{name_from_props}Block"));
3345 }
3346 }
3347
3348 None
3349 }
3350
3351 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3352 if description.len() > 100 || description.contains('\n') {
3354 return None;
3355 }
3356
3357 let words: Vec<&str> = description
3359 .split_whitespace()
3360 .take(2) .filter(|word| {
3362 let w = word.to_lowercase();
3363 word.len() > 2
3364 && ![
3365 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3366 ]
3367 .contains(&w.as_str())
3368 })
3369 .collect();
3370
3371 if words.is_empty() {
3372 return None;
3373 }
3374
3375 let combined = words.join("_");
3377 let pascal_name = self.discriminator_to_variant_name(&combined);
3378
3379 if !pascal_name.ends_with("Content")
3381 && !pascal_name.ends_with("Block")
3382 && !pascal_name.ends_with("Type")
3383 {
3384 Some(format!("{pascal_name}Content"))
3385 } else {
3386 Some(pascal_name)
3387 }
3388 }
3389
3390 fn extract_type_name_from_properties(
3391 &self,
3392 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3393 ) -> Option<String> {
3394 let significant_props: Vec<&String> = properties
3396 .keys()
3397 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3398 .collect();
3399
3400 if significant_props.is_empty() {
3401 return None;
3402 }
3403
3404 if significant_props.len() == 1 {
3406 let prop_name = significant_props[0];
3407 return Some(self.discriminator_to_variant_name(prop_name));
3408 }
3409
3410 let mut sorted_props = significant_props.clone();
3413 sorted_props.sort();
3414 if let Some(first_prop) = sorted_props.first() {
3415 return Some(self.discriminator_to_variant_name(first_prop));
3416 }
3417
3418 None
3419 }
3420
3421 fn openapi_type_to_rust_type(
3422 &self,
3423 openapi_type: OpenApiSchemaType,
3424 details: &crate::openapi::SchemaDetails,
3425 ) -> String {
3426 self.type_mapper.map(openapi_type, details).rust_type
3431 }
3432
3433 #[allow(dead_code)]
3434 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3435 self.fallback_discriminator_value_for_field(schema_name, "type")
3436 }
3437
3438 fn fallback_discriminator_value_for_field(
3439 &self,
3440 schema_name: &str,
3441 field_name: &str,
3442 ) -> String {
3443 if let Some(ref_schema) = self.schemas.get(schema_name) {
3445 if let Some(extracted) =
3446 self.extract_discriminator_value_for_field(ref_schema, field_name)
3447 {
3448 return extracted;
3449 }
3450 }
3451
3452 self.generate_discriminator_value_from_name(schema_name)
3454 }
3455
3456 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3457 let mut result = String::new();
3459 let mut chars = schema_name.chars().peekable();
3460 let mut first = true;
3461
3462 while let Some(c) = chars.next() {
3463 if c.is_uppercase()
3464 && !first
3465 && chars
3466 .peek()
3467 .map(|&next| next.is_lowercase())
3468 .unwrap_or(false)
3469 {
3470 result.push('.');
3471 }
3472 result.push(c.to_ascii_lowercase());
3473 first = false;
3474 }
3475
3476 if result.ends_with("event") {
3478 result = result[..result.len() - 5].to_string();
3479 }
3480
3481 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3483 result = format!("response.{}", result.trim_start_matches("response"));
3484 }
3485
3486 result
3487 }
3488
3489 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3490 let mut name = schema_name;
3492
3493 if name.starts_with("Response") && name.len() > 8 {
3495 name = &name[8..]; }
3497
3498 if name.ends_with("Event") && name.len() > 5 {
3500 name = &name[..name.len() - 5]; }
3502
3503 name = name.trim_matches('_');
3505
3506 if name.is_empty() {
3508 schema_name.to_string()
3509 } else {
3510 self.discriminator_to_variant_name(name)
3512 }
3513 }
3514
3515 fn hoist_inline_string_enum(
3539 &mut self,
3540 schema: &Schema,
3541 enum_values: Vec<String>,
3542 primary_name: String,
3543 dependencies: &mut HashSet<String>,
3544 ) -> SchemaType {
3545 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3546 matches!(
3547 &existing.schema_type,
3548 SchemaType::StringEnum { values: existing_values }
3549 if existing_values == values
3550 )
3551 }
3552
3553 let mut enum_type_name = primary_name.clone();
3554 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3555 None => true,
3556 Some(existing) if matches_values(existing, &enum_values) => false,
3557 Some(_) => {
3558 let suffix = enum_values
3561 .first()
3562 .map(|v| self.to_pascal_case(v))
3563 .unwrap_or_else(|| "Variant".to_string());
3564 let candidate = format!("{primary_name}{suffix}");
3565
3566 let resolved = match self.resolved_cache.get(&candidate) {
3567 None => Some((candidate.clone(), true)),
3568 Some(existing) if matches_values(existing, &enum_values) => {
3569 Some((candidate.clone(), false))
3570 }
3571 Some(_) => {
3572 let mut found = None;
3575 for n in 2..1000 {
3576 let numbered = format!("{candidate}_{n}");
3577 match self.resolved_cache.get(&numbered) {
3578 None => {
3579 found = Some((numbered, true));
3580 break;
3581 }
3582 Some(existing) if matches_values(existing, &enum_values) => {
3583 found = Some((numbered, false));
3584 break;
3585 }
3586 Some(_) => continue,
3587 }
3588 }
3589 found
3590 }
3591 };
3592
3593 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3594 enum_type_name = resolved_name;
3595 insert
3596 }
3597 };
3598
3599 if should_insert {
3602 self.resolved_cache.insert(
3603 enum_type_name.clone(),
3604 AnalyzedSchema {
3605 name: enum_type_name.clone(),
3606 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3607 schema_type: SchemaType::StringEnum {
3608 values: enum_values,
3609 },
3610 dependencies: HashSet::new(),
3611 nullable: false,
3612 description: schema.details().description.clone(),
3613 default: schema.details().default.clone(),
3614 },
3615 );
3616 }
3617
3618 dependencies.insert(enum_type_name.clone());
3620 SchemaType::Reference {
3621 target: enum_type_name,
3622 }
3623 }
3624
3625 fn analyze_array_schema(
3626 &mut self,
3627 schema: &Schema,
3628 parent_schema_name: &str,
3629 dependencies: &mut HashSet<String>,
3630 ) -> Result<SchemaType> {
3631 let details = schema.details();
3632
3633 if let Some(items_schema) = &details.items {
3635 let item_type = match items_schema.as_ref() {
3637 Schema::Reference { reference, .. } => {
3638 let target = self
3640 .extract_schema_name(reference)
3641 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3642 .to_string();
3643 dependencies.insert(target.clone());
3644 SchemaType::Reference { target }
3645 }
3646 Schema::RecursiveRef { recursive_ref, .. } => {
3647 if recursive_ref == "#" {
3649 let target = self
3651 .find_recursive_anchor_schema()
3652 .unwrap_or_else(|| parent_schema_name.to_string());
3653 dependencies.insert(target.clone());
3654 SchemaType::Reference { target }
3655 } else {
3656 let target = self
3657 .extract_schema_name(recursive_ref)
3658 .unwrap_or("RecursiveType")
3659 .to_string();
3660 dependencies.insert(target.clone());
3661 SchemaType::Reference { target }
3662 }
3663 }
3664 Schema::Typed { schema_type, .. } => {
3665 match schema_type {
3667 OpenApiSchemaType::String => {
3668 match items_schema
3672 .details()
3673 .string_enum_values()
3674 .filter(|values| !values.is_empty())
3675 {
3676 Some(values) => self.hoist_inline_string_enum(
3677 items_schema,
3678 values,
3679 format!("{parent_schema_name}Item"),
3680 dependencies,
3681 ),
3682 None => SchemaType::Primitive {
3683 rust_type: "String".to_string(),
3684 serde_with: None,
3685 },
3686 }
3687 }
3688 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3689 let details = items_schema.details();
3690 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3691 SchemaType::Primitive {
3692 rust_type,
3693 serde_with: None,
3694 }
3695 }
3696 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3697 rust_type: "bool".to_string(),
3698 serde_with: None,
3699 },
3700 OpenApiSchemaType::Object => {
3701 let object_type_name = format!("{parent_schema_name}Item");
3703
3704 let object_type =
3706 self.analyze_object_schema(items_schema, dependencies)?;
3707
3708 let inline_schema = AnalyzedSchema {
3710 name: object_type_name.clone(),
3711 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3712 schema_type: object_type,
3713 dependencies: dependencies.clone(),
3714 nullable: false,
3715 description: items_schema.details().description.clone(),
3716 default: None,
3717 };
3718
3719 self.resolved_cache
3721 .insert(object_type_name.clone(), inline_schema);
3722 dependencies.insert(object_type_name.clone());
3723
3724 SchemaType::Reference {
3726 target: object_type_name,
3727 }
3728 }
3729 OpenApiSchemaType::Array => {
3730 self.analyze_array_schema(
3732 items_schema,
3733 parent_schema_name,
3734 dependencies,
3735 )?
3736 }
3737 _ => SchemaType::Primitive {
3738 rust_type: "serde_json::Value".to_string(),
3739 serde_with: None,
3740 },
3741 }
3742 }
3743 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3744 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3746
3747 match &analyzed.schema_type {
3749 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3750 let union_name = format!("{parent_schema_name}ItemUnion");
3753
3754 let mut union_schema = analyzed;
3756 union_schema.name = union_name.clone();
3757
3758 self.resolved_cache.insert(union_name.clone(), union_schema);
3760
3761 dependencies.insert(union_name.clone());
3763
3764 SchemaType::Reference { target: union_name }
3766 }
3767 _ => analyzed.schema_type,
3768 }
3769 }
3770 Schema::Untyped { .. } => {
3771 if let Some(inferred) = items_schema.inferred_type() {
3773 match inferred {
3774 OpenApiSchemaType::Object => {
3775 let object_type_name = format!("{parent_schema_name}Item");
3777
3778 let object_type =
3780 self.analyze_object_schema(items_schema, dependencies)?;
3781
3782 let inline_schema = AnalyzedSchema {
3784 name: object_type_name.clone(),
3785 original: serde_json::to_value(items_schema)
3786 .unwrap_or(Value::Null),
3787 schema_type: object_type,
3788 dependencies: dependencies.clone(),
3789 nullable: false,
3790 description: items_schema.details().description.clone(),
3791 default: None,
3792 };
3793
3794 self.resolved_cache
3796 .insert(object_type_name.clone(), inline_schema);
3797 dependencies.insert(object_type_name.clone());
3798
3799 SchemaType::Reference {
3801 target: object_type_name,
3802 }
3803 }
3804 OpenApiSchemaType::String => {
3805 match items_schema
3808 .details()
3809 .string_enum_values()
3810 .filter(|values| !values.is_empty())
3811 {
3812 Some(values) => self.hoist_inline_string_enum(
3813 items_schema,
3814 values,
3815 format!("{parent_schema_name}Item"),
3816 dependencies,
3817 ),
3818 None => SchemaType::Primitive {
3819 rust_type: "String".to_string(),
3820 serde_with: None,
3821 },
3822 }
3823 }
3824 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3825 let details = items_schema.details();
3826 let rust_type = self.get_number_rust_type(inferred, details);
3827 SchemaType::Primitive {
3828 rust_type,
3829 serde_with: None,
3830 }
3831 }
3832 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3833 rust_type: "bool".to_string(),
3834 serde_with: None,
3835 },
3836 _ => SchemaType::Primitive {
3837 rust_type: "serde_json::Value".to_string(),
3838 serde_with: None,
3839 },
3840 }
3841 } else {
3842 SchemaType::Primitive {
3843 rust_type: "serde_json::Value".to_string(),
3844 serde_with: None,
3845 }
3846 }
3847 }
3848 _ => SchemaType::Primitive {
3849 rust_type: "serde_json::Value".to_string(),
3850 serde_with: None,
3851 },
3852 };
3853
3854 Ok(SchemaType::Array {
3855 item_type: Box::new(item_type),
3856 })
3857 } else {
3858 Ok(SchemaType::Primitive {
3860 rust_type: "Vec<serde_json::Value>".to_string(),
3861 serde_with: None,
3862 })
3863 }
3864 }
3865
3866 fn get_number_rust_type(
3867 &self,
3868 schema_type: OpenApiSchemaType,
3869 details: &crate::openapi::SchemaDetails,
3870 ) -> String {
3871 let format = details.format.as_deref();
3875 match schema_type {
3876 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
3877 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
3878 _ => self.type_mapper.dynamic_json().rust_type,
3879 }
3880 }
3881
3882 fn analyze_anyof_union(
3883 &mut self,
3884 any_of_schemas: &[Schema],
3885 discriminator: Option<&Discriminator>,
3886 dependencies: &mut HashSet<String>,
3887 context_name: &str,
3888 ) -> Result<SchemaType> {
3889 let filtered_owned: Vec<Schema>;
3894 let any_of_schemas: &[Schema] = if any_of_schemas
3895 .iter()
3896 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3897 {
3898 filtered_owned = any_of_schemas
3899 .iter()
3900 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3901 .cloned()
3902 .collect();
3903 if filtered_owned.is_empty() {
3904 return Ok(SchemaType::Primitive {
3905 rust_type: "serde_json::Value".to_string(),
3906 serde_with: None,
3907 });
3908 }
3909 if filtered_owned.len() == 1 {
3910 return self
3911 .analyze_schema_value(&filtered_owned[0], context_name)
3912 .map(|a| a.schema_type);
3913 }
3914 &filtered_owned
3915 } else {
3916 any_of_schemas
3917 };
3918
3919 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3921 let has_objects = any_of_schemas.iter().any(|s| {
3922 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3923 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3924 });
3925 let has_arrays = any_of_schemas
3926 .iter()
3927 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3928
3929 let all_string_like = any_of_schemas.iter().all(|s| {
3932 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3933 || s.details().const_value.is_some()
3934 });
3935
3936 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3937 if let Some(disc) = discriminator {
3939 return self.analyze_oneof_union(
3941 any_of_schemas,
3942 Some(disc),
3943 context_name,
3944 dependencies,
3945 );
3946 }
3947
3948 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3950 return self.analyze_oneof_union(
3951 any_of_schemas,
3952 Some(&Discriminator {
3953 property_name: disc_field,
3954 mapping: None,
3955 default_mapping: None,
3956 extensions: crate::extensions::Extensions::default(),
3957 }),
3958 context_name,
3959 dependencies,
3960 );
3961 }
3962
3963 let mut variants = Vec::new();
3965
3966 for schema in any_of_schemas {
3967 if let Some(ref_str) = schema.reference() {
3968 if let Some(target) = self.extract_schema_name(ref_str) {
3969 dependencies.insert(target.to_string());
3970 variants.push(SchemaRef {
3971 target: target.to_string(),
3972 nullable: false,
3973 });
3974 }
3975 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3976 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3977 {
3978 let inline_index = variants.len();
3980 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3981
3982 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3984
3985 variants.push(SchemaRef {
3986 target: inline_type_name,
3987 nullable: false,
3988 });
3989 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3990 let array_type =
3992 self.analyze_array_schema(schema, context_name, dependencies)?;
3993
3994 let array_type_name = if let Some(items_schema) = &schema.details().items {
3996 if let Some(ref_str) = items_schema.reference() {
3997 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3998 dependencies.insert(item_type_name.to_string());
3999 format!("{item_type_name}Array")
4000 } else {
4001 self.generate_context_aware_name(
4002 context_name,
4003 "Array",
4004 variants.len(),
4005 Some(schema),
4006 )
4007 }
4008 } else {
4009 self.generate_context_aware_name(
4010 context_name,
4011 "Array",
4012 variants.len(),
4013 Some(schema),
4014 )
4015 }
4016 } else {
4017 self.generate_context_aware_name(
4018 context_name,
4019 "Array",
4020 variants.len(),
4021 Some(schema),
4022 )
4023 };
4024
4025 self.resolved_cache.insert(
4027 array_type_name.clone(),
4028 AnalyzedSchema {
4029 name: array_type_name.clone(),
4030 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4031 schema_type: array_type,
4032 dependencies: HashSet::new(),
4033 nullable: false,
4034 description: Some("Array variant in union".to_string()),
4035 default: None,
4036 },
4037 );
4038
4039 dependencies.insert(array_type_name.clone());
4041
4042 variants.push(SchemaRef {
4043 target: array_type_name,
4044 nullable: false,
4045 });
4046 } else if let Some(schema_type) = schema.schema_type() {
4047 let primitive_unions = self
4057 .type_mapper
4058 .config_shape_primitive_unions()
4059 .unwrap_or(true);
4060
4061 if primitive_unions {
4062 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
4063 variants.push(SchemaRef {
4064 target: mapped.rust_type,
4065 nullable: false,
4066 });
4067 } else {
4068 let inline_index = variants.len();
4069 let inline_type_name = match schema_type {
4070 OpenApiSchemaType::String => {
4071 if inline_index == 0 {
4072 format!("{context_name}String")
4073 } else {
4074 format!("{context_name}StringVariant{inline_index}")
4075 }
4076 }
4077 OpenApiSchemaType::Number => {
4078 if inline_index == 0 {
4079 format!("{context_name}Number")
4080 } else {
4081 format!("{context_name}NumberVariant{inline_index}")
4082 }
4083 }
4084 OpenApiSchemaType::Integer => {
4085 if inline_index == 0 {
4086 format!("{context_name}Integer")
4087 } else {
4088 format!("{context_name}IntegerVariant{inline_index}")
4089 }
4090 }
4091 OpenApiSchemaType::Boolean => {
4092 if inline_index == 0 {
4093 format!("{context_name}Boolean")
4094 } else {
4095 format!("{context_name}BooleanVariant{inline_index}")
4096 }
4097 }
4098 _ => format!("{context_name}Variant{inline_index}"),
4099 };
4100
4101 let rust_type =
4102 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4103
4104 self.resolved_cache.insert(
4105 inline_type_name.clone(),
4106 AnalyzedSchema {
4107 name: inline_type_name.clone(),
4108 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4109 schema_type: SchemaType::Primitive {
4110 rust_type,
4111 serde_with: None,
4112 },
4113 dependencies: HashSet::new(),
4114 nullable: false,
4115 description: schema.details().description.clone(),
4116 default: None,
4117 },
4118 );
4119
4120 dependencies.insert(inline_type_name.clone());
4121
4122 variants.push(SchemaRef {
4123 target: inline_type_name,
4124 nullable: false,
4125 });
4126 }
4127 }
4128 }
4129
4130 if !variants.is_empty() {
4131 return Ok(SchemaType::Union { variants });
4132 }
4133 }
4134
4135 let all_strings = any_of_schemas.iter().all(|schema| {
4137 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4138 || schema.details().const_value.is_some()
4139 });
4140
4141 if all_strings {
4142 let mut enum_values = Vec::new();
4144 let mut has_open_string = false;
4145
4146 for schema in any_of_schemas {
4147 if let Some(const_val) = &schema.details().const_value {
4148 if let Some(const_str) = const_val.as_str() {
4149 enum_values.push(const_str.to_string());
4150 }
4151 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4152 has_open_string = true;
4153 }
4154 }
4155
4156 if !enum_values.is_empty() {
4157 if has_open_string {
4158 return Ok(SchemaType::ExtensibleEnum {
4161 known_values: enum_values,
4162 });
4163 } else {
4164 return Ok(SchemaType::StringEnum {
4166 values: enum_values,
4167 });
4168 }
4169 }
4170 }
4171
4172 Ok(SchemaType::Primitive {
4174 rust_type: "serde_json::Value".to_string(),
4175 serde_with: None,
4176 })
4177 }
4178
4179 fn find_recursive_anchor_schema(&self) -> Option<String> {
4181 for (schema_name, schema) in &self.schemas {
4183 let details = schema.details();
4184 if details.recursive_anchor == Some(true) {
4185 return Some(schema_name.clone());
4186 }
4187 }
4188
4189 None
4193 }
4194
4195 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4198 if let Schema::AnyOf { any_of, .. } = schema {
4200 if any_of.len() == 2 {
4201 let has_null = any_of
4202 .iter()
4203 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4204 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4205
4206 if has_null && has_empty_object {
4207 return true;
4208 }
4209 }
4210 }
4211
4212 self.is_dynamic_object_pattern(schema)
4214 }
4215
4216 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4218 let is_object = match schema.schema_type() {
4220 Some(OpenApiSchemaType::Object) => true,
4221 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4222 _ => false,
4223 };
4224
4225 if !is_object {
4226 return false;
4227 }
4228
4229 let details = schema.details();
4230
4231 if self.has_explicit_additional_properties(schema) {
4234 return false;
4235 }
4236
4237 let no_properties = details
4239 .properties
4240 .as_ref()
4241 .map(|props| props.is_empty())
4242 .unwrap_or(true);
4243
4244 if no_properties {
4245 let has_structural_constraints = details
4248 .required
4249 .as_ref()
4250 .map(|req| req.iter().any(|r| r != "type"))
4251 .unwrap_or(false)
4252 || details.pattern_properties.is_some()
4253 || details.property_names.is_some()
4254 || details.min_properties.is_some()
4255 || details.max_properties.is_some()
4256 || details.dependent_required.is_some()
4257 || details.dependent_schemas.is_some()
4258 || details.if_schema.is_some()
4259 || details.then_schema.is_some()
4260 || details.else_schema.is_some();
4261
4262 return !has_structural_constraints;
4263 }
4264
4265 false
4266 }
4267
4268 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4270 let details = schema.details();
4271
4272 matches!(
4274 &details.additional_properties,
4275 Some(crate::openapi::AdditionalProperties::Boolean(true))
4276 | Some(crate::openapi::AdditionalProperties::Schema(_))
4277 )
4278 }
4279
4280 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4282 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4283 .map_err(GeneratorError::ParseError)?;
4284 let mut canonical_operation_ids = HashSet::new();
4289
4290 if let Some(paths) = &spec.paths {
4291 for (path, path_item) in paths {
4292 let resolved = self.resolve_path_item(path_item, &spec)?;
4294 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4295 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4296 }
4297 }
4298 if let Some(webhooks) = &spec.webhooks {
4305 for (name, path_item) in webhooks {
4306 let synthetic_path = format!("__webhook__/{name}");
4307 self.ingest_path_item_operations(
4308 &synthetic_path,
4309 path_item,
4310 analysis,
4311 &mut canonical_operation_ids,
4312 )?;
4313 }
4314 }
4315 Ok(())
4316 }
4317
4318 fn resolve_path_item(
4322 &self,
4323 path_item: &crate::openapi::PathItem,
4324 spec: &crate::openapi::OpenApiSpec,
4325 ) -> Result<Option<crate::openapi::PathItem>> {
4326 let Some(reference) = &path_item.reference else {
4327 return Ok(None);
4328 };
4329 let target_name = reference
4330 .strip_prefix("#/components/pathItems/")
4331 .ok_or_else(|| {
4332 GeneratorError::UnresolvedReference(format!(
4333 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4334 ))
4335 })?;
4336 let pi = spec
4337 .components
4338 .as_ref()
4339 .and_then(|c| c.path_items.as_ref())
4340 .and_then(|map| map.get(target_name))
4341 .ok_or_else(|| {
4342 GeneratorError::UnresolvedReference(format!(
4343 "Path Item ref {reference} not found in components/pathItems"
4344 ))
4345 })?;
4346 Ok(Some(pi.clone()))
4347 }
4348
4349 fn ingest_path_item_operations(
4350 &mut self,
4351 path: &str,
4352 path_item: &crate::openapi::PathItem,
4353 analysis: &mut SchemaAnalysis,
4354 canonical_operation_ids: &mut HashSet<String>,
4355 ) -> Result<()> {
4356 for (method, operation) in path_item.operations() {
4357 let raw_operation_id = operation
4359 .operation_id
4360 .clone()
4361 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4362
4363 let operation_id = if canonical_operation_ids
4374 .contains(&Self::canonical_operation_id(&raw_operation_id))
4375 {
4376 let method_lower = method.to_lowercase();
4377 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4378 let mut suffix = 2;
4379 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4380 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4381 suffix += 1;
4382 }
4383 eprintln!(
4384 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4385 raw_operation_id, method, path, candidate
4386 );
4387 candidate
4388 } else {
4389 raw_operation_id.clone()
4390 };
4391
4392 let (op_info, responses) = self.analyze_single_operation(
4393 &operation_id,
4394 method,
4395 path,
4396 operation,
4397 path_item.parameters.as_ref(),
4398 analysis,
4399 )?;
4400 analysis
4401 .operation_id_aliases
4402 .entry(raw_operation_id)
4403 .or_default()
4404 .push(operation_id.clone());
4405 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4406 analysis
4407 .operation_responses
4408 .insert(operation_id.clone(), responses);
4409 analysis.operations.insert(operation_id, op_info);
4410 }
4411 Ok(())
4412 }
4413
4414 fn canonical_operation_id(operation_id: &str) -> String {
4415 use heck::ToPascalCase;
4416 operation_id.replace('.', "_").to_pascal_case()
4417 }
4418
4419 fn generate_operation_id(method: &str, path: &str) -> String {
4422 let mut operation_id = method.to_lowercase();
4424
4425 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4427
4428 for part in path_parts {
4429 if part.is_empty() {
4430 continue;
4431 }
4432
4433 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4435 &part[1..part.len() - 1]
4436 } else {
4437 part
4438 };
4439
4440 let pascal_case_part = cleaned_part
4442 .split(&['-', '_'][..])
4443 .map(|s| {
4444 let mut chars = s.chars();
4445 match chars.next() {
4446 None => String::new(),
4447 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4448 }
4449 })
4450 .collect::<String>();
4451
4452 operation_id.push_str(&pascal_case_part);
4453 }
4454
4455 operation_id
4456 }
4457
4458 fn analyze_single_operation(
4460 &mut self,
4461 operation_id: &str,
4462 method: &str,
4463 path: &str,
4464 operation: &crate::openapi::Operation,
4465 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4466 _analysis: &mut SchemaAnalysis,
4467 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
4468 let raw_path_item = self
4469 .openapi_spec
4470 .get("paths")
4471 .and_then(|paths| paths.get(path))
4472 .cloned();
4473 let raw_operation = raw_path_item
4474 .as_ref()
4475 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
4476 .cloned();
4477 let mut op_info = OperationInfo {
4478 operation_id: operation_id.to_string(),
4479 method: method.to_uppercase(),
4480 path: path.to_string(),
4481 summary: operation.summary.clone(),
4482 description: operation.description.clone(),
4483 request_body: None,
4484 request_body_required: operation
4486 .request_body
4487 .as_ref()
4488 .and_then(|rb| rb.required)
4489 .unwrap_or(false),
4490 response_schemas: BTreeMap::new(),
4491 parameters: Vec::new(),
4492 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4495 };
4496 let mut operation_responses = BTreeMap::new();
4497
4498 if let Some(request_body) = &operation.request_body {
4500 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
4501 if let Some((content_type, maybe_schema)) = request_body.best_content() {
4502 op_info.request_body = if is_json_media_type(content_type) {
4503 match maybe_schema {
4504 Some(s) => {
4505 let validation_schema = self
4506 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4507 .unwrap_or(
4508 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4509 );
4510 Some(
4511 self.resolve_or_inline_schema(s, operation_id, "Request")
4512 .map(|name| RequestBodyContent::Json {
4513 schema_name: name,
4514 media_type: content_type.to_string(),
4515 validation_schema,
4516 })?,
4517 )
4518 }
4519 None => Some(RequestBodyContent::SchemaLess {
4520 media_type: content_type.to_string(),
4521 }),
4522 }
4523 } else if is_form_urlencoded_media_type(content_type) {
4524 match maybe_schema {
4525 Some(s) => {
4526 let validation_schema = self
4527 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4528 .unwrap_or(
4529 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4530 );
4531 Some(
4532 self.resolve_or_inline_schema(s, operation_id, "Request")
4533 .map(|name| RequestBodyContent::FormUrlEncoded {
4534 schema_name: name,
4535 media_type: content_type.to_string(),
4536 validation_schema,
4537 })?,
4538 )
4539 }
4540 None => Some(RequestBodyContent::SchemaLess {
4541 media_type: content_type.to_string(),
4542 }),
4543 }
4544 } else {
4545 match content_type {
4546 "multipart/form-data" => Some(RequestBodyContent::Multipart),
4547 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
4548 "text/plain" => Some(RequestBodyContent::TextPlain),
4549 _ => None,
4550 }
4551 };
4552 }
4553 if op_info.request_body.is_none() {
4554 let mut media_types = request_body
4555 .content
4556 .as_ref()
4557 .map(|content| content.keys().cloned().collect::<Vec<_>>())
4558 .unwrap_or_default();
4559 media_types.sort();
4560 if !media_types.is_empty() {
4561 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
4562 }
4563 }
4564 }
4565
4566 if let Some(responses) = &operation.responses {
4568 for (status_code, response) in responses {
4569 let response = self.resolve_response(response)?;
4570 let supports_streaming = response.content.as_ref().is_some_and(|content| {
4576 content
4577 .keys()
4578 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
4579 });
4580 if supports_streaming {
4581 op_info.supports_streaming = true;
4582 }
4583
4584 let mut response_info = OperationResponse {
4585 supports_streaming,
4586 has_content: response
4587 .content
4588 .as_ref()
4589 .is_some_and(|content| !content.is_empty()),
4590 ..Default::default()
4591 };
4592 if let Some((media_type, schema)) = response.json_content() {
4593 if let Some(schema_ref) = schema.reference() {
4594 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4596 op_info
4597 .response_schemas
4598 .insert(status_code.clone(), schema_name.to_string());
4599 response_info.schema_name = Some(schema_name.to_string());
4600 response_info.media_type = Some(media_type.to_string());
4601 }
4602 } else {
4603 let synthetic_name =
4605 self.generate_inline_response_type_name(operation_id, status_code);
4606
4607 let mut deps = HashSet::new();
4609 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4610
4611 op_info
4612 .response_schemas
4613 .insert(status_code.clone(), synthetic_name.clone());
4614 response_info.schema_name = Some(synthetic_name);
4615 response_info.media_type = Some(media_type.to_string());
4616 }
4617 }
4618 response_info.unsupported_media_types = response
4619 .content
4620 .as_ref()
4621 .into_iter()
4622 .flat_map(|content| content.iter())
4623 .filter(|(media_type, content)| {
4624 !crate::openapi::is_event_stream_media_type(media_type)
4625 && (!crate::openapi::is_json_media_type(media_type)
4626 || content.schema.is_none())
4627 })
4628 .map(|(media_type, _)| media_type.clone())
4629 .collect();
4630 operation_responses.insert(status_code.clone(), response_info);
4631 }
4632 }
4633
4634 if op_info.supports_streaming
4637 && let Some(parameters) = &operation.parameters
4638 {
4639 for param in parameters {
4640 if let Some(name) = param.name.as_deref() {
4641 if name.eq_ignore_ascii_case("stream") {
4642 op_info.stream_parameter = Some(name.to_string());
4643 break;
4644 }
4645 }
4646 }
4647 }
4648
4649 if let Some(parameters) = &operation.parameters {
4651 for (index, param) in parameters.iter().enumerate() {
4652 let resolved = self.resolve_parameter(param).into_owned();
4656 let validation_schema = raw_operation
4657 .as_ref()
4658 .and_then(|operation| operation.get("parameters"))
4659 .and_then(Value::as_array)
4660 .and_then(|parameters| parameters.get(index))
4661 .and_then(|parameter| self.raw_parameter_schema(parameter));
4662 if let Some(param_info) =
4663 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4664 {
4665 op_info.parameters.push(param_info);
4666 }
4667 }
4668 }
4669
4670 if let Some(path_params) = path_item_parameters {
4672 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4673 .parameters
4674 .iter()
4675 .map(|p| (p.name.clone(), p.location.clone()))
4676 .collect();
4677 for (index, param) in path_params.iter().enumerate() {
4678 let resolved = self.resolve_parameter(param).into_owned();
4679 let validation_schema = raw_path_item
4680 .as_ref()
4681 .and_then(|path_item| path_item.get("parameters"))
4682 .and_then(Value::as_array)
4683 .and_then(|parameters| parameters.get(index))
4684 .and_then(|parameter| self.raw_parameter_schema(parameter));
4685 if let Some(param_info) =
4686 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4687 {
4688 if !existing_keys
4689 .contains(&(param_info.name.clone(), param_info.location.clone()))
4690 {
4691 op_info.parameters.push(param_info);
4692 }
4693 }
4694 }
4695 }
4696
4697 let mut declared_path_names: std::collections::HashSet<String> = op_info
4705 .parameters
4706 .iter()
4707 .filter(|p| p.location == "path")
4708 .map(|p| p.name.clone())
4709 .collect();
4710 let bytes = path.as_bytes().iter();
4711 let mut current = String::new();
4712 let mut in_brace = false;
4713 let mut synthesized: Vec<String> = Vec::new();
4714 for b in bytes {
4715 match *b {
4716 b'{' => {
4717 in_brace = true;
4718 current.clear();
4719 }
4720 b'}' if in_brace => {
4721 in_brace = false;
4722 if !current.is_empty() && !declared_path_names.contains(¤t) {
4723 synthesized.push(current.clone());
4724 declared_path_names.insert(current.clone());
4725 }
4726 }
4727 _ if in_brace => current.push(*b as char),
4728 _ => {}
4729 }
4730 }
4731 for name in synthesized {
4732 eprintln!(
4733 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
4734 path, name
4735 );
4736 op_info.parameters.push(ParameterInfo {
4737 name,
4738 location: "path".to_string(),
4739 required: true,
4740 schema_ref: None,
4741 rust_type: "String".to_string(),
4742 description: None,
4743 enum_values: None,
4744 enum_varnames: None,
4745 rust_ident: None,
4746 query_serialization: None,
4747 validation_schema: None,
4748 });
4749 }
4750
4751 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
4759 for p in op_info.parameters.iter_mut() {
4760 let raw = base_param_ident(&p.name);
4761 let mut chosen = raw.clone();
4762 let mut suffix = 2;
4763 while !used.insert(chosen.clone()) {
4764 chosen = format!("{raw}_{suffix}");
4765 suffix += 1;
4766 }
4767 p.rust_ident = Some(chosen);
4768 }
4769
4770 Ok((op_info, operation_responses))
4771 }
4772
4773 fn resolve_response(
4780 &self,
4781 response: &crate::openapi::Response,
4782 ) -> Result<crate::openapi::Response> {
4783 let mut current = response.clone();
4784 let mut visited = HashSet::new();
4785 while let Some(reference) = current.reference.clone() {
4786 if !visited.insert(reference.clone()) {
4787 return Err(GeneratorError::CircularDependency(format!(
4788 "response reference {reference}"
4789 )));
4790 }
4791
4792 let pointer = reference.strip_prefix('#').ok_or_else(|| {
4793 GeneratorError::UnresolvedReference(format!(
4794 "external response reference `{reference}` is not supported"
4795 ))
4796 })?;
4797 if !pointer.is_empty() && !pointer.starts_with('/') {
4798 return Err(GeneratorError::UnresolvedReference(format!(
4799 "response reference `{reference}` is not a local JSON Pointer"
4800 )));
4801 }
4802 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
4803 GeneratorError::UnresolvedReference(format!(
4804 "response reference `{reference}` does not exist"
4805 ))
4806 })?;
4807 let object = value.as_object().ok_or_else(|| {
4808 GeneratorError::InvalidSchema(format!(
4809 "response reference `{reference}` must target an object"
4810 ))
4811 })?;
4812 if !["$ref", "description", "headers", "content", "links"]
4813 .iter()
4814 .any(|field| object.contains_key(*field))
4815 {
4816 return Err(GeneratorError::InvalidSchema(format!(
4817 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
4818 )));
4819 }
4820 current = serde_json::from_value(value.clone()).map_err(|error| {
4821 GeneratorError::InvalidSchema(format!(
4822 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
4823 ))
4824 })?;
4825 }
4826 Ok(current)
4827 }
4828
4829 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
4836 use heck::ToPascalCase;
4837 let base_name = operation_id.replace('.', "_").to_pascal_case();
4838 let suffix = Self::status_code_suffix(status_code);
4839 format!("{}Response{}", base_name, suffix)
4840 }
4841
4842 fn status_code_suffix(status_code: &str) -> String {
4849 match status_code {
4850 "" | "200" => String::new(),
4851 "default" | "Default" => "Default".to_string(),
4852 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
4853 other => other.to_ascii_lowercase(),
4854 }
4855 }
4856
4857 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
4859 use heck::ToPascalCase;
4860 let base_name = operation_id.replace('.', "_").to_pascal_case();
4864 format!("{}Request", base_name)
4865 }
4866
4867 fn resolve_or_inline_schema(
4870 &mut self,
4871 schema: &crate::openapi::Schema,
4872 operation_id: &str,
4873 suffix: &str,
4874 ) -> Result<String> {
4875 if let Some(schema_ref) = schema.reference()
4876 && let Some(schema_name) = self.extract_schema_name(schema_ref)
4877 {
4878 return Ok(schema_name.to_string());
4879 }
4880 let synthetic_name = if suffix == "Request" {
4882 self.generate_inline_request_type_name(operation_id)
4883 } else {
4884 self.generate_inline_response_type_name(operation_id, "")
4885 };
4886 let mut deps = HashSet::new();
4887 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4888 Ok(synthetic_name)
4889 }
4890
4891 fn resolve_parameter<'a>(
4894 &'a self,
4895 param: &'a crate::openapi::Parameter,
4896 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
4897 if let Some(ref_str) = param.reference.as_deref() {
4898 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
4899 if let Some(resolved) = self.component_parameters.get(param_name) {
4900 return std::borrow::Cow::Borrowed(resolved);
4901 }
4902 }
4903 }
4904 std::borrow::Cow::Borrowed(param)
4905 }
4906
4907 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
4920 if self.resolve_cached_schema(name).is_some_and(|schema| {
4921 matches!(
4922 schema.schema_type,
4923 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
4924 )
4925 }) {
4926 return true;
4927 }
4928 let Some(schema_value) = self
4929 .openapi_spec
4930 .get("components")
4931 .and_then(|c| c.get("schemas"))
4932 .and_then(|s| s.get(name))
4933 else {
4934 return false;
4935 };
4936 let is_string_type = schema_value
4937 .get("type")
4938 .and_then(|v| v.as_str())
4939 .map(|s| s == "string")
4940 .unwrap_or(false);
4941 let has_enum_or_const =
4942 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
4943 is_string_type && has_enum_or_const
4944 }
4945
4946 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
4947 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
4948 return Some(value.clone());
4949 };
4950 let pointer = reference.strip_prefix('#')?;
4951 self.openapi_spec.pointer(pointer).cloned()
4952 }
4953
4954 fn raw_request_body_schema(
4955 &self,
4956 operation: Option<&Value>,
4957 content_type: &str,
4958 ) -> Option<Value> {
4959 let request_body = operation?.get("requestBody")?;
4960 self.resolve_raw_local_reference(request_body)?
4961 .get("content")?
4962 .get(content_type)?
4963 .get("schema")
4964 .cloned()
4965 }
4966
4967 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
4968 self.resolve_raw_local_reference(parameter)?
4969 .get("schema")
4970 .cloned()
4971 }
4972
4973 fn analyze_parameter(
4974 &mut self,
4975 param: &crate::openapi::Parameter,
4976 operation_id: &str,
4977 raw_validation_schema: Option<Value>,
4978 ) -> Result<Option<ParameterInfo>> {
4979 use heck::ToPascalCase;
4980
4981 let name = param.name.as_deref().unwrap_or("");
4982 let location = param.location.as_deref().unwrap_or("");
4983 let required = param.required.unwrap_or(false);
4984 let validation_schema = match raw_validation_schema {
4985 Some(schema) => Some(schema),
4986 None => param
4987 .schema
4988 .as_ref()
4989 .map(serde_json::to_value)
4990 .transpose()
4991 .map_err(GeneratorError::ParseError)?,
4992 };
4993
4994 let mut rust_type = "String".to_string();
4995 let mut schema_ref = None;
4996 let mut enum_values: Option<Vec<String>> = None;
4997 let mut enum_varnames: Option<Vec<String>> = None;
4998 let mut query_serialization: Option<QuerySerialization> = None;
4999
5000 let is_query = location == "query";
5006 let form_style = matches!(param.style.as_deref(), None | Some("form"));
5007 let form_exploded = form_style && param.explode.unwrap_or(true);
5008 let deep_object =
5009 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
5010
5011 let object_serialization = if !is_query {
5012 None
5013 } else if deep_object {
5014 Some(QuerySerialization::DeepObject)
5015 } else if form_exploded {
5016 Some(QuerySerialization::FormExplodedObject)
5017 } else if form_style {
5018 Some(QuerySerialization::FormObject)
5019 } else {
5020 None
5021 };
5022
5023 if let Some(schema) = ¶m.schema {
5024 if let Some(ref_str) = schema.reference() {
5025 if let Some(name) = self.extract_schema_name(ref_str) {
5031 if self.referenced_schema_is_string_enum(name) {
5032 schema_ref = Some(name.to_string());
5033 } else if object_serialization.is_some()
5034 && self.referenced_schema_is_object(name)
5035 {
5036 schema_ref = Some(name.to_string());
5037 query_serialization = object_serialization.clone();
5038 } else if is_query
5039 && form_style
5040 && let Some(item_type) = self.referenced_array_param_item_type(name)
5041 {
5042 schema_ref = Some(name.to_string());
5048 query_serialization = Some(if form_exploded {
5049 QuerySerialization::FormExplodedArray { item_type }
5050 } else {
5051 QuerySerialization::FormArray { item_type }
5052 });
5053 }
5054 }
5055 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
5056 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5061 let param_pascal = name.to_pascal_case();
5062 let synthetic_name = format!("{op_pascal}{param_pascal}");
5063 let mut deps = HashSet::new();
5064 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5065 schema_ref = Some(synthetic_name);
5066 query_serialization = object_serialization.clone();
5067 } else if is_query
5068 && form_style
5069 && matches!(
5070 schema.schema_type(),
5071 Some(crate::openapi::SchemaType::Array)
5072 )
5073 && let Some(item_type) = self.array_param_item_type(schema)
5074 {
5075 query_serialization = Some(if form_exploded {
5083 QuerySerialization::FormExplodedArray { item_type }
5084 } else {
5085 QuerySerialization::FormArray { item_type }
5086 });
5087 } else if let Some(schema_type) = schema.schema_type() {
5088 let format = schema.details().format.clone();
5094 rust_type = match schema_type {
5095 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5096 crate::openapi::SchemaType::Integer => {
5097 self.type_mapper.integer_format(format.as_deref()).rust_type
5098 }
5099 crate::openapi::SchemaType::Number => {
5100 self.type_mapper.number_format(format.as_deref()).rust_type
5101 }
5102 crate::openapi::SchemaType::String => "String".to_string(),
5103 _ => "String".to_string(),
5104 };
5105
5106 if matches!(schema_type, crate::openapi::SchemaType::String) {
5107 let details = schema.details();
5108 if details.is_string_enum() {
5109 if let Some(values) = details.string_enum_values() {
5110 if !values.is_empty() {
5111 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5112 let param_pascal = name.to_pascal_case();
5113 rust_type = format!("{op_pascal}{param_pascal}");
5114 enum_varnames = details
5119 .extra
5120 .get("x-enum-varnames")
5121 .and_then(Value::as_array)
5122 .map(|raw| {
5123 raw.iter()
5124 .filter_map(Value::as_str)
5125 .map(str::to_owned)
5126 .collect::<Vec<_>>()
5127 })
5128 .filter(|names| names.len() == values.len());
5129 enum_values = Some(values);
5130 }
5131 }
5132 }
5133 }
5134 }
5135
5136 if is_query && query_serialization.is_none() {
5137 let referenced_name = schema
5138 .reference()
5139 .and_then(|reference| self.extract_schema_name(reference));
5140 let is_object = referenced_name
5141 .is_some_and(|name| self.referenced_schema_is_object(name))
5142 || Self::schema_is_inline_object(schema);
5143 let is_array = referenced_name
5144 .is_some_and(|name| self.referenced_schema_is_array(name))
5145 || matches!(
5146 schema.schema_type(),
5147 Some(crate::openapi::SchemaType::Array)
5148 );
5149 let is_composed = referenced_name
5150 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
5151 let reason = if param.style.as_deref() == Some("deepObject")
5152 && param.explode == Some(false)
5153 {
5154 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
5155 } else if param.style.as_deref() == Some("deepObject") && !is_object {
5156 Some("style=deepObject is defined only for object query parameters".to_string())
5157 } else if is_object {
5158 Some(format!(
5159 "object query parameters do not support style={}",
5160 param.style.as_deref().unwrap_or("form")
5161 ))
5162 } else if is_array && form_style {
5163 Some(
5164 "form array query parameters require scalar or string-enum items"
5165 .to_string(),
5166 )
5167 } else if is_array {
5168 Some(format!(
5169 "array query parameters do not yet support style={}",
5170 param.style.as_deref().unwrap_or("form")
5171 ))
5172 } else if is_composed {
5173 Some(
5174 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
5175 .to_string(),
5176 )
5177 } else {
5178 None
5179 };
5180 if let Some(reason) = reason {
5181 query_serialization = Some(QuerySerialization::Unsupported { reason });
5182 }
5183 }
5184 }
5185
5186 Ok(Some(ParameterInfo {
5187 name: name.to_string(),
5188 location: location.to_string(),
5189 required,
5190 schema_ref,
5191 rust_type,
5192 description: param.description.clone(),
5193 enum_values,
5194 enum_varnames,
5195 rust_ident: None,
5196 query_serialization,
5197 validation_schema,
5198 }))
5199 }
5200
5201 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
5210 let items = schema.details().items.as_deref()?;
5211 if let Some(ref_str) = items.reference() {
5212 let name = self.extract_schema_name(ref_str)?;
5213 return self
5214 .referenced_schema_is_string_enum(name)
5215 .then(|| ArrayItemType::EnumRef(name.to_string()));
5216 }
5217 let format = items.details().format.clone();
5218 let scalar = match items.schema_type()? {
5219 crate::openapi::SchemaType::String => "String".to_string(),
5220 crate::openapi::SchemaType::Integer => {
5221 self.type_mapper.integer_format(format.as_deref()).rust_type
5222 }
5223 crate::openapi::SchemaType::Number => {
5224 self.type_mapper.number_format(format.as_deref()).rust_type
5225 }
5226 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5227 _ => return None,
5228 };
5229 Some(ArrayItemType::Scalar(scalar))
5230 }
5231
5232 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
5235 let schema = self.resolve_cached_schema(name)?;
5236 let SchemaType::Array { item_type } = &schema.schema_type else {
5237 return None;
5238 };
5239 self.analyzed_array_item_type(item_type)
5240 }
5241
5242 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
5243 match item_type {
5244 SchemaType::Primitive { rust_type, .. } => {
5245 Some(ArrayItemType::Scalar(rust_type.clone()))
5246 }
5247 SchemaType::Reference { target } => {
5248 let resolved = self.resolve_cached_schema(target)?;
5249 matches!(
5250 resolved.schema_type,
5251 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
5252 )
5253 .then(|| ArrayItemType::EnumRef(target.clone()))
5254 }
5255 _ => None,
5256 }
5257 }
5258
5259 fn referenced_schema_is_object(&self, name: &str) -> bool {
5263 self.resolve_cached_schema(name)
5264 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
5265 }
5266
5267 fn referenced_schema_is_array(&self, name: &str) -> bool {
5268 self.resolve_cached_schema(name)
5269 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
5270 }
5271
5272 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
5273 self.resolve_cached_schema(name).is_some_and(|schema| {
5274 matches!(
5275 schema.schema_type,
5276 SchemaType::Composition { .. }
5277 | SchemaType::Union { .. }
5278 | SchemaType::DiscriminatedUnion { .. }
5279 )
5280 })
5281 }
5282
5283 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
5284 let mut current = name;
5285 let mut visited = HashSet::new();
5286 loop {
5287 if !visited.insert(current) {
5288 return None;
5289 }
5290 let schema = self.resolved_cache.get(current)?;
5291 if let SchemaType::Reference { target } = &schema.schema_type {
5292 current = target;
5293 } else {
5294 return Some(schema);
5295 }
5296 }
5297 }
5298
5299 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
5301 match schema.schema_type() {
5302 Some(crate::openapi::SchemaType::Object) => true,
5303 None => schema.details().properties.is_some(),
5304 _ => false,
5305 }
5306 }
5307}