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")]
458 pub rust_ident: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
468 pub query_serialization: Option<QuerySerialization>,
469 #[serde(skip)]
472 pub validation_schema: Option<Value>,
473}
474
475#[derive(Debug, Clone, PartialEq, serde::Serialize)]
478pub enum QuerySerialization {
479 FormExplodedObject,
483 FormObject,
486 DeepObject,
489 FormExplodedArray { item_type: ArrayItemType },
492 FormArray { item_type: ArrayItemType },
495 Unsupported { reason: String },
500}
501
502#[derive(Debug, Clone, PartialEq, serde::Serialize)]
509pub enum ArrayItemType {
510 Scalar(String),
512 EnumRef(String),
514}
515
516impl Default for DependencyGraph {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522impl DependencyGraph {
523 pub fn new() -> Self {
524 Self {
525 edges: BTreeMap::new(),
526 recursive_schemas: HashSet::new(),
527 }
528 }
529
530 pub fn add_dependency(&mut self, from: String, to: String) {
531 self.edges.entry(from).or_default().insert(to);
532 }
533
534 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
536 self.detect_recursive_schemas();
538
539 let mut temp_edges = self.edges.clone();
541 for (schema, deps) in &mut temp_edges {
542 deps.remove(schema); }
544
545 let mut visited = HashSet::new();
546 let mut temp_visited = HashSet::new();
547 let mut result = Vec::new();
548
549 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
551 all_nodes.sort();
552 for node in all_nodes {
553 if !visited.contains(node) {
554 self.visit_node_recursive(
555 node,
556 &temp_edges,
557 &mut visited,
558 &mut temp_visited,
559 &mut result,
560 )?;
561 }
562 }
563
564 result.reverse();
565 Ok(result)
566 }
567
568 fn detect_recursive_schemas(&mut self) {
569 for (schema, deps) in &self.edges {
570 if deps.contains(schema) {
571 self.recursive_schemas.insert(schema.clone());
573 } else {
574 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
576 self.recursive_schemas.insert(schema.clone());
577 }
578 }
579 }
580
581 for (schema, deps) in &self.edges {
583 for dep in deps {
584 if let Some(dep_deps) = self.edges.get(dep) {
585 if dep_deps.contains(schema) {
586 self.recursive_schemas.insert(schema.clone());
588 self.recursive_schemas.insert(dep.clone());
589 }
590 }
591 }
592 }
593 }
594
595 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
596 if visited.contains(current) {
597 return false; }
599
600 visited.insert(current.to_string());
601
602 if let Some(deps) = self.edges.get(current) {
603 for dep in deps {
604 if dep == start {
605 return true; }
607 if self.has_cycle_from(start, dep, visited) {
608 return true;
609 }
610 }
611 }
612
613 false
614 }
615
616 #[allow(clippy::only_used_in_recursion)]
617 fn visit_node_recursive(
618 &self,
619 node: &str,
620 temp_edges: &BTreeMap<String, HashSet<String>>,
621 visited: &mut HashSet<String>,
622 temp_visited: &mut HashSet<String>,
623 result: &mut Vec<String>,
624 ) -> Result<()> {
625 if temp_visited.contains(node) {
626 return Ok(());
628 }
629
630 if visited.contains(node) {
631 return Ok(());
632 }
633
634 temp_visited.insert(node.to_string());
635
636 if let Some(dependencies) = temp_edges.get(node) {
637 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
639 sorted_deps.sort();
640 for dep in sorted_deps {
641 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
642 }
643 }
644
645 temp_visited.remove(node);
646 visited.insert(node.to_string());
647 result.push(node.to_string());
648
649 Ok(())
650 }
651}
652
653pub fn merge_schema_extensions(
656 main_spec: Value,
657 extension_paths: &[impl AsRef<Path>],
658) -> Result<Value> {
659 let mut result = main_spec;
660
661 for path in extension_paths {
662 let extension = load_extension_file(path.as_ref())?;
663 result = merge_json_objects_with_replacements(result, extension)?;
664 }
665
666 Ok(result)
667}
668
669fn load_extension_file(path: &Path) -> Result<Value> {
673 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
674 message: format!("Failed to read file {}: {}", path.display(), e),
675 })?;
676
677 let is_yaml = path
678 .extension()
679 .and_then(|extension| extension.to_str())
680 .is_some_and(|extension| {
681 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
682 });
683
684 if is_yaml {
685 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
686 GeneratorError::FileError {
687 message: format!(
688 "Failed to parse schema extension {} as YAML: {}",
689 path.display(),
690 error
691 ),
692 }
693 })
694 } else {
695 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
696 message: format!(
697 "Failed to parse schema extension {} as JSON: {}",
698 path.display(),
699 error
700 ),
701 })
702 }
703}
704
705fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
707 let replacements = extract_replacement_rules(&extension);
709
710 Ok(merge_json_objects_with_rules(
712 main,
713 extension,
714 &replacements,
715 ))
716}
717
718fn extract_replacement_rules(
720 extension: &Value,
721) -> std::collections::HashMap<String, (String, String)> {
722 let mut rules = std::collections::HashMap::new();
723
724 if let Some(x_replacements) = extension.get("x-replacements") {
725 if let Some(x_replacements_obj) = x_replacements.as_object() {
726 for (schema_name, replacement_rule) in x_replacements_obj {
727 if let Some(rule_obj) = replacement_rule.as_object() {
728 if let (Some(replace), Some(with)) = (
729 rule_obj.get("replace").and_then(|v| v.as_str()),
730 rule_obj.get("with").and_then(|v| v.as_str()),
731 ) {
732 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
733 }
735 }
736 }
737 }
738 }
739
740 rules
741}
742
743fn should_replace_variant(
745 schema_name: &str,
746 extension_refs: &[String],
747 replacements: &std::collections::HashMap<String, (String, String)>,
748) -> bool {
749 for (replace_schema, with_schema) in replacements.values() {
751 if schema_name == replace_schema {
752 let replacement_exists = extension_refs.iter().any(|ext_ref| {
754 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
755 ext_schema_name == with_schema
756 });
757
758 if replacement_exists {
759 return true;
760 }
761 }
762 }
763
764 extension_refs.iter().any(|ext_ref| {
766 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
767 schema_name == ext_schema_name
768 })
769}
770
771fn merge_json_objects_with_rules(
776 main: Value,
777 extension: Value,
778 replacements: &std::collections::HashMap<String, (String, String)>,
779) -> Value {
780 match (main, extension) {
781 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
783 let main_union_keyword = if main_obj.contains_key("oneOf") {
786 Some("oneOf")
787 } else if main_obj.contains_key("anyOf") {
788 Some("anyOf")
789 } else {
790 None
791 };
792 if let (Some(main_variants), Some(ext_variants)) = (
793 extract_schema_variants(&Value::Object(main_obj.clone())),
794 extract_schema_variants(&Value::Object(ext_obj.clone())),
795 ) {
796 let union_key = main_union_keyword.unwrap_or("oneOf");
797 println!(
798 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
799 main_variants.len(),
800 ext_variants.len()
801 );
802 let mut merged_variants = Vec::new();
805 let extension_refs: Vec<String> = ext_variants
806 .iter()
807 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
808 .map(|s| s.to_string())
809 .collect();
810
811 for main_variant in main_variants {
813 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
814 let schema_name = main_ref.split('/').next_back().unwrap_or("");
816 let should_replace =
817 should_replace_variant(schema_name, &extension_refs, replacements);
818
819 if should_replace {
820 println!("🔄 REPLACING {} (explicit rule)", schema_name);
821 }
822
823 if !should_replace {
824 merged_variants.push(main_variant);
825 }
826 } else {
827 merged_variants.push(main_variant);
829 }
830 }
831
832 for ext_variant in ext_variants {
834 merged_variants.push(ext_variant);
835 }
836
837 main_obj.remove("oneOf");
839 main_obj.remove("anyOf");
840 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
841
842 for (key, ext_value) in ext_obj {
844 if key != "oneOf" && key != "anyOf" {
845 match main_obj.get(&key) {
846 Some(main_value) => {
847 let merged_value = merge_json_objects_with_rules(
848 main_value.clone(),
849 ext_value,
850 replacements,
851 );
852 main_obj.insert(key, merged_value);
853 }
854 None => {
855 main_obj.insert(key, ext_value);
856 }
857 }
858 }
859 }
860
861 return Value::Object(main_obj);
862 }
863
864 for (key, ext_value) in ext_obj {
866 match main_obj.get(&key) {
867 Some(main_value) => {
868 let merged_value = merge_json_objects_with_rules(
870 main_value.clone(),
871 ext_value,
872 replacements,
873 );
874 main_obj.insert(key, merged_value);
875 }
876 None => {
877 main_obj.insert(key, ext_value);
879 }
880 }
881 }
882 Value::Object(main_obj)
883 }
884
885 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
887 main_arr.extend(ext_arr);
888 Value::Array(main_arr)
889 }
890
891 (_, extension) => extension,
893 }
894}
895
896fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
898 if let Value::Object(map) = obj {
899 if let Some(Value::Array(variants)) = map.get("oneOf") {
900 return Some(variants.clone());
901 }
902 if let Some(Value::Array(variants)) = map.get("anyOf") {
903 return Some(variants.clone());
904 }
905 }
906 None
907}
908
909pub struct SchemaAnalyzer {
910 schemas: BTreeMap<String, Schema>,
911 resolved_cache: BTreeMap<String, AnalyzedSchema>,
912 openapi_spec: Value,
913 current_schema_name: Option<String>,
914 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
915 type_mapper: TypeMapper,
920}
921
922impl SchemaAnalyzer {
923 pub fn new(openapi_spec: Value) -> Result<Self> {
927 Self::with_type_mapper(openapi_spec, TypeMapper::default())
928 }
929
930 pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
934 let spec: OpenApiSpec =
935 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
936 let schemas = Self::extract_schemas(&spec)?;
937
938 let component_parameters = spec
939 .components
940 .as_ref()
941 .and_then(|c| c.parameters.as_ref())
942 .cloned()
943 .unwrap_or_default();
944 Ok(Self {
945 schemas,
946 resolved_cache: BTreeMap::new(),
947 openapi_spec,
948 current_schema_name: None,
949 component_parameters,
950 type_mapper,
951 })
952 }
953
954 pub fn new_with_extensions(
957 openapi_spec: Value,
958 extension_paths: &[std::path::PathBuf],
959 ) -> Result<Self> {
960 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
961 Self::new(merged_spec)
962 }
963
964 pub fn new_with_extensions_and_type_mapper(
967 openapi_spec: Value,
968 extension_paths: &[std::path::PathBuf],
969 type_mapper: TypeMapper,
970 ) -> Result<Self> {
971 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
972 Self::with_type_mapper(merged_spec, type_mapper)
973 }
974
975 pub fn type_mapper(&self) -> &TypeMapper {
979 &self.type_mapper
980 }
981
982 fn generate_context_aware_name(
985 &self,
986 base_context: &str,
987 type_hint: &str,
988 index: usize,
989 schema: Option<&Schema>,
990 ) -> String {
991 if let Some(schema) = schema {
993 if type_hint == "Array"
995 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
996 {
997 if let Some(items_schema) = &schema.details().items {
998 if let Some(item_type) = items_schema.schema_type() {
1000 match item_type {
1001 OpenApiSchemaType::Object => {
1002 return format!("{base_context}ItemArray");
1003 }
1004 OpenApiSchemaType::String => {
1005 return format!("{base_context}StringArray");
1006 }
1007 _ => {}
1008 }
1009 }
1010 }
1011 }
1012 }
1013
1014 match type_hint {
1016 "Array" => {
1017 format!("{base_context}Array")
1019 }
1020 "Variant" | "InlineVariant" => {
1021 if index == 0 {
1023 format!("{base_context}{type_hint}")
1024 } else {
1025 format!("{}{}{}", base_context, type_hint, index + 1)
1026 }
1027 }
1028 _ => {
1029 format!("{base_context}{type_hint}{index}")
1031 }
1032 }
1033 }
1034
1035 fn to_pascal_case(&self, s: &str) -> String {
1037 s.split(['_', '-'])
1038 .filter(|part| !part.is_empty())
1039 .map(|part| {
1040 let mut chars = part.chars();
1041 match chars.next() {
1042 None => String::new(),
1043 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1044 }
1045 })
1046 .collect()
1047 }
1048
1049 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
1050 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
1055 Ok(schemas
1056 .map(|m| {
1057 m.iter()
1058 .map(|(k, v)| (k.clone(), v.clone()))
1059 .collect::<BTreeMap<_, _>>()
1060 })
1061 .unwrap_or_default())
1062 }
1063
1064 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
1065 let validation_context = ValidationContext {
1066 openapi_version: self
1067 .openapi_spec
1068 .get("openapi")
1069 .and_then(Value::as_str)
1070 .unwrap_or_default()
1071 .to_string(),
1072 json_schema_dialect: self
1073 .openapi_spec
1074 .get("jsonSchemaDialect")
1075 .and_then(Value::as_str)
1076 .map(str::to_string),
1077 component_schemas: self
1078 .openapi_spec
1079 .pointer("/components/schemas")
1080 .and_then(Value::as_object)
1081 .map(|schemas| {
1082 schemas
1083 .iter()
1084 .map(|(name, schema)| (name.clone(), schema.clone()))
1085 .collect()
1086 })
1087 .unwrap_or_default(),
1088 };
1089 let mut analysis = SchemaAnalysis {
1090 schemas: BTreeMap::new(),
1091 dependencies: DependencyGraph::new(),
1092 patterns: DetectedPatterns {
1093 tagged_enum_schemas: HashSet::new(),
1094 untagged_enum_schemas: HashSet::new(),
1095 type_mappings: BTreeMap::new(),
1096 },
1097 operations: BTreeMap::new(),
1098 operation_responses: BTreeMap::new(),
1099 operation_id_aliases: BTreeMap::new(),
1100 used_type_features: crate::type_mapping::UsedFeatures::default(),
1101 enum_extensions: BTreeMap::new(),
1102 validation_context,
1103 };
1104
1105 self.detect_patterns(&mut analysis.patterns)?;
1107
1108 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
1110 for schema_name in schema_names {
1111 let analyzed = self.analyze_schema(&schema_name)?;
1112
1113 for dep in &analyzed.dependencies {
1115 analysis
1116 .dependencies
1117 .add_dependency(schema_name.clone(), dep.clone());
1118 }
1119
1120 analysis.schemas.insert(schema_name, analyzed);
1121 }
1122
1123 for (inline_name, inline_schema) in &self.resolved_cache {
1126 if !analysis.schemas.contains_key(inline_name) {
1127 analysis
1129 .schemas
1130 .insert(inline_name.clone(), inline_schema.clone());
1131
1132 for dep in &inline_schema.dependencies {
1134 analysis
1135 .dependencies
1136 .add_dependency(inline_name.clone(), dep.clone());
1137 }
1138
1139 let mut schemas_to_update = Vec::new();
1144 for (schema_name, schema) in &analysis.schemas {
1145 if schema_name == inline_name {
1147 continue;
1148 }
1149
1150 if schema.dependencies.contains(inline_name) {
1151 schemas_to_update.push(schema_name.clone());
1153 }
1154 }
1155
1156 for schema_name in schemas_to_update {
1158 analysis
1159 .dependencies
1160 .add_dependency(schema_name, inline_name.clone());
1161 }
1162 }
1163 }
1164
1165 self.analyze_operations(&mut analysis)?;
1167
1168 for (inline_name, inline_schema) in &self.resolved_cache {
1171 if !analysis.schemas.contains_key(inline_name) {
1172 analysis
1173 .schemas
1174 .insert(inline_name.clone(), inline_schema.clone());
1175
1176 for dep in &inline_schema.dependencies {
1178 analysis
1179 .dependencies
1180 .add_dependency(inline_name.clone(), dep.clone());
1181 }
1182 }
1183 }
1184
1185 analysis.used_type_features = self.type_mapper.used_features();
1189
1190 for (name, analyzed) in &analysis.schemas {
1195 let enum_value_count = match &analyzed.schema_type {
1196 SchemaType::StringEnum { values } => values.len(),
1197 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1198 _ => continue,
1199 };
1200 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1201 analysis.enum_extensions.insert(name.clone(), ext);
1202 }
1203 }
1204
1205 Ok(analysis)
1206 }
1207
1208 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1209 for (schema_name, schema) in &self.schemas {
1210 if self.is_discriminated_union(schema) {
1212 patterns.tagged_enum_schemas.insert(schema_name.clone());
1213
1214 if let Some(mappings) = self.extract_type_mappings(schema)? {
1216 patterns.type_mappings.insert(schema_name.clone(), mappings);
1217 }
1218 }
1219 else if self.is_simple_union(schema) {
1221 patterns.untagged_enum_schemas.insert(schema_name.clone());
1222 }
1223 }
1224
1225 Ok(())
1226 }
1227
1228 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1229 if schema.is_discriminated_union() {
1231 return true;
1232 }
1233
1234 if let Some(variants) = schema.union_variants() {
1236 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1237 }
1238
1239 false
1240 }
1241
1242 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1243 variants.iter().all(|variant| {
1244 if let Some(ref_str) = variant.reference() {
1245 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1247 if let Some(schema) = self.schemas.get(schema_name) {
1248 return self.has_const_discriminator_field(schema, field_name);
1249 }
1250 }
1251 } else {
1252 return self.has_const_discriminator_field(variant, field_name);
1254 }
1255 false
1256 })
1257 }
1258
1259 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1268 if let Some(ref_str) = schema.reference() {
1270 return match self
1271 .extract_schema_name(ref_str)
1272 .and_then(|n| self.schemas.get(n))
1273 {
1274 Some(target) => self.branch_resolves_to_object(target),
1275 None => false,
1276 };
1277 }
1278 if matches!(
1281 schema,
1282 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1283 ) {
1284 return true;
1285 }
1286 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1287 return true;
1288 }
1289 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1290 return true;
1291 }
1292 false
1295 }
1296
1297 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1301 if variants.is_empty() {
1302 return None;
1303 }
1304
1305 let first_variant = &variants[0];
1307 let first_schema = if let Some(ref_str) = first_variant.reference() {
1308 let schema_name = self.extract_schema_name(ref_str)?;
1309 self.schemas.get(schema_name)?
1310 } else {
1311 first_variant
1312 };
1313
1314 let properties = first_schema.details().properties.as_ref()?;
1315 let mut candidates: Vec<String> = Vec::new();
1316
1317 for (field_name, field_schema) in properties {
1318 let details = field_schema.details();
1319 let is_const = details.const_value.is_some()
1320 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1321 || details.extra.contains_key("const");
1322 if is_const {
1323 candidates.push(field_name.clone());
1324 }
1325 }
1326
1327 if candidates.is_empty() {
1328 return None;
1329 }
1330
1331 candidates.sort_by(|a, b| {
1333 if a == "type" {
1334 std::cmp::Ordering::Less
1335 } else if b == "type" {
1336 std::cmp::Ordering::Greater
1337 } else {
1338 a.cmp(b)
1339 }
1340 });
1341
1342 for candidate in &candidates {
1344 if self.all_variants_have_const_field(variants, candidate) {
1345 return Some(candidate.clone());
1346 }
1347 }
1348
1349 None
1350 }
1351
1352 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1353 if let Some(properties) = &schema.details().properties {
1354 if let Some(field) = properties.get(field_name) {
1355 if field.details().const_value.is_some() {
1357 return true;
1358 }
1359 if let Some(enum_vals) = &field.details().enum_values {
1361 return enum_vals.len() == 1;
1362 }
1363 return field.details().extra.contains_key("const");
1365 }
1366 }
1367 false
1368 }
1369
1370 fn is_simple_union(&self, schema: &Schema) -> bool {
1371 if let Some(variants) = schema.union_variants() {
1372 if variants.len() > 1 && !schema.is_nullable_pattern() {
1374 let has_refs = variants.iter().any(|v| v.is_reference());
1375 return has_refs;
1376 }
1377 }
1378 false
1379 }
1380
1381 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1382 let variants = schema.union_variants().ok_or_else(|| {
1383 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1384 })?;
1385
1386 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1388 discriminator.property_name.clone()
1389 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1390 detected
1391 } else {
1392 "type".to_string() };
1394
1395 let mut mappings = BTreeMap::new();
1396
1397 for variant in variants {
1398 if let Some(ref_str) = variant.reference() {
1399 if let Some(type_name) = self.extract_schema_name(ref_str) {
1400 if let Some(variant_schema) = self.schemas.get(type_name) {
1401 if let Some(discriminator_value) = self
1402 .extract_discriminator_value_for_field(
1403 variant_schema,
1404 &discriminator_field,
1405 )
1406 {
1407 mappings.insert(type_name.to_string(), discriminator_value);
1408 }
1409 }
1410 }
1411 }
1412 }
1413
1414 if mappings.is_empty() {
1415 Ok(None)
1416 } else {
1417 Ok(Some(mappings))
1418 }
1419 }
1420
1421 #[allow(dead_code)]
1422 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1423 self.extract_discriminator_value_for_field(schema, "type")
1424 }
1425
1426 fn extract_discriminator_value_for_field(
1427 &self,
1428 schema: &Schema,
1429 field_name: &str,
1430 ) -> Option<String> {
1431 if let Some(properties) = &schema.details().properties {
1432 if let Some(type_field) = properties.get(field_name) {
1433 if let Some(const_value) = &type_field.details().const_value {
1435 if let Some(value) = const_value.as_str() {
1436 return Some(value.to_string());
1437 }
1438 }
1439 if let Some(enum_values) = &type_field.details().enum_values {
1441 if enum_values.len() == 1 {
1442 return enum_values[0].as_str().map(|s| s.to_string());
1443 }
1444 }
1445 if let Some(const_value) = type_field.details().extra.get("const") {
1447 return const_value.as_str().map(|s| s.to_string());
1448 }
1449 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1451 if stainless_const.as_bool() == Some(true) {
1452 if let Some(default_value) = &type_field.details().default {
1453 if let Some(value) = default_value.as_str() {
1454 return Some(value.to_string());
1455 }
1456 }
1457 }
1458 }
1459 }
1460 }
1461 None
1462 }
1463
1464 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1465 schema.reference().or_else(|| schema.recursive_reference())
1466 }
1467
1468 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1469 if ref_str == "#" {
1470 return None; }
1472
1473 let parts: Vec<&str> = ref_str.split('/').collect();
1474
1475 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1477 return Some(parts[3]);
1478 }
1479
1480 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1483 return Some(parts[2]);
1484 }
1485
1486 let last = parts.last()?;
1492 if last.is_empty()
1493 || last.chars().all(|c| c.is_ascii_digit())
1494 || matches!(
1495 *last,
1496 "schema" | "properties" | "items" | "additionalProperties"
1497 )
1498 {
1499 return None;
1500 }
1501 let first = last.chars().next().unwrap_or(' ');
1502 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1503 return None;
1504 }
1505 Some(last)
1506 }
1507
1508 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1509 if let Some(cached) = self.resolved_cache.get(schema_name) {
1511 return Ok(cached.clone());
1512 }
1513
1514 self.current_schema_name = Some(schema_name.to_string());
1516
1517 let schema = self
1518 .schemas
1519 .get(schema_name)
1520 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1521 .clone();
1522
1523 self.resolved_cache.insert(
1525 schema_name.to_string(),
1526 AnalyzedSchema {
1527 name: schema_name.to_string(),
1528 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1529 schema_type: SchemaType::Reference {
1530 target: "placeholder".to_string(),
1531 },
1532 dependencies: HashSet::new(),
1533 nullable: false,
1534 description: None,
1535 default: None,
1536 },
1537 );
1538
1539 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1540
1541 self.resolved_cache
1543 .insert(schema_name.to_string(), analyzed.clone());
1544
1545 Ok(analyzed)
1546 }
1547
1548 fn analyze_schema_value(
1549 &mut self,
1550 schema: &Schema,
1551 schema_name: &str,
1552 ) -> Result<AnalyzedSchema> {
1553 let details = schema.details();
1554 let description = details.description.clone();
1555 let nullable = details.is_nullable() || schema.type_array_contains_null();
1557 let mut dependencies = HashSet::new();
1558
1559 let schema_type = match schema {
1560 Schema::Reference { reference, .. } => {
1561 match self.extract_schema_name(reference) {
1566 Some(name) => {
1567 let target = name.to_string();
1568 dependencies.insert(target.clone());
1569 SchemaType::Reference { target }
1570 }
1571 None => {
1572 eprintln!(
1573 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1574 reference
1575 );
1576 SchemaType::Primitive {
1577 rust_type: "serde_json::Value".to_string(),
1578 serde_with: None,
1579 }
1580 }
1581 }
1582 }
1583 Schema::RecursiveRef { recursive_ref, .. }
1584 | Schema::DynamicRef {
1585 dynamic_ref: recursive_ref,
1586 ..
1587 } => {
1588 if recursive_ref == "#" {
1594 dependencies.insert(schema_name.to_string());
1595 SchemaType::Reference {
1596 target: schema_name.to_string(),
1597 }
1598 } else {
1599 let target = self
1600 .extract_schema_name(recursive_ref)
1601 .unwrap_or(schema_name)
1602 .to_string();
1603 dependencies.insert(target.clone());
1604 SchemaType::Reference { target }
1605 }
1606 }
1607 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1608 let primary = schema
1609 .schema_type()
1610 .cloned()
1611 .unwrap_or(OpenApiSchemaType::Object);
1612 let format = details.format.as_deref();
1613 match primary {
1614 OpenApiSchemaType::String => {
1615 if let Some(values) = details.string_enum_values() {
1616 SchemaType::StringEnum { values }
1617 } else {
1618 SchemaType::Primitive {
1619 rust_type: self.type_mapper.string_format(format).rust_type,
1620 serde_with: None,
1621 }
1622 }
1623 }
1624 OpenApiSchemaType::Integer => SchemaType::Primitive {
1625 rust_type: self.type_mapper.integer_format(format).rust_type,
1626 serde_with: None,
1627 },
1628 OpenApiSchemaType::Number => SchemaType::Primitive {
1629 rust_type: self.type_mapper.number_format(format).rust_type,
1630 serde_with: None,
1631 },
1632 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1633 rust_type: self.type_mapper.boolean().rust_type,
1634 serde_with: None,
1635 },
1636 OpenApiSchemaType::Array => {
1637 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1639 }
1640 OpenApiSchemaType::Object => {
1641 if self.should_use_dynamic_json(schema) {
1643 SchemaType::Primitive {
1644 rust_type: self.type_mapper.dynamic_json().rust_type,
1645 serde_with: None,
1646 }
1647 } else {
1648 self.analyze_object_schema(schema, &mut dependencies)?
1650 }
1651 }
1652 _ => SchemaType::Primitive {
1653 rust_type: self.type_mapper.dynamic_json().rust_type,
1654 serde_with: None,
1655 },
1656 }
1657 }
1658 Schema::AnyOf {
1659 any_of,
1660 discriminator,
1661 ..
1662 } => {
1663 self.analyze_anyof_union(
1665 any_of,
1666 discriminator.as_ref(),
1667 &mut dependencies,
1668 schema_name,
1669 )?
1670 }
1671 Schema::OneOf {
1672 one_of,
1673 discriminator,
1674 ..
1675 } => {
1676 self.analyze_oneof_union(
1678 one_of,
1679 discriminator.as_ref(),
1680 schema_name,
1681 &mut dependencies,
1682 )?
1683 }
1684 Schema::AllOf { all_of, .. } => {
1685 self.analyze_allof_composition(all_of, &mut dependencies)?
1687 }
1688 Schema::Untyped { .. } => {
1689 if let Some(inferred) = schema.inferred_type() {
1691 match inferred {
1692 OpenApiSchemaType::Object => {
1693 if self.should_use_dynamic_json(schema) {
1694 SchemaType::Primitive {
1695 rust_type: "serde_json::Value".to_string(),
1696 serde_with: None,
1697 }
1698 } else {
1699 self.analyze_object_schema(schema, &mut dependencies)?
1700 }
1701 }
1702 OpenApiSchemaType::String if details.is_string_enum() => {
1703 SchemaType::StringEnum {
1704 values: details.string_enum_values().unwrap_or_default(),
1705 }
1706 }
1707 _ => SchemaType::Primitive {
1708 rust_type: "serde_json::Value".to_string(),
1709 serde_with: None,
1710 },
1711 }
1712 } else {
1713 SchemaType::Primitive {
1714 rust_type: "serde_json::Value".to_string(),
1715 serde_with: None,
1716 }
1717 }
1718 }
1719 };
1720
1721 Ok(AnalyzedSchema {
1722 name: schema_name.to_string(),
1723 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1725 dependencies,
1726 nullable,
1727 description,
1728 default: details.default.clone(),
1729 })
1730 }
1731
1732 fn analyze_object_schema(
1733 &mut self,
1734 schema: &Schema,
1735 dependencies: &mut HashSet<String>,
1736 ) -> Result<SchemaType> {
1737 let details = schema.details();
1738 let properties = &details.properties;
1739 let required = details
1740 .required
1741 .as_ref()
1742 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1743 .unwrap_or_default();
1744
1745 let mut property_info = BTreeMap::new();
1746
1747 if let Some(props) = properties {
1748 for (prop_name, prop_schema) in props {
1749 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1751 if self.should_use_dynamic_json(prop_schema) {
1753 SchemaType::Primitive {
1755 rust_type: "serde_json::Value".to_string(),
1756 serde_with: None,
1757 }
1758 } else if prop_schema.is_nullable_pattern()
1759 && let Some(non_null) = prop_schema.non_null_variant()
1760 {
1761 self.analyze_property_schema_with_context(
1769 non_null,
1770 Some(prop_name),
1771 dependencies,
1772 )?
1773 } else {
1774 let context_name = self
1777 .current_schema_name
1778 .clone()
1779 .unwrap_or_else(|| "Unknown".to_string());
1780
1781 let prop_pascal = self.to_pascal_case(prop_name);
1783 let mut union_type_name = format!("{context_name}{prop_pascal}");
1784
1785 if self.schemas.contains_key(&union_type_name)
1788 || self.resolved_cache.contains_key(&union_type_name)
1789 {
1790 let mut suffix = 2;
1791 loop {
1792 let candidate = format!("{union_type_name}Union{suffix}");
1793 if !self.schemas.contains_key(&candidate)
1794 && !self.resolved_cache.contains_key(&candidate)
1795 {
1796 union_type_name = candidate;
1797 break;
1798 }
1799 suffix += 1;
1800 if suffix > 1000 {
1801 break;
1802 }
1803 }
1804 }
1805
1806 let union_schema_type = self.analyze_anyof_union(
1808 any_of,
1809 prop_schema.discriminator(),
1810 dependencies,
1811 &union_type_name,
1812 )?;
1813
1814 self.resolved_cache.insert(
1816 union_type_name.clone(),
1817 AnalyzedSchema {
1818 name: union_type_name.clone(),
1819 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1820 schema_type: union_schema_type,
1821 dependencies: HashSet::new(),
1822 nullable: false,
1823 description: prop_schema.details().description.clone(),
1824 default: None,
1825 },
1826 );
1827
1828 dependencies.insert(union_type_name.clone());
1830 SchemaType::Reference {
1831 target: union_type_name,
1832 }
1833 }
1834 } else if let Schema::OneOf {
1835 one_of,
1836 discriminator,
1837 ..
1838 } = prop_schema
1839 {
1840 if prop_schema.is_nullable_pattern()
1847 && let Some(non_null) = prop_schema.non_null_variant()
1848 {
1849 let unwrapped = self.analyze_property_schema_with_context(
1850 non_null,
1851 Some(prop_name),
1852 dependencies,
1853 )?;
1854 let prop_details = prop_schema.details();
1855 let prop_nullable = true;
1856 let prop_description = prop_details.description.clone();
1857 let prop_default = prop_details.default.clone();
1858 property_info.insert(
1859 prop_name.clone(),
1860 PropertyInfo {
1861 schema_type: unwrapped,
1862 nullable: prop_nullable,
1863 description: prop_description,
1864 default: prop_default,
1865 serde_attrs: Vec::new(),
1866 constraints: PropertyConstraints::from_schema_details(prop_details),
1867 },
1868 );
1869 continue;
1870 }
1871
1872 let context_name = self
1874 .current_schema_name
1875 .clone()
1876 .unwrap_or_else(|| "Unknown".to_string());
1877 let prop_pascal = self.to_pascal_case(prop_name);
1878 let mut union_type_name = format!("{context_name}{prop_pascal}");
1879 if self.schemas.contains_key(&union_type_name)
1881 || self.resolved_cache.contains_key(&union_type_name)
1882 {
1883 let mut suffix = 2;
1884 loop {
1885 let candidate = format!("{union_type_name}Union{suffix}");
1886 if !self.schemas.contains_key(&candidate)
1887 && !self.resolved_cache.contains_key(&candidate)
1888 {
1889 union_type_name = candidate;
1890 break;
1891 }
1892 suffix += 1;
1893 if suffix > 1000 {
1894 break;
1895 }
1896 }
1897 }
1898
1899 let union_schema_type = self.analyze_oneof_union(
1901 one_of,
1902 discriminator.as_ref(),
1903 &union_type_name,
1904 dependencies,
1905 )?;
1906
1907 self.resolved_cache.insert(
1909 union_type_name.clone(),
1910 AnalyzedSchema {
1911 name: union_type_name.clone(),
1912 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1913 schema_type: union_schema_type,
1914 dependencies: HashSet::new(),
1915 nullable: false,
1916 description: prop_schema.details().description.clone(),
1917 default: None,
1918 },
1919 );
1920
1921 dependencies.insert(union_type_name.clone());
1923 SchemaType::Reference {
1924 target: union_type_name,
1925 }
1926 } else {
1927 self.analyze_property_schema_with_context(
1929 prop_schema,
1930 Some(prop_name),
1931 dependencies,
1932 )?
1933 };
1934
1935 let prop_details = prop_schema.details();
1936 let prop_nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
1938 let prop_description = prop_details.description.clone();
1939 let prop_default = prop_details.default.clone();
1940
1941 property_info.insert(
1942 prop_name.clone(),
1943 PropertyInfo {
1944 schema_type: prop_type,
1945 nullable: prop_nullable,
1946 description: prop_description,
1947 default: prop_default,
1948 serde_attrs: Vec::new(),
1949 constraints: PropertyConstraints::from_schema_details(prop_details),
1950 },
1951 );
1952 }
1953 }
1954
1955 let typed_enabled = self
1963 .type_mapper
1964 .config()
1965 .shape
1966 .as_ref()
1967 .and_then(|s| s.additional_properties_typed)
1968 .unwrap_or(true);
1969
1970 let additional_properties = match &details.additional_properties {
1971 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
1972 ObjectAdditionalProperties::Untyped
1973 }
1974 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
1975 ObjectAdditionalProperties::Forbidden
1976 }
1977 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
1978 let analyzed =
1979 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
1980 ObjectAdditionalProperties::Typed {
1981 value_type: Box::new(analyzed),
1982 }
1983 }
1984 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
1985 ObjectAdditionalProperties::Untyped
1987 }
1988 None => ObjectAdditionalProperties::Forbidden,
1989 };
1990
1991 Ok(SchemaType::Object {
1992 properties: property_info,
1993 required,
1994 additional_properties,
1995 })
1996 }
1997
1998 fn analyze_property_schema_with_context(
1999 &mut self,
2000 schema: &Schema,
2001 property_name: Option<&str>,
2002 dependencies: &mut HashSet<String>,
2003 ) -> Result<SchemaType> {
2004 if let Some(ref_str) = self.get_any_reference(schema) {
2005 let target_opt = if ref_str == "#" {
2006 Some(
2007 self.find_recursive_anchor_schema()
2008 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2009 )
2010 } else {
2011 self.extract_schema_name(ref_str).map(|s| s.to_string())
2012 };
2013 match target_opt {
2014 Some(target) => {
2015 dependencies.insert(target.clone());
2016 return Ok(SchemaType::Reference { target });
2017 }
2018 None => {
2019 eprintln!(
2020 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2021 ref_str
2022 );
2023 return Ok(SchemaType::Primitive {
2024 rust_type: "serde_json::Value".to_string(),
2025 serde_with: None,
2026 });
2027 }
2028 }
2029 }
2030
2031 if let Some(schema_type) = schema.schema_type() {
2032 match schema_type {
2033 OpenApiSchemaType::String => {
2034 if let Some(enum_values) = schema.details().string_enum_values() {
2036 let context_name = self
2039 .current_schema_name
2040 .clone()
2041 .unwrap_or_else(|| "Unknown".to_string());
2042
2043 let primary_name = if let Some(prop_name) = property_name {
2045 let prop_pascal = self.to_pascal_case(prop_name);
2047 format!("{context_name}{prop_pascal}")
2048 } else {
2049 let suffix = if !enum_values.is_empty() {
2052 let first_value = self.to_pascal_case(&enum_values[0]);
2053 format!("{first_value}Enum")
2054 } else {
2055 "StringEnum".to_string()
2056 };
2057 format!("{context_name}{suffix}")
2058 };
2059
2060 return Ok(self.hoist_inline_string_enum(
2061 schema,
2062 enum_values,
2063 primary_name,
2064 dependencies,
2065 ));
2066 } else {
2067 let mapped = self
2073 .type_mapper
2074 .string_format(schema.details().format.as_deref());
2075 return Ok(SchemaType::Primitive {
2076 rust_type: mapped.rust_type,
2077 serde_with: mapped.serde_with,
2078 });
2079 }
2080 }
2081 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2082 let details = schema.details();
2083 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2084 return Ok(SchemaType::Primitive {
2085 rust_type,
2086 serde_with: None,
2087 });
2088 }
2089 OpenApiSchemaType::Boolean => {
2090 return Ok(SchemaType::Primitive {
2091 rust_type: "bool".to_string(),
2092 serde_with: None,
2093 });
2094 }
2095 OpenApiSchemaType::Array => {
2096 let context_name = if let Some(prop_name) = property_name {
2098 let prop_pascal = self.to_pascal_case(prop_name);
2100 format!(
2101 "{}{}",
2102 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2103 prop_pascal
2104 )
2105 } else {
2106 "ArrayItem".to_string()
2108 };
2109 return self.analyze_array_schema(schema, &context_name, dependencies);
2110 }
2111 OpenApiSchemaType::Object => {
2112 if self.should_use_dynamic_json(schema) {
2114 return Ok(SchemaType::Primitive {
2115 rust_type: "serde_json::Value".to_string(),
2116 serde_with: None,
2117 });
2118 }
2119 let object_type_name = if let Some(prop_name) = property_name {
2121 let prop_pascal = self.to_pascal_case(prop_name);
2123 format!(
2124 "{}{}",
2125 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2126 prop_pascal
2127 )
2128 } else {
2129 format!(
2131 "{}Object",
2132 self.current_schema_name.as_deref().unwrap_or("Unknown")
2133 )
2134 };
2135
2136 let object_type = self.analyze_object_schema(schema, dependencies)?;
2138
2139 let inline_schema = AnalyzedSchema {
2141 name: object_type_name.clone(),
2142 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2143 schema_type: object_type,
2144 dependencies: dependencies.clone(),
2145 nullable: false,
2146 description: schema.details().description.clone(),
2147 default: None,
2148 };
2149
2150 self.resolved_cache
2152 .insert(object_type_name.clone(), inline_schema);
2153 dependencies.insert(object_type_name.clone());
2154
2155 return Ok(SchemaType::Reference {
2157 target: object_type_name,
2158 });
2159 }
2160 _ => {
2161 return Ok(SchemaType::Primitive {
2162 rust_type: "serde_json::Value".to_string(),
2163 serde_with: None,
2164 });
2165 }
2166 }
2167 }
2168
2169 if schema.is_nullable_pattern() {
2171 if let Some(non_null) = schema.non_null_variant() {
2172 return self.analyze_property_schema_with_context(
2173 non_null,
2174 property_name,
2175 dependencies,
2176 );
2177 }
2178 }
2179
2180 if self.should_use_dynamic_json(schema) {
2182 return Ok(SchemaType::Primitive {
2183 rust_type: "serde_json::Value".to_string(),
2184 serde_with: None,
2185 });
2186 }
2187
2188 if let Schema::AllOf { all_of, .. } = schema {
2190 return self.analyze_allof_composition(all_of, dependencies);
2191 }
2192
2193 if let Some(variants) = schema.union_variants() {
2195 match variants.len().cmp(&1) {
2196 std::cmp::Ordering::Equal => {
2197 return self.analyze_property_schema_with_context(
2199 &variants[0],
2200 property_name,
2201 dependencies,
2202 );
2203 }
2204 std::cmp::Ordering::Greater => {
2205 let union_name = if let Some(prop_name) = property_name {
2208 let prop_pascal = self.to_pascal_case(prop_name);
2210 format!(
2211 "{}{}",
2212 self.current_schema_name.as_deref().unwrap_or(""),
2213 prop_pascal
2214 )
2215 } else {
2216 "UnionType".to_string()
2217 };
2218
2219 if let Schema::OneOf {
2221 one_of,
2222 discriminator,
2223 ..
2224 } = schema
2225 {
2226 let oneof_result = self.analyze_oneof_union(
2228 one_of,
2229 discriminator.as_ref(),
2230 &union_name,
2231 dependencies,
2232 )?;
2233
2234 if let SchemaType::Union {
2236 variants: _union_variants,
2237 } = &oneof_result
2238 {
2239 self.resolved_cache.insert(
2241 union_name.clone(),
2242 AnalyzedSchema {
2243 name: union_name.clone(),
2244 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2245 schema_type: oneof_result.clone(),
2246 dependencies: dependencies.clone(),
2247 nullable: false,
2248 description: schema.details().description.clone(),
2249 default: None,
2250 },
2251 );
2252
2253 dependencies.insert(union_name.clone());
2255 return Ok(SchemaType::Reference { target: union_name });
2256 }
2257
2258 return Ok(oneof_result);
2259 } else if let Schema::AnyOf {
2260 any_of,
2261 discriminator,
2262 ..
2263 } = schema
2264 {
2265 let union_analysis = self.analyze_anyof_union(
2267 any_of,
2268 discriminator.as_ref(),
2269 dependencies,
2270 &union_name,
2271 )?;
2272 return Ok(union_analysis);
2273 } else {
2274 let mut union_variants = Vec::new();
2277 for variant in variants {
2278 if let Some(ref_str) = variant.reference() {
2279 if let Some(target) = self.extract_schema_name(ref_str) {
2280 dependencies.insert(target.to_string());
2281 union_variants.push(SchemaRef {
2282 target: target.to_string(),
2283 nullable: false,
2284 });
2285 }
2286 }
2287 }
2288 return Ok(SchemaType::Union {
2289 variants: union_variants,
2290 });
2291 }
2292 }
2293 std::cmp::Ordering::Less => {}
2294 }
2295 }
2296
2297 if let Some(inferred_type) = schema.inferred_type() {
2299 match inferred_type {
2300 OpenApiSchemaType::Object => {
2301 if self.should_use_dynamic_json(schema) {
2303 return Ok(SchemaType::Primitive {
2304 rust_type: "serde_json::Value".to_string(),
2305 serde_with: None,
2306 });
2307 }
2308 return self.analyze_object_schema(schema, dependencies);
2309 }
2310 OpenApiSchemaType::Array => {
2311 let context_name = if let Some(prop_name) = property_name {
2312 let prop_pascal = self.to_pascal_case(prop_name);
2314 format!(
2315 "{}{}",
2316 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2317 prop_pascal
2318 )
2319 } else {
2320 "ArrayItem".to_string()
2322 };
2323 return self.analyze_array_schema(schema, &context_name, dependencies);
2324 }
2325 OpenApiSchemaType::String => {
2326 if let Some(enum_values) = schema.details().string_enum_values() {
2327 return Ok(SchemaType::StringEnum {
2328 values: enum_values,
2329 });
2330 } else {
2331 return Ok(SchemaType::Primitive {
2332 rust_type: "String".to_string(),
2333 serde_with: None,
2334 });
2335 }
2336 }
2337 _ => {
2338 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2340 return Ok(SchemaType::Primitive {
2341 rust_type,
2342 serde_with: None,
2343 });
2344 }
2345 }
2346 }
2347
2348 Ok(SchemaType::Primitive {
2349 rust_type: "serde_json::Value".to_string(),
2350 serde_with: None,
2351 })
2352 }
2353
2354 fn analyze_allof_composition(
2355 &mut self,
2356 all_of_schemas: &[Schema],
2357 dependencies: &mut HashSet<String>,
2358 ) -> Result<SchemaType> {
2359 if all_of_schemas.len() == 1 {
2362 if let Schema::Reference { reference, .. } = &all_of_schemas[0] {
2363 if let Some(target) = self.extract_schema_name(reference) {
2364 dependencies.insert(target.to_string());
2365 return Ok(SchemaType::Reference {
2366 target: target.to_string(),
2367 });
2368 }
2369 }
2370 }
2371
2372 let mut merged_properties = BTreeMap::new();
2374 let mut merged_required = HashSet::new();
2375 let mut descriptions = Vec::new();
2376
2377 let current_context = self.current_schema_name.clone();
2379
2380 for schema in all_of_schemas {
2381 match schema {
2382 Schema::Reference { reference, .. } => {
2383 if let Some(target) = self.extract_schema_name(reference) {
2385 dependencies.insert(target.to_string());
2386
2387 let analyzed_ref = self.analyze_schema(target)?;
2389
2390 match &analyzed_ref.schema_type {
2392 SchemaType::Object {
2393 properties,
2394 required,
2395 ..
2396 } => {
2397 for (prop_name, prop_info) in properties {
2399 merged_properties.insert(prop_name.clone(), prop_info.clone());
2400 }
2401 for req in required {
2403 merged_required.insert(req.clone());
2404 }
2405 }
2406 _ => {
2407 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2409 self.merge_schema_into_properties(
2410 &ref_schema,
2411 &mut merged_properties,
2412 &mut merged_required,
2413 dependencies,
2414 )?;
2415 }
2416 }
2417 }
2418 }
2419 }
2420 Schema::Typed {
2421 schema_type: OpenApiSchemaType::Object,
2422 ..
2423 }
2424 | Schema::Untyped { .. } => {
2425 let saved_context = self.current_schema_name.clone();
2427 self.current_schema_name = current_context.clone();
2428
2429 self.merge_schema_into_properties(
2431 schema,
2432 &mut merged_properties,
2433 &mut merged_required,
2434 dependencies,
2435 )?;
2436
2437 self.current_schema_name = saved_context;
2439 }
2440 _ => {
2441 self.merge_schema_into_properties(
2444 schema,
2445 &mut merged_properties,
2446 &mut merged_required,
2447 dependencies,
2448 )?;
2449 }
2450 }
2451
2452 if let Some(desc) = &schema.details().description {
2454 descriptions.push(desc.clone());
2455 }
2456 }
2457
2458 if !merged_properties.is_empty() {
2460 Ok(SchemaType::Object {
2461 properties: merged_properties,
2462 required: merged_required,
2463 additional_properties: ObjectAdditionalProperties::Forbidden,
2464 })
2465 } else {
2466 Ok(SchemaType::Composition {
2468 schemas: all_of_schemas
2469 .iter()
2470 .filter_map(|s| {
2471 if let Some(ref_str) = s.reference() {
2472 if let Some(target) = self.extract_schema_name(ref_str) {
2473 dependencies.insert(target.to_string());
2474 Some(SchemaRef {
2475 target: target.to_string(),
2476 nullable: false,
2477 })
2478 } else {
2479 None
2480 }
2481 } else {
2482 None
2483 }
2484 })
2485 .collect(),
2486 })
2487 }
2488 }
2489
2490 fn merge_schema_into_properties(
2491 &mut self,
2492 schema: &Schema,
2493 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2494 merged_required: &mut HashSet<String>,
2495 dependencies: &mut HashSet<String>,
2496 ) -> Result<()> {
2497 let details = schema.details();
2498
2499 if let Some(properties) = &details.properties {
2501 for (prop_name, prop_schema) in properties {
2502 let prop_type = self.analyze_property_schema_with_context(
2503 prop_schema,
2504 Some(prop_name),
2505 dependencies,
2506 )?;
2507 let prop_details = prop_schema.details();
2508
2509 let nullable = prop_details.is_nullable() || prop_schema.is_nullable_pattern();
2515 merged_properties.insert(
2516 prop_name.clone(),
2517 PropertyInfo {
2518 schema_type: prop_type,
2519 nullable,
2520 description: prop_details.description.clone(),
2521 default: prop_details.default.clone(),
2522 serde_attrs: Vec::new(),
2523 constraints: PropertyConstraints::from_schema_details(prop_details),
2524 },
2525 );
2526 }
2527 }
2528
2529 if let Some(required) = &details.required {
2531 for field in required {
2532 merged_required.insert(field.clone());
2533 }
2534 }
2535
2536 Ok(())
2537 }
2538
2539 fn analyze_oneof_union(
2540 &mut self,
2541 one_of_schemas: &[Schema],
2542 discriminator: Option<&crate::openapi::Discriminator>,
2543 parent_name: &str,
2544 dependencies: &mut HashSet<String>,
2545 ) -> Result<SchemaType> {
2546 if one_of_schemas.len() == 2 {
2549 let null_count = one_of_schemas
2550 .iter()
2551 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2552 .count();
2553 if null_count == 1 {
2554 if let Some(non_null) = one_of_schemas
2555 .iter()
2556 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2557 {
2558 return self
2559 .analyze_schema_value(non_null, parent_name)
2560 .map(|a| a.schema_type);
2561 }
2562 }
2563 }
2564
2565 if discriminator.is_none() {
2567 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2569 }
2570
2571 if one_of_schemas
2577 .iter()
2578 .any(|s| !self.branch_resolves_to_object(s))
2579 {
2580 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2581 }
2582
2583 let discriminator_field = discriminator
2585 .ok_or_else(|| {
2586 GeneratorError::InvalidDiscriminator(
2587 "expected discriminator after guard check".to_string(),
2588 )
2589 })?
2590 .property_name
2591 .clone();
2592
2593 let mut variants = Vec::new();
2594 let mut used_variant_names = std::collections::HashSet::new();
2595
2596 for variant_schema in one_of_schemas {
2597 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2599 Some((ref_str, false))
2600 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2601 Some((recursive_ref, true))
2602 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2603 if all_of.len() == 1 {
2605 if let Some(ref_str) = all_of[0].reference() {
2606 Some((ref_str, false))
2607 } else {
2608 all_of[0]
2609 .recursive_reference()
2610 .map(|recursive_ref| (recursive_ref, true))
2611 }
2612 } else {
2613 None
2614 }
2615 } else {
2616 None
2617 };
2618
2619 if let Some((ref_str, is_recursive)) = ref_info {
2620 let schema_name = if is_recursive && ref_str == "#" {
2621 self.find_recursive_anchor_schema()
2623 .or_else(|| self.current_schema_name.clone())
2624 .unwrap_or_else(|| "CompoundFilter".to_string())
2625 } else {
2626 self.extract_schema_name(ref_str)
2627 .map(|s| s.to_string())
2628 .unwrap_or_else(|| "UnknownRef".to_string())
2629 };
2630
2631 if !schema_name.is_empty() {
2632 dependencies.insert(schema_name.clone());
2633
2634 let discriminator_value = if let Some(disc) = discriminator {
2639 if let Some(mappings) = &disc.mapping {
2640 mappings
2643 .iter()
2644 .find(|(_, target_ref)| {
2645 target_ref.as_str() == ref_str
2647 || self
2648 .extract_schema_name(target_ref)
2649 .map(|s| s.to_string())
2650 == Some(schema_name.clone())
2651 })
2652 .map(|(key, _)| key.clone())
2653 .unwrap_or_else(|| {
2654 self.fallback_discriminator_value_for_field(
2655 &schema_name,
2656 &discriminator_field,
2657 )
2658 })
2659 } else {
2660 self.fallback_discriminator_value_for_field(
2661 &schema_name,
2662 &discriminator_field,
2663 )
2664 }
2665 } else {
2666 self.fallback_discriminator_value_for_field(
2667 &schema_name,
2668 &discriminator_field,
2669 )
2670 };
2671
2672 let base_name = self.to_rust_variant_name(&schema_name);
2674 let rust_name =
2675 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2676
2677 let final_discriminator_value = discriminator_value;
2679
2680 variants.push(UnionVariant {
2681 rust_name,
2682 type_name: schema_name,
2683 discriminator_value: final_discriminator_value,
2684 schema_ref: ref_str.to_string(),
2685 });
2686 }
2687 } else {
2688 let variant_index = variants.len();
2690 let inline_type_name =
2691 self.generate_inline_type_name(variant_schema, variant_index);
2692
2693 let discriminator_value = if let Some(disc) = discriminator {
2695 if let Some(mappings) = &disc.mapping {
2696 mappings
2698 .iter()
2699 .find(|(_, target_ref)| {
2700 target_ref.contains(&format!("variant_{variant_index}"))
2701 })
2702 .map(|(key, _)| key.clone())
2703 .unwrap_or_else(|| {
2704 self.extract_inline_discriminator_value(
2705 variant_schema,
2706 &discriminator_field,
2707 variant_index,
2708 )
2709 })
2710 } else {
2711 self.extract_inline_discriminator_value(
2712 variant_schema,
2713 &discriminator_field,
2714 variant_index,
2715 )
2716 }
2717 } else {
2718 self.extract_inline_discriminator_value(
2719 variant_schema,
2720 &discriminator_field,
2721 variant_index,
2722 )
2723 };
2724
2725 let base_name = if discriminator_value.starts_with("variant_") {
2727 format!("Variant{variant_index}")
2728 } else {
2729 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2731 self.to_rust_variant_name(&clean_name)
2732 };
2733 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2734
2735 let final_discriminator_value = discriminator_value;
2737
2738 variants.push(UnionVariant {
2739 rust_name,
2740 type_name: inline_type_name.clone(),
2741 discriminator_value: final_discriminator_value,
2742 schema_ref: format!("inline_{variant_index}"),
2743 });
2744
2745 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2747 }
2748 }
2749
2750 if variants.is_empty() {
2751 let mut union_variants = Vec::new();
2754
2755 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2756 if let Some(ref_str) = variant_schema.reference() {
2758 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2759 dependencies.insert(schema_name.to_string());
2760 union_variants.push(SchemaRef {
2761 target: schema_name.to_string(),
2762 nullable: false,
2763 });
2764 }
2765 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2766 let schema_name = if recursive_ref == "#" {
2767 self.find_recursive_anchor_schema()
2769 .or_else(|| self.current_schema_name.clone())
2770 .unwrap_or_else(|| "CompoundFilter".to_string())
2771 } else {
2772 self.extract_schema_name(recursive_ref)
2773 .map(|s| s.to_string())
2774 .unwrap_or_else(|| "RecursiveType".to_string())
2775 };
2776 dependencies.insert(schema_name.clone());
2777 union_variants.push(SchemaRef {
2778 target: schema_name,
2779 nullable: false,
2780 });
2781 } else {
2782 let inline_name = self.generate_context_aware_name(
2784 parent_name,
2785 "InlineVariant",
2786 variant_index,
2787 Some(variant_schema),
2788 );
2789 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2790 let variant_type = analyzed.schema_type;
2791
2792 for dep in &analyzed.dependencies {
2794 dependencies.insert(dep.clone());
2795 }
2796
2797 match &variant_type {
2798 SchemaType::Primitive { rust_type, .. } => {
2800 union_variants.push(SchemaRef {
2801 target: rust_type.clone(),
2802 nullable: false,
2803 });
2804 }
2805 SchemaType::Array { item_type } => {
2807 match item_type.as_ref() {
2808 SchemaType::Primitive { rust_type, .. } => {
2809 let type_name = format!("Vec<{rust_type}>");
2810 union_variants.push(SchemaRef {
2811 target: type_name,
2812 nullable: false,
2813 });
2814 }
2815 SchemaType::Reference { target } => {
2816 let type_name = format!("Vec<{target}>");
2817 union_variants.push(SchemaRef {
2818 target: type_name,
2819 nullable: false,
2820 });
2821 }
2822 _ => {
2823 let inline_type_name = self.generate_context_aware_name(
2825 parent_name,
2826 "Variant",
2827 variant_index,
2828 None,
2829 );
2830 self.add_inline_schema(
2831 &inline_type_name,
2832 variant_schema,
2833 dependencies,
2834 )?;
2835 union_variants.push(SchemaRef {
2836 target: inline_type_name,
2837 nullable: false,
2838 });
2839 }
2840 }
2841 }
2842 SchemaType::Reference { target } => {
2844 union_variants.push(SchemaRef {
2845 target: target.clone(),
2846 nullable: false,
2847 });
2848 }
2849 _ => {
2851 let inline_type_name =
2852 format!("{}Variant{}", parent_name, variant_index + 1);
2853 self.add_inline_schema(
2854 &inline_type_name,
2855 variant_schema,
2856 dependencies,
2857 )?;
2858 union_variants.push(SchemaRef {
2859 target: inline_type_name,
2860 nullable: false,
2861 });
2862 }
2863 }
2864 }
2865 }
2866
2867 if !union_variants.is_empty() {
2868 return Ok(SchemaType::Union {
2869 variants: union_variants,
2870 });
2871 }
2872
2873 return Ok(SchemaType::Primitive {
2875 rust_type: "serde_json::Value".to_string(),
2876 serde_with: None,
2877 });
2878 }
2879
2880 Ok(SchemaType::DiscriminatedUnion {
2881 discriminator_field,
2882 variants,
2883 })
2884 }
2885
2886 fn analyze_untagged_oneof_union(
2887 &mut self,
2888 one_of_schemas: &[Schema],
2889 parent_name: &str,
2890 dependencies: &mut HashSet<String>,
2891 ) -> Result<SchemaType> {
2892 let filtered: Vec<&Schema> = one_of_schemas
2896 .iter()
2897 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2898 .collect();
2899
2900 if filtered.len() == 1 {
2902 return self
2903 .analyze_schema_value(filtered[0], parent_name)
2904 .map(|a| a.schema_type);
2905 }
2906
2907 let mut union_variants = Vec::new();
2908
2909 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
2910 if let Some(ref_str) = variant_schema.reference() {
2912 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2913 dependencies.insert(schema_name.to_string());
2914 union_variants.push(SchemaRef {
2915 target: schema_name.to_string(),
2916 nullable: false,
2917 });
2918 }
2919 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2920 let schema_name = if recursive_ref == "#" {
2921 self.find_recursive_anchor_schema()
2923 .or_else(|| self.current_schema_name.clone())
2924 .unwrap_or_else(|| "CompoundFilter".to_string())
2925 } else {
2926 self.extract_schema_name(recursive_ref)
2927 .map(|s| s.to_string())
2928 .unwrap_or_else(|| "RecursiveType".to_string())
2929 };
2930 dependencies.insert(schema_name.clone());
2931 union_variants.push(SchemaRef {
2932 target: schema_name,
2933 nullable: false,
2934 });
2935 } else {
2936 let inline_name = self.generate_context_aware_name(
2938 parent_name,
2939 "InlineVariant",
2940 variant_index,
2941 Some(variant_schema),
2942 );
2943 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2944 let variant_type = analyzed.schema_type;
2945
2946 for dep in &analyzed.dependencies {
2948 dependencies.insert(dep.clone());
2949 }
2950
2951 match &variant_type {
2952 SchemaType::Primitive { rust_type, .. } => {
2954 union_variants.push(SchemaRef {
2955 target: rust_type.clone(),
2956 nullable: false,
2957 });
2958 }
2959 SchemaType::Array { item_type } => {
2961 match item_type.as_ref() {
2962 SchemaType::Primitive { rust_type, .. } => {
2963 let type_name = format!("Vec<{rust_type}>");
2964 union_variants.push(SchemaRef {
2965 target: type_name,
2966 nullable: false,
2967 });
2968 }
2969 SchemaType::Reference { target } => {
2970 let type_name = format!("Vec<{target}>");
2971 union_variants.push(SchemaRef {
2972 target: type_name,
2973 nullable: false,
2974 });
2975 }
2976 SchemaType::Array {
2978 item_type: inner_item_type,
2979 } => {
2980 match inner_item_type.as_ref() {
2981 SchemaType::Primitive { rust_type, .. } => {
2982 let type_name = format!("Vec<Vec<{rust_type}>>");
2983 union_variants.push(SchemaRef {
2984 target: type_name,
2985 nullable: false,
2986 });
2987 }
2988 SchemaType::Reference { target } => {
2989 let type_name = format!("Vec<Vec<{target}>>");
2990 union_variants.push(SchemaRef {
2991 target: type_name,
2992 nullable: false,
2993 });
2994 }
2995 _ => {
2996 let inline_type_name = self.generate_context_aware_name(
2998 parent_name,
2999 "Variant",
3000 variant_index,
3001 None,
3002 );
3003 self.add_inline_schema(
3004 &inline_type_name,
3005 variant_schema,
3006 dependencies,
3007 )?;
3008 union_variants.push(SchemaRef {
3009 target: inline_type_name,
3010 nullable: false,
3011 });
3012 }
3013 }
3014 }
3015 _ => {
3016 let inline_type_name = self.generate_context_aware_name(
3018 parent_name,
3019 "Variant",
3020 variant_index,
3021 None,
3022 );
3023 self.add_inline_schema(
3024 &inline_type_name,
3025 variant_schema,
3026 dependencies,
3027 )?;
3028 union_variants.push(SchemaRef {
3029 target: inline_type_name,
3030 nullable: false,
3031 });
3032 }
3033 }
3034 }
3035 SchemaType::Reference { target } => {
3037 union_variants.push(SchemaRef {
3038 target: target.clone(),
3039 nullable: false,
3040 });
3041 }
3042 _ => {
3044 let inline_type_name = self.generate_context_aware_name(
3045 parent_name,
3046 "Variant",
3047 variant_index,
3048 None,
3049 );
3050 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3051 union_variants.push(SchemaRef {
3052 target: inline_type_name,
3053 nullable: false,
3054 });
3055 }
3056 }
3057 }
3058 }
3059
3060 if !union_variants.is_empty() {
3061 return Ok(SchemaType::Union {
3062 variants: union_variants,
3063 });
3064 }
3065
3066 Ok(SchemaType::Primitive {
3068 rust_type: "serde_json::Value".to_string(),
3069 serde_with: None,
3070 })
3071 }
3072
3073 fn add_inline_schema(
3074 &mut self,
3075 type_name: &str,
3076 schema: &Schema,
3077 dependencies: &mut HashSet<String>,
3078 ) -> Result<()> {
3079 if let Some(schema_type) = schema.schema_type() {
3081 match schema_type {
3082 OpenApiSchemaType::String
3083 | OpenApiSchemaType::Integer
3084 | OpenApiSchemaType::Number
3085 | OpenApiSchemaType::Boolean => {
3086 let rust_type =
3087 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3088
3089 self.resolved_cache.insert(
3091 type_name.to_string(),
3092 AnalyzedSchema {
3093 name: type_name.to_string(),
3094 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3095 schema_type: SchemaType::Primitive {
3096 rust_type,
3097 serde_with: None,
3098 },
3099 dependencies: HashSet::new(),
3100 nullable: false,
3101 description: schema.details().description.clone(),
3102 default: None,
3103 },
3104 );
3105 return Ok(());
3106 }
3107 _ => {}
3108 }
3109 }
3110
3111 let previous_schema_name = self.current_schema_name.take();
3115 self.current_schema_name = Some(type_name.to_string());
3116 let analyzed = self.analyze_schema_value(schema, type_name)?;
3117 self.current_schema_name = previous_schema_name;
3118
3119 self.resolved_cache.insert(type_name.to_string(), analyzed);
3121
3122 if let Some(cached) = self.resolved_cache.get(type_name) {
3124 for dep in &cached.dependencies {
3125 dependencies.insert(dep.clone());
3126 }
3127 }
3128
3129 Ok(())
3130 }
3131
3132 fn extract_inline_discriminator_value(
3133 &self,
3134 schema: &Schema,
3135 discriminator_field: &str,
3136 variant_index: usize,
3137 ) -> String {
3138 if let Some(properties) = &schema.details().properties {
3140 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3141 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3143 if enum_values.len() == 1 {
3144 if let Some(value) = enum_values[0].as_str() {
3145 return value.to_string();
3146 }
3147 }
3148 }
3149 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3151 if let Some(value) = const_value.as_str() {
3152 return value.to_string();
3153 }
3154 }
3155 if let Some(const_value) = &discriminator_prop.details().const_value {
3157 if let Some(value) = const_value.as_str() {
3158 return value.to_string();
3159 }
3160 }
3161 }
3162 }
3163
3164 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3166 return inferred_name;
3167 }
3168
3169 format!("variant_{variant_index}")
3171 }
3172
3173 fn infer_variant_name_from_structure(
3174 &self,
3175 schema: &Schema,
3176 _variant_index: usize,
3177 ) -> Option<String> {
3178 let details = schema.details();
3179
3180 if let Some(properties) = &details.properties {
3182 if properties.contains_key("text") && properties.len() <= 3 {
3184 return Some("text".to_string());
3185 }
3186 if properties.contains_key("image") || properties.contains_key("source") {
3187 return Some("image".to_string());
3188 }
3189 if properties.contains_key("document") {
3190 return Some("document".to_string());
3191 }
3192 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3193 return Some("tool_result".to_string());
3194 }
3195 if properties.contains_key("content") && properties.contains_key("is_error") {
3196 return Some("tool_result".to_string());
3197 }
3198 if properties.contains_key("partial_json") {
3199 return Some("partial_json".to_string());
3200 }
3201
3202 let property_names: Vec<&String> = properties.keys().collect();
3204
3205 for prop_name in &property_names {
3207 if prop_name.contains("result") {
3208 return Some("result".to_string());
3209 }
3210 if prop_name.contains("error") {
3211 return Some("error".to_string());
3212 }
3213 if prop_name.contains("content") && property_names.len() <= 2 {
3214 return Some("content".to_string());
3215 }
3216 }
3217
3218 let significant_props = property_names
3220 .iter()
3221 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3222 .collect::<Vec<_>>();
3223
3224 if significant_props.len() == 1 {
3225 return Some((*significant_props[0]).clone());
3226 }
3227 }
3228
3229 if let Some(description) = &details.description {
3231 let desc_lower = description.to_lowercase();
3232 if desc_lower.contains("text") && desc_lower.len() < 100 {
3233 return Some("text".to_string());
3234 }
3235 if desc_lower.contains("image") {
3236 return Some("image".to_string());
3237 }
3238 if desc_lower.contains("document") {
3239 return Some("document".to_string());
3240 }
3241 if desc_lower.contains("tool") && desc_lower.contains("result") {
3242 return Some("tool_result".to_string());
3243 }
3244 }
3245
3246 None
3247 }
3248
3249 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3250 if discriminator.is_empty() {
3252 return "Variant".to_string();
3253 }
3254
3255 let mut result = String::new();
3256 let mut next_upper = true;
3257
3258 for c in discriminator.chars() {
3259 match c {
3260 'a'..='z' => {
3261 if next_upper {
3262 result.push(c.to_ascii_uppercase());
3263 next_upper = false;
3264 } else {
3265 result.push(c);
3266 }
3267 }
3268 'A'..='Z' => {
3269 result.push(c);
3270 next_upper = false;
3271 }
3272 '0'..='9' => {
3273 result.push(c);
3274 next_upper = false;
3275 }
3276 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3277 next_upper = true;
3279 }
3280 _ => {
3281 next_upper = true;
3283 }
3284 }
3285 }
3286
3287 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3289 result = format!("Variant{result}");
3290 }
3291
3292 result
3293 }
3294
3295 fn ensure_unique_variant_name(
3296 &self,
3297 base_name: String,
3298 used_names: &mut std::collections::HashSet<String>,
3299 ) -> String {
3300 let mut candidate = base_name.clone();
3301 let mut counter = 1;
3302
3303 while used_names.contains(&candidate) {
3304 counter += 1;
3305 candidate = format!("{base_name}{counter}");
3306 }
3307
3308 used_names.insert(candidate.clone());
3309 candidate
3310 }
3311
3312 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3313 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3315 return meaningful_name;
3316 }
3317
3318 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3320 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3321 }
3322
3323 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3324 let details = schema.details();
3325
3326 if let Some(description) = &details.description {
3328 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3329 return Some(name_from_desc);
3330 }
3331 }
3332
3333 if let Some(properties) = &details.properties {
3335 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3336 return Some(format!("{name_from_props}Block"));
3337 }
3338 }
3339
3340 None
3341 }
3342
3343 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3344 if description.len() > 100 || description.contains('\n') {
3346 return None;
3347 }
3348
3349 let words: Vec<&str> = description
3351 .split_whitespace()
3352 .take(2) .filter(|word| {
3354 let w = word.to_lowercase();
3355 word.len() > 2
3356 && ![
3357 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3358 ]
3359 .contains(&w.as_str())
3360 })
3361 .collect();
3362
3363 if words.is_empty() {
3364 return None;
3365 }
3366
3367 let combined = words.join("_");
3369 let pascal_name = self.discriminator_to_variant_name(&combined);
3370
3371 if !pascal_name.ends_with("Content")
3373 && !pascal_name.ends_with("Block")
3374 && !pascal_name.ends_with("Type")
3375 {
3376 Some(format!("{pascal_name}Content"))
3377 } else {
3378 Some(pascal_name)
3379 }
3380 }
3381
3382 fn extract_type_name_from_properties(
3383 &self,
3384 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3385 ) -> Option<String> {
3386 let significant_props: Vec<&String> = properties
3388 .keys()
3389 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3390 .collect();
3391
3392 if significant_props.is_empty() {
3393 return None;
3394 }
3395
3396 if significant_props.len() == 1 {
3398 let prop_name = significant_props[0];
3399 return Some(self.discriminator_to_variant_name(prop_name));
3400 }
3401
3402 let mut sorted_props = significant_props.clone();
3405 sorted_props.sort();
3406 if let Some(first_prop) = sorted_props.first() {
3407 return Some(self.discriminator_to_variant_name(first_prop));
3408 }
3409
3410 None
3411 }
3412
3413 fn openapi_type_to_rust_type(
3414 &self,
3415 openapi_type: OpenApiSchemaType,
3416 details: &crate::openapi::SchemaDetails,
3417 ) -> String {
3418 self.type_mapper.map(openapi_type, details).rust_type
3423 }
3424
3425 #[allow(dead_code)]
3426 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3427 self.fallback_discriminator_value_for_field(schema_name, "type")
3428 }
3429
3430 fn fallback_discriminator_value_for_field(
3431 &self,
3432 schema_name: &str,
3433 field_name: &str,
3434 ) -> String {
3435 if let Some(ref_schema) = self.schemas.get(schema_name) {
3437 if let Some(extracted) =
3438 self.extract_discriminator_value_for_field(ref_schema, field_name)
3439 {
3440 return extracted;
3441 }
3442 }
3443
3444 self.generate_discriminator_value_from_name(schema_name)
3446 }
3447
3448 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3449 let mut result = String::new();
3451 let mut chars = schema_name.chars().peekable();
3452 let mut first = true;
3453
3454 while let Some(c) = chars.next() {
3455 if c.is_uppercase()
3456 && !first
3457 && chars
3458 .peek()
3459 .map(|&next| next.is_lowercase())
3460 .unwrap_or(false)
3461 {
3462 result.push('.');
3463 }
3464 result.push(c.to_ascii_lowercase());
3465 first = false;
3466 }
3467
3468 if result.ends_with("event") {
3470 result = result[..result.len() - 5].to_string();
3471 }
3472
3473 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3475 result = format!("response.{}", result.trim_start_matches("response"));
3476 }
3477
3478 result
3479 }
3480
3481 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3482 let mut name = schema_name;
3484
3485 if name.starts_with("Response") && name.len() > 8 {
3487 name = &name[8..]; }
3489
3490 if name.ends_with("Event") && name.len() > 5 {
3492 name = &name[..name.len() - 5]; }
3494
3495 name = name.trim_matches('_');
3497
3498 if name.is_empty() {
3500 schema_name.to_string()
3501 } else {
3502 self.discriminator_to_variant_name(name)
3504 }
3505 }
3506
3507 fn hoist_inline_string_enum(
3531 &mut self,
3532 schema: &Schema,
3533 enum_values: Vec<String>,
3534 primary_name: String,
3535 dependencies: &mut HashSet<String>,
3536 ) -> SchemaType {
3537 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3538 matches!(
3539 &existing.schema_type,
3540 SchemaType::StringEnum { values: existing_values }
3541 if existing_values == values
3542 )
3543 }
3544
3545 let mut enum_type_name = primary_name.clone();
3546 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3547 None => true,
3548 Some(existing) if matches_values(existing, &enum_values) => false,
3549 Some(_) => {
3550 let suffix = enum_values
3553 .first()
3554 .map(|v| self.to_pascal_case(v))
3555 .unwrap_or_else(|| "Variant".to_string());
3556 let candidate = format!("{primary_name}{suffix}");
3557
3558 let resolved = match self.resolved_cache.get(&candidate) {
3559 None => Some((candidate.clone(), true)),
3560 Some(existing) if matches_values(existing, &enum_values) => {
3561 Some((candidate.clone(), false))
3562 }
3563 Some(_) => {
3564 let mut found = None;
3567 for n in 2..1000 {
3568 let numbered = format!("{candidate}_{n}");
3569 match self.resolved_cache.get(&numbered) {
3570 None => {
3571 found = Some((numbered, true));
3572 break;
3573 }
3574 Some(existing) if matches_values(existing, &enum_values) => {
3575 found = Some((numbered, false));
3576 break;
3577 }
3578 Some(_) => continue,
3579 }
3580 }
3581 found
3582 }
3583 };
3584
3585 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3586 enum_type_name = resolved_name;
3587 insert
3588 }
3589 };
3590
3591 if should_insert {
3594 self.resolved_cache.insert(
3595 enum_type_name.clone(),
3596 AnalyzedSchema {
3597 name: enum_type_name.clone(),
3598 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3599 schema_type: SchemaType::StringEnum {
3600 values: enum_values,
3601 },
3602 dependencies: HashSet::new(),
3603 nullable: false,
3604 description: schema.details().description.clone(),
3605 default: schema.details().default.clone(),
3606 },
3607 );
3608 }
3609
3610 dependencies.insert(enum_type_name.clone());
3612 SchemaType::Reference {
3613 target: enum_type_name,
3614 }
3615 }
3616
3617 fn analyze_array_schema(
3618 &mut self,
3619 schema: &Schema,
3620 parent_schema_name: &str,
3621 dependencies: &mut HashSet<String>,
3622 ) -> Result<SchemaType> {
3623 let details = schema.details();
3624
3625 if let Some(items_schema) = &details.items {
3627 let item_type = match items_schema.as_ref() {
3629 Schema::Reference { reference, .. } => {
3630 let target = self
3632 .extract_schema_name(reference)
3633 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3634 .to_string();
3635 dependencies.insert(target.clone());
3636 SchemaType::Reference { target }
3637 }
3638 Schema::RecursiveRef { recursive_ref, .. } => {
3639 if recursive_ref == "#" {
3641 let target = self
3643 .find_recursive_anchor_schema()
3644 .unwrap_or_else(|| parent_schema_name.to_string());
3645 dependencies.insert(target.clone());
3646 SchemaType::Reference { target }
3647 } else {
3648 let target = self
3649 .extract_schema_name(recursive_ref)
3650 .unwrap_or("RecursiveType")
3651 .to_string();
3652 dependencies.insert(target.clone());
3653 SchemaType::Reference { target }
3654 }
3655 }
3656 Schema::Typed { schema_type, .. } => {
3657 match schema_type {
3659 OpenApiSchemaType::String => {
3660 match items_schema
3664 .details()
3665 .string_enum_values()
3666 .filter(|values| !values.is_empty())
3667 {
3668 Some(values) => self.hoist_inline_string_enum(
3669 items_schema,
3670 values,
3671 format!("{parent_schema_name}Item"),
3672 dependencies,
3673 ),
3674 None => SchemaType::Primitive {
3675 rust_type: "String".to_string(),
3676 serde_with: None,
3677 },
3678 }
3679 }
3680 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3681 let details = items_schema.details();
3682 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3683 SchemaType::Primitive {
3684 rust_type,
3685 serde_with: None,
3686 }
3687 }
3688 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3689 rust_type: "bool".to_string(),
3690 serde_with: None,
3691 },
3692 OpenApiSchemaType::Object => {
3693 let object_type_name = format!("{parent_schema_name}Item");
3695
3696 let object_type =
3698 self.analyze_object_schema(items_schema, dependencies)?;
3699
3700 let inline_schema = AnalyzedSchema {
3702 name: object_type_name.clone(),
3703 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3704 schema_type: object_type,
3705 dependencies: dependencies.clone(),
3706 nullable: false,
3707 description: items_schema.details().description.clone(),
3708 default: None,
3709 };
3710
3711 self.resolved_cache
3713 .insert(object_type_name.clone(), inline_schema);
3714 dependencies.insert(object_type_name.clone());
3715
3716 SchemaType::Reference {
3718 target: object_type_name,
3719 }
3720 }
3721 OpenApiSchemaType::Array => {
3722 self.analyze_array_schema(
3724 items_schema,
3725 parent_schema_name,
3726 dependencies,
3727 )?
3728 }
3729 _ => SchemaType::Primitive {
3730 rust_type: "serde_json::Value".to_string(),
3731 serde_with: None,
3732 },
3733 }
3734 }
3735 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3736 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3738
3739 match &analyzed.schema_type {
3741 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3742 let union_name = format!("{parent_schema_name}ItemUnion");
3745
3746 let mut union_schema = analyzed;
3748 union_schema.name = union_name.clone();
3749
3750 self.resolved_cache.insert(union_name.clone(), union_schema);
3752
3753 dependencies.insert(union_name.clone());
3755
3756 SchemaType::Reference { target: union_name }
3758 }
3759 _ => analyzed.schema_type,
3760 }
3761 }
3762 Schema::Untyped { .. } => {
3763 if let Some(inferred) = items_schema.inferred_type() {
3765 match inferred {
3766 OpenApiSchemaType::Object => {
3767 let object_type_name = format!("{parent_schema_name}Item");
3769
3770 let object_type =
3772 self.analyze_object_schema(items_schema, dependencies)?;
3773
3774 let inline_schema = AnalyzedSchema {
3776 name: object_type_name.clone(),
3777 original: serde_json::to_value(items_schema)
3778 .unwrap_or(Value::Null),
3779 schema_type: object_type,
3780 dependencies: dependencies.clone(),
3781 nullable: false,
3782 description: items_schema.details().description.clone(),
3783 default: None,
3784 };
3785
3786 self.resolved_cache
3788 .insert(object_type_name.clone(), inline_schema);
3789 dependencies.insert(object_type_name.clone());
3790
3791 SchemaType::Reference {
3793 target: object_type_name,
3794 }
3795 }
3796 OpenApiSchemaType::String => {
3797 match items_schema
3800 .details()
3801 .string_enum_values()
3802 .filter(|values| !values.is_empty())
3803 {
3804 Some(values) => self.hoist_inline_string_enum(
3805 items_schema,
3806 values,
3807 format!("{parent_schema_name}Item"),
3808 dependencies,
3809 ),
3810 None => SchemaType::Primitive {
3811 rust_type: "String".to_string(),
3812 serde_with: None,
3813 },
3814 }
3815 }
3816 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3817 let details = items_schema.details();
3818 let rust_type = self.get_number_rust_type(inferred, details);
3819 SchemaType::Primitive {
3820 rust_type,
3821 serde_with: None,
3822 }
3823 }
3824 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3825 rust_type: "bool".to_string(),
3826 serde_with: None,
3827 },
3828 _ => SchemaType::Primitive {
3829 rust_type: "serde_json::Value".to_string(),
3830 serde_with: None,
3831 },
3832 }
3833 } else {
3834 SchemaType::Primitive {
3835 rust_type: "serde_json::Value".to_string(),
3836 serde_with: None,
3837 }
3838 }
3839 }
3840 _ => SchemaType::Primitive {
3841 rust_type: "serde_json::Value".to_string(),
3842 serde_with: None,
3843 },
3844 };
3845
3846 Ok(SchemaType::Array {
3847 item_type: Box::new(item_type),
3848 })
3849 } else {
3850 Ok(SchemaType::Primitive {
3852 rust_type: "Vec<serde_json::Value>".to_string(),
3853 serde_with: None,
3854 })
3855 }
3856 }
3857
3858 fn get_number_rust_type(
3859 &self,
3860 schema_type: OpenApiSchemaType,
3861 details: &crate::openapi::SchemaDetails,
3862 ) -> String {
3863 let format = details.format.as_deref();
3867 match schema_type {
3868 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
3869 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
3870 _ => self.type_mapper.dynamic_json().rust_type,
3871 }
3872 }
3873
3874 fn analyze_anyof_union(
3875 &mut self,
3876 any_of_schemas: &[Schema],
3877 discriminator: Option<&Discriminator>,
3878 dependencies: &mut HashSet<String>,
3879 context_name: &str,
3880 ) -> Result<SchemaType> {
3881 let filtered_owned: Vec<Schema>;
3886 let any_of_schemas: &[Schema] = if any_of_schemas
3887 .iter()
3888 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3889 {
3890 filtered_owned = any_of_schemas
3891 .iter()
3892 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3893 .cloned()
3894 .collect();
3895 if filtered_owned.is_empty() {
3896 return Ok(SchemaType::Primitive {
3897 rust_type: "serde_json::Value".to_string(),
3898 serde_with: None,
3899 });
3900 }
3901 if filtered_owned.len() == 1 {
3902 return self
3903 .analyze_schema_value(&filtered_owned[0], context_name)
3904 .map(|a| a.schema_type);
3905 }
3906 &filtered_owned
3907 } else {
3908 any_of_schemas
3909 };
3910
3911 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
3913 let has_objects = any_of_schemas.iter().any(|s| {
3914 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
3915 || s.inferred_type() == Some(OpenApiSchemaType::Object)
3916 });
3917 let has_arrays = any_of_schemas
3918 .iter()
3919 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
3920
3921 let all_string_like = any_of_schemas.iter().all(|s| {
3924 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
3925 || s.details().const_value.is_some()
3926 });
3927
3928 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
3929 if let Some(disc) = discriminator {
3931 return self.analyze_oneof_union(
3933 any_of_schemas,
3934 Some(disc),
3935 context_name,
3936 dependencies,
3937 );
3938 }
3939
3940 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
3942 return self.analyze_oneof_union(
3943 any_of_schemas,
3944 Some(&Discriminator {
3945 property_name: disc_field,
3946 mapping: None,
3947 default_mapping: None,
3948 extensions: crate::extensions::Extensions::default(),
3949 }),
3950 context_name,
3951 dependencies,
3952 );
3953 }
3954
3955 let mut variants = Vec::new();
3957
3958 for schema in any_of_schemas {
3959 if let Some(ref_str) = schema.reference() {
3960 if let Some(target) = self.extract_schema_name(ref_str) {
3961 dependencies.insert(target.to_string());
3962 variants.push(SchemaRef {
3963 target: target.to_string(),
3964 nullable: false,
3965 });
3966 }
3967 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
3968 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
3969 {
3970 let inline_index = variants.len();
3972 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
3973
3974 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
3976
3977 variants.push(SchemaRef {
3978 target: inline_type_name,
3979 nullable: false,
3980 });
3981 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
3982 let array_type =
3984 self.analyze_array_schema(schema, context_name, dependencies)?;
3985
3986 let array_type_name = if let Some(items_schema) = &schema.details().items {
3988 if let Some(ref_str) = items_schema.reference() {
3989 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
3990 dependencies.insert(item_type_name.to_string());
3991 format!("{item_type_name}Array")
3992 } else {
3993 self.generate_context_aware_name(
3994 context_name,
3995 "Array",
3996 variants.len(),
3997 Some(schema),
3998 )
3999 }
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
4017 self.resolved_cache.insert(
4019 array_type_name.clone(),
4020 AnalyzedSchema {
4021 name: array_type_name.clone(),
4022 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4023 schema_type: array_type,
4024 dependencies: HashSet::new(),
4025 nullable: false,
4026 description: Some("Array variant in union".to_string()),
4027 default: None,
4028 },
4029 );
4030
4031 dependencies.insert(array_type_name.clone());
4033
4034 variants.push(SchemaRef {
4035 target: array_type_name,
4036 nullable: false,
4037 });
4038 } else if let Some(schema_type) = schema.schema_type() {
4039 let primitive_unions = self
4049 .type_mapper
4050 .config_shape_primitive_unions()
4051 .unwrap_or(true);
4052
4053 if primitive_unions {
4054 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
4055 variants.push(SchemaRef {
4056 target: mapped.rust_type,
4057 nullable: false,
4058 });
4059 } else {
4060 let inline_index = variants.len();
4061 let inline_type_name = match schema_type {
4062 OpenApiSchemaType::String => {
4063 if inline_index == 0 {
4064 format!("{context_name}String")
4065 } else {
4066 format!("{context_name}StringVariant{inline_index}")
4067 }
4068 }
4069 OpenApiSchemaType::Number => {
4070 if inline_index == 0 {
4071 format!("{context_name}Number")
4072 } else {
4073 format!("{context_name}NumberVariant{inline_index}")
4074 }
4075 }
4076 OpenApiSchemaType::Integer => {
4077 if inline_index == 0 {
4078 format!("{context_name}Integer")
4079 } else {
4080 format!("{context_name}IntegerVariant{inline_index}")
4081 }
4082 }
4083 OpenApiSchemaType::Boolean => {
4084 if inline_index == 0 {
4085 format!("{context_name}Boolean")
4086 } else {
4087 format!("{context_name}BooleanVariant{inline_index}")
4088 }
4089 }
4090 _ => format!("{context_name}Variant{inline_index}"),
4091 };
4092
4093 let rust_type =
4094 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4095
4096 self.resolved_cache.insert(
4097 inline_type_name.clone(),
4098 AnalyzedSchema {
4099 name: inline_type_name.clone(),
4100 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4101 schema_type: SchemaType::Primitive {
4102 rust_type,
4103 serde_with: None,
4104 },
4105 dependencies: HashSet::new(),
4106 nullable: false,
4107 description: schema.details().description.clone(),
4108 default: None,
4109 },
4110 );
4111
4112 dependencies.insert(inline_type_name.clone());
4113
4114 variants.push(SchemaRef {
4115 target: inline_type_name,
4116 nullable: false,
4117 });
4118 }
4119 }
4120 }
4121
4122 if !variants.is_empty() {
4123 return Ok(SchemaType::Union { variants });
4124 }
4125 }
4126
4127 let all_strings = any_of_schemas.iter().all(|schema| {
4129 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4130 || schema.details().const_value.is_some()
4131 });
4132
4133 if all_strings {
4134 let mut enum_values = Vec::new();
4136 let mut has_open_string = false;
4137
4138 for schema in any_of_schemas {
4139 if let Some(const_val) = &schema.details().const_value {
4140 if let Some(const_str) = const_val.as_str() {
4141 enum_values.push(const_str.to_string());
4142 }
4143 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4144 has_open_string = true;
4145 }
4146 }
4147
4148 if !enum_values.is_empty() {
4149 if has_open_string {
4150 return Ok(SchemaType::ExtensibleEnum {
4153 known_values: enum_values,
4154 });
4155 } else {
4156 return Ok(SchemaType::StringEnum {
4158 values: enum_values,
4159 });
4160 }
4161 }
4162 }
4163
4164 Ok(SchemaType::Primitive {
4166 rust_type: "serde_json::Value".to_string(),
4167 serde_with: None,
4168 })
4169 }
4170
4171 fn find_recursive_anchor_schema(&self) -> Option<String> {
4173 for (schema_name, schema) in &self.schemas {
4175 let details = schema.details();
4176 if details.recursive_anchor == Some(true) {
4177 return Some(schema_name.clone());
4178 }
4179 }
4180
4181 None
4185 }
4186
4187 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4190 if let Schema::AnyOf { any_of, .. } = schema {
4192 if any_of.len() == 2 {
4193 let has_null = any_of
4194 .iter()
4195 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4196 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4197
4198 if has_null && has_empty_object {
4199 return true;
4200 }
4201 }
4202 }
4203
4204 self.is_dynamic_object_pattern(schema)
4206 }
4207
4208 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4210 let is_object = match schema.schema_type() {
4212 Some(OpenApiSchemaType::Object) => true,
4213 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4214 _ => false,
4215 };
4216
4217 if !is_object {
4218 return false;
4219 }
4220
4221 let details = schema.details();
4222
4223 if self.has_explicit_additional_properties(schema) {
4226 return false;
4227 }
4228
4229 let no_properties = details
4231 .properties
4232 .as_ref()
4233 .map(|props| props.is_empty())
4234 .unwrap_or(true);
4235
4236 if no_properties {
4237 let has_structural_constraints = details
4240 .required
4241 .as_ref()
4242 .map(|req| req.iter().any(|r| r != "type"))
4243 .unwrap_or(false)
4244 || details.pattern_properties.is_some()
4245 || details.property_names.is_some()
4246 || details.min_properties.is_some()
4247 || details.max_properties.is_some()
4248 || details.dependent_required.is_some()
4249 || details.dependent_schemas.is_some()
4250 || details.if_schema.is_some()
4251 || details.then_schema.is_some()
4252 || details.else_schema.is_some();
4253
4254 return !has_structural_constraints;
4255 }
4256
4257 false
4258 }
4259
4260 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4262 let details = schema.details();
4263
4264 matches!(
4266 &details.additional_properties,
4267 Some(crate::openapi::AdditionalProperties::Boolean(true))
4268 | Some(crate::openapi::AdditionalProperties::Schema(_))
4269 )
4270 }
4271
4272 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4274 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4275 .map_err(GeneratorError::ParseError)?;
4276 let mut canonical_operation_ids = HashSet::new();
4281
4282 if let Some(paths) = &spec.paths {
4283 for (path, path_item) in paths {
4284 let resolved = self.resolve_path_item(path_item, &spec)?;
4286 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4287 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4288 }
4289 }
4290 if let Some(webhooks) = &spec.webhooks {
4297 for (name, path_item) in webhooks {
4298 let synthetic_path = format!("__webhook__/{name}");
4299 self.ingest_path_item_operations(
4300 &synthetic_path,
4301 path_item,
4302 analysis,
4303 &mut canonical_operation_ids,
4304 )?;
4305 }
4306 }
4307 Ok(())
4308 }
4309
4310 fn resolve_path_item(
4314 &self,
4315 path_item: &crate::openapi::PathItem,
4316 spec: &crate::openapi::OpenApiSpec,
4317 ) -> Result<Option<crate::openapi::PathItem>> {
4318 let Some(reference) = &path_item.reference else {
4319 return Ok(None);
4320 };
4321 let target_name = reference
4322 .strip_prefix("#/components/pathItems/")
4323 .ok_or_else(|| {
4324 GeneratorError::UnresolvedReference(format!(
4325 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4326 ))
4327 })?;
4328 let pi = spec
4329 .components
4330 .as_ref()
4331 .and_then(|c| c.path_items.as_ref())
4332 .and_then(|map| map.get(target_name))
4333 .ok_or_else(|| {
4334 GeneratorError::UnresolvedReference(format!(
4335 "Path Item ref {reference} not found in components/pathItems"
4336 ))
4337 })?;
4338 Ok(Some(pi.clone()))
4339 }
4340
4341 fn ingest_path_item_operations(
4342 &mut self,
4343 path: &str,
4344 path_item: &crate::openapi::PathItem,
4345 analysis: &mut SchemaAnalysis,
4346 canonical_operation_ids: &mut HashSet<String>,
4347 ) -> Result<()> {
4348 for (method, operation) in path_item.operations() {
4349 let raw_operation_id = operation
4351 .operation_id
4352 .clone()
4353 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4354
4355 let operation_id = if canonical_operation_ids
4366 .contains(&Self::canonical_operation_id(&raw_operation_id))
4367 {
4368 let method_lower = method.to_lowercase();
4369 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4370 let mut suffix = 2;
4371 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4372 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4373 suffix += 1;
4374 }
4375 eprintln!(
4376 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4377 raw_operation_id, method, path, candidate
4378 );
4379 candidate
4380 } else {
4381 raw_operation_id.clone()
4382 };
4383
4384 let (op_info, responses) = self.analyze_single_operation(
4385 &operation_id,
4386 method,
4387 path,
4388 operation,
4389 path_item.parameters.as_ref(),
4390 analysis,
4391 )?;
4392 analysis
4393 .operation_id_aliases
4394 .entry(raw_operation_id)
4395 .or_default()
4396 .push(operation_id.clone());
4397 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4398 analysis
4399 .operation_responses
4400 .insert(operation_id.clone(), responses);
4401 analysis.operations.insert(operation_id, op_info);
4402 }
4403 Ok(())
4404 }
4405
4406 fn canonical_operation_id(operation_id: &str) -> String {
4407 use heck::ToPascalCase;
4408 operation_id.replace('.', "_").to_pascal_case()
4409 }
4410
4411 fn generate_operation_id(method: &str, path: &str) -> String {
4414 let mut operation_id = method.to_lowercase();
4416
4417 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4419
4420 for part in path_parts {
4421 if part.is_empty() {
4422 continue;
4423 }
4424
4425 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4427 &part[1..part.len() - 1]
4428 } else {
4429 part
4430 };
4431
4432 let pascal_case_part = cleaned_part
4434 .split(&['-', '_'][..])
4435 .map(|s| {
4436 let mut chars = s.chars();
4437 match chars.next() {
4438 None => String::new(),
4439 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4440 }
4441 })
4442 .collect::<String>();
4443
4444 operation_id.push_str(&pascal_case_part);
4445 }
4446
4447 operation_id
4448 }
4449
4450 fn analyze_single_operation(
4452 &mut self,
4453 operation_id: &str,
4454 method: &str,
4455 path: &str,
4456 operation: &crate::openapi::Operation,
4457 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4458 _analysis: &mut SchemaAnalysis,
4459 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
4460 let raw_path_item = self
4461 .openapi_spec
4462 .get("paths")
4463 .and_then(|paths| paths.get(path))
4464 .cloned();
4465 let raw_operation = raw_path_item
4466 .as_ref()
4467 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
4468 .cloned();
4469 let mut op_info = OperationInfo {
4470 operation_id: operation_id.to_string(),
4471 method: method.to_uppercase(),
4472 path: path.to_string(),
4473 summary: operation.summary.clone(),
4474 description: operation.description.clone(),
4475 request_body: None,
4476 request_body_required: operation
4478 .request_body
4479 .as_ref()
4480 .and_then(|rb| rb.required)
4481 .unwrap_or(false),
4482 response_schemas: BTreeMap::new(),
4483 parameters: Vec::new(),
4484 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4487 };
4488 let mut operation_responses = BTreeMap::new();
4489
4490 if let Some(request_body) = &operation.request_body {
4492 use crate::openapi::{is_form_urlencoded_media_type, is_json_media_type};
4493 if let Some((content_type, maybe_schema)) = request_body.best_content() {
4494 op_info.request_body = if is_json_media_type(content_type) {
4495 match maybe_schema {
4496 Some(s) => {
4497 let validation_schema = self
4498 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4499 .unwrap_or(
4500 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4501 );
4502 Some(
4503 self.resolve_or_inline_schema(s, operation_id, "Request")
4504 .map(|name| RequestBodyContent::Json {
4505 schema_name: name,
4506 media_type: content_type.to_string(),
4507 validation_schema,
4508 })?,
4509 )
4510 }
4511 None => Some(RequestBodyContent::SchemaLess {
4512 media_type: content_type.to_string(),
4513 }),
4514 }
4515 } else if is_form_urlencoded_media_type(content_type) {
4516 match maybe_schema {
4517 Some(s) => {
4518 let validation_schema = self
4519 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4520 .unwrap_or(
4521 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4522 );
4523 Some(
4524 self.resolve_or_inline_schema(s, operation_id, "Request")
4525 .map(|name| RequestBodyContent::FormUrlEncoded {
4526 schema_name: name,
4527 media_type: content_type.to_string(),
4528 validation_schema,
4529 })?,
4530 )
4531 }
4532 None => Some(RequestBodyContent::SchemaLess {
4533 media_type: content_type.to_string(),
4534 }),
4535 }
4536 } else {
4537 match content_type {
4538 "multipart/form-data" => Some(RequestBodyContent::Multipart),
4539 "application/octet-stream" => Some(RequestBodyContent::OctetStream),
4540 "text/plain" => Some(RequestBodyContent::TextPlain),
4541 _ => None,
4542 }
4543 };
4544 }
4545 if op_info.request_body.is_none() {
4546 let mut media_types = request_body
4547 .content
4548 .as_ref()
4549 .map(|content| content.keys().cloned().collect::<Vec<_>>())
4550 .unwrap_or_default();
4551 media_types.sort();
4552 if !media_types.is_empty() {
4553 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
4554 }
4555 }
4556 }
4557
4558 if let Some(responses) = &operation.responses {
4560 for (status_code, response) in responses {
4561 let response = self.resolve_response(response)?;
4562 let supports_streaming = response.content.as_ref().is_some_and(|content| {
4568 content
4569 .keys()
4570 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
4571 });
4572 if supports_streaming {
4573 op_info.supports_streaming = true;
4574 }
4575
4576 let mut response_info = OperationResponse {
4577 supports_streaming,
4578 has_content: response
4579 .content
4580 .as_ref()
4581 .is_some_and(|content| !content.is_empty()),
4582 ..Default::default()
4583 };
4584 if let Some((media_type, schema)) = response.json_content() {
4585 if let Some(schema_ref) = schema.reference() {
4586 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4588 op_info
4589 .response_schemas
4590 .insert(status_code.clone(), schema_name.to_string());
4591 response_info.schema_name = Some(schema_name.to_string());
4592 response_info.media_type = Some(media_type.to_string());
4593 }
4594 } else {
4595 let synthetic_name =
4597 self.generate_inline_response_type_name(operation_id, status_code);
4598
4599 let mut deps = HashSet::new();
4601 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4602
4603 op_info
4604 .response_schemas
4605 .insert(status_code.clone(), synthetic_name.clone());
4606 response_info.schema_name = Some(synthetic_name);
4607 response_info.media_type = Some(media_type.to_string());
4608 }
4609 }
4610 response_info.unsupported_media_types = response
4611 .content
4612 .as_ref()
4613 .into_iter()
4614 .flat_map(|content| content.iter())
4615 .filter(|(media_type, content)| {
4616 !crate::openapi::is_event_stream_media_type(media_type)
4617 && (!crate::openapi::is_json_media_type(media_type)
4618 || content.schema.is_none())
4619 })
4620 .map(|(media_type, _)| media_type.clone())
4621 .collect();
4622 operation_responses.insert(status_code.clone(), response_info);
4623 }
4624 }
4625
4626 if op_info.supports_streaming
4629 && let Some(parameters) = &operation.parameters
4630 {
4631 for param in parameters {
4632 if let Some(name) = param.name.as_deref() {
4633 if name.eq_ignore_ascii_case("stream") {
4634 op_info.stream_parameter = Some(name.to_string());
4635 break;
4636 }
4637 }
4638 }
4639 }
4640
4641 if let Some(parameters) = &operation.parameters {
4643 for (index, param) in parameters.iter().enumerate() {
4644 let resolved = self.resolve_parameter(param).into_owned();
4648 let validation_schema = raw_operation
4649 .as_ref()
4650 .and_then(|operation| operation.get("parameters"))
4651 .and_then(Value::as_array)
4652 .and_then(|parameters| parameters.get(index))
4653 .and_then(|parameter| self.raw_parameter_schema(parameter));
4654 if let Some(param_info) =
4655 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4656 {
4657 op_info.parameters.push(param_info);
4658 }
4659 }
4660 }
4661
4662 if let Some(path_params) = path_item_parameters {
4664 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4665 .parameters
4666 .iter()
4667 .map(|p| (p.name.clone(), p.location.clone()))
4668 .collect();
4669 for (index, param) in path_params.iter().enumerate() {
4670 let resolved = self.resolve_parameter(param).into_owned();
4671 let validation_schema = raw_path_item
4672 .as_ref()
4673 .and_then(|path_item| path_item.get("parameters"))
4674 .and_then(Value::as_array)
4675 .and_then(|parameters| parameters.get(index))
4676 .and_then(|parameter| self.raw_parameter_schema(parameter));
4677 if let Some(param_info) =
4678 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4679 {
4680 if !existing_keys
4681 .contains(&(param_info.name.clone(), param_info.location.clone()))
4682 {
4683 op_info.parameters.push(param_info);
4684 }
4685 }
4686 }
4687 }
4688
4689 let mut declared_path_names: std::collections::HashSet<String> = op_info
4697 .parameters
4698 .iter()
4699 .filter(|p| p.location == "path")
4700 .map(|p| p.name.clone())
4701 .collect();
4702 let bytes = path.as_bytes().iter();
4703 let mut current = String::new();
4704 let mut in_brace = false;
4705 let mut synthesized: Vec<String> = Vec::new();
4706 for b in bytes {
4707 match *b {
4708 b'{' => {
4709 in_brace = true;
4710 current.clear();
4711 }
4712 b'}' if in_brace => {
4713 in_brace = false;
4714 if !current.is_empty() && !declared_path_names.contains(¤t) {
4715 synthesized.push(current.clone());
4716 declared_path_names.insert(current.clone());
4717 }
4718 }
4719 _ if in_brace => current.push(*b as char),
4720 _ => {}
4721 }
4722 }
4723 for name in synthesized {
4724 eprintln!(
4725 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
4726 path, name
4727 );
4728 op_info.parameters.push(ParameterInfo {
4729 name,
4730 location: "path".to_string(),
4731 required: true,
4732 schema_ref: None,
4733 rust_type: "String".to_string(),
4734 description: None,
4735 enum_values: None,
4736 rust_ident: None,
4737 query_serialization: None,
4738 validation_schema: None,
4739 });
4740 }
4741
4742 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
4750 for p in op_info.parameters.iter_mut() {
4751 let raw = base_param_ident(&p.name);
4752 let mut chosen = raw.clone();
4753 let mut suffix = 2;
4754 while !used.insert(chosen.clone()) {
4755 chosen = format!("{raw}_{suffix}");
4756 suffix += 1;
4757 }
4758 p.rust_ident = Some(chosen);
4759 }
4760
4761 Ok((op_info, operation_responses))
4762 }
4763
4764 fn resolve_response(
4771 &self,
4772 response: &crate::openapi::Response,
4773 ) -> Result<crate::openapi::Response> {
4774 let mut current = response.clone();
4775 let mut visited = HashSet::new();
4776 while let Some(reference) = current.reference.clone() {
4777 if !visited.insert(reference.clone()) {
4778 return Err(GeneratorError::CircularDependency(format!(
4779 "response reference {reference}"
4780 )));
4781 }
4782
4783 let pointer = reference.strip_prefix('#').ok_or_else(|| {
4784 GeneratorError::UnresolvedReference(format!(
4785 "external response reference `{reference}` is not supported"
4786 ))
4787 })?;
4788 if !pointer.is_empty() && !pointer.starts_with('/') {
4789 return Err(GeneratorError::UnresolvedReference(format!(
4790 "response reference `{reference}` is not a local JSON Pointer"
4791 )));
4792 }
4793 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
4794 GeneratorError::UnresolvedReference(format!(
4795 "response reference `{reference}` does not exist"
4796 ))
4797 })?;
4798 let object = value.as_object().ok_or_else(|| {
4799 GeneratorError::InvalidSchema(format!(
4800 "response reference `{reference}` must target an object"
4801 ))
4802 })?;
4803 if !["$ref", "description", "headers", "content", "links"]
4804 .iter()
4805 .any(|field| object.contains_key(*field))
4806 {
4807 return Err(GeneratorError::InvalidSchema(format!(
4808 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
4809 )));
4810 }
4811 current = serde_json::from_value(value.clone()).map_err(|error| {
4812 GeneratorError::InvalidSchema(format!(
4813 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
4814 ))
4815 })?;
4816 }
4817 Ok(current)
4818 }
4819
4820 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
4827 use heck::ToPascalCase;
4828 let base_name = operation_id.replace('.', "_").to_pascal_case();
4829 let suffix = Self::status_code_suffix(status_code);
4830 format!("{}Response{}", base_name, suffix)
4831 }
4832
4833 fn status_code_suffix(status_code: &str) -> String {
4840 match status_code {
4841 "" | "200" => String::new(),
4842 "default" | "Default" => "Default".to_string(),
4843 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
4844 other => other.to_ascii_lowercase(),
4845 }
4846 }
4847
4848 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
4850 use heck::ToPascalCase;
4851 let base_name = operation_id.replace('.', "_").to_pascal_case();
4855 format!("{}Request", base_name)
4856 }
4857
4858 fn resolve_or_inline_schema(
4861 &mut self,
4862 schema: &crate::openapi::Schema,
4863 operation_id: &str,
4864 suffix: &str,
4865 ) -> Result<String> {
4866 if let Some(schema_ref) = schema.reference()
4867 && let Some(schema_name) = self.extract_schema_name(schema_ref)
4868 {
4869 return Ok(schema_name.to_string());
4870 }
4871 let synthetic_name = if suffix == "Request" {
4873 self.generate_inline_request_type_name(operation_id)
4874 } else {
4875 self.generate_inline_response_type_name(operation_id, "")
4876 };
4877 let mut deps = HashSet::new();
4878 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4879 Ok(synthetic_name)
4880 }
4881
4882 fn resolve_parameter<'a>(
4885 &'a self,
4886 param: &'a crate::openapi::Parameter,
4887 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
4888 if let Some(ref_str) = param.reference.as_deref() {
4889 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
4890 if let Some(resolved) = self.component_parameters.get(param_name) {
4891 return std::borrow::Cow::Borrowed(resolved);
4892 }
4893 }
4894 }
4895 std::borrow::Cow::Borrowed(param)
4896 }
4897
4898 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
4911 if self.resolve_cached_schema(name).is_some_and(|schema| {
4912 matches!(
4913 schema.schema_type,
4914 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
4915 )
4916 }) {
4917 return true;
4918 }
4919 let Some(schema_value) = self
4920 .openapi_spec
4921 .get("components")
4922 .and_then(|c| c.get("schemas"))
4923 .and_then(|s| s.get(name))
4924 else {
4925 return false;
4926 };
4927 let is_string_type = schema_value
4928 .get("type")
4929 .and_then(|v| v.as_str())
4930 .map(|s| s == "string")
4931 .unwrap_or(false);
4932 let has_enum_or_const =
4933 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
4934 is_string_type && has_enum_or_const
4935 }
4936
4937 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
4938 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
4939 return Some(value.clone());
4940 };
4941 let pointer = reference.strip_prefix('#')?;
4942 self.openapi_spec.pointer(pointer).cloned()
4943 }
4944
4945 fn raw_request_body_schema(
4946 &self,
4947 operation: Option<&Value>,
4948 content_type: &str,
4949 ) -> Option<Value> {
4950 let request_body = operation?.get("requestBody")?;
4951 self.resolve_raw_local_reference(request_body)?
4952 .get("content")?
4953 .get(content_type)?
4954 .get("schema")
4955 .cloned()
4956 }
4957
4958 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
4959 self.resolve_raw_local_reference(parameter)?
4960 .get("schema")
4961 .cloned()
4962 }
4963
4964 fn analyze_parameter(
4965 &mut self,
4966 param: &crate::openapi::Parameter,
4967 operation_id: &str,
4968 raw_validation_schema: Option<Value>,
4969 ) -> Result<Option<ParameterInfo>> {
4970 use heck::ToPascalCase;
4971
4972 let name = param.name.as_deref().unwrap_or("");
4973 let location = param.location.as_deref().unwrap_or("");
4974 let required = param.required.unwrap_or(false);
4975 let validation_schema = match raw_validation_schema {
4976 Some(schema) => Some(schema),
4977 None => param
4978 .schema
4979 .as_ref()
4980 .map(serde_json::to_value)
4981 .transpose()
4982 .map_err(GeneratorError::ParseError)?,
4983 };
4984
4985 let mut rust_type = "String".to_string();
4986 let mut schema_ref = None;
4987 let mut enum_values: Option<Vec<String>> = None;
4988 let mut query_serialization: Option<QuerySerialization> = None;
4989
4990 let is_query = location == "query";
4996 let form_style = matches!(param.style.as_deref(), None | Some("form"));
4997 let form_exploded = form_style && param.explode.unwrap_or(true);
4998 let deep_object =
4999 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
5000
5001 let object_serialization = if !is_query {
5002 None
5003 } else if deep_object {
5004 Some(QuerySerialization::DeepObject)
5005 } else if form_exploded {
5006 Some(QuerySerialization::FormExplodedObject)
5007 } else if form_style {
5008 Some(QuerySerialization::FormObject)
5009 } else {
5010 None
5011 };
5012
5013 if let Some(schema) = ¶m.schema {
5014 if let Some(ref_str) = schema.reference() {
5015 if let Some(name) = self.extract_schema_name(ref_str) {
5021 if self.referenced_schema_is_string_enum(name) {
5022 schema_ref = Some(name.to_string());
5023 } else if object_serialization.is_some()
5024 && self.referenced_schema_is_object(name)
5025 {
5026 schema_ref = Some(name.to_string());
5027 query_serialization = object_serialization.clone();
5028 } else if is_query
5029 && form_style
5030 && let Some(item_type) = self.referenced_array_param_item_type(name)
5031 {
5032 schema_ref = Some(name.to_string());
5038 query_serialization = Some(if form_exploded {
5039 QuerySerialization::FormExplodedArray { item_type }
5040 } else {
5041 QuerySerialization::FormArray { item_type }
5042 });
5043 }
5044 }
5045 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
5046 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5051 let param_pascal = name.to_pascal_case();
5052 let synthetic_name = format!("{op_pascal}{param_pascal}");
5053 let mut deps = HashSet::new();
5054 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5055 schema_ref = Some(synthetic_name);
5056 query_serialization = object_serialization.clone();
5057 } else if is_query
5058 && form_style
5059 && matches!(
5060 schema.schema_type(),
5061 Some(crate::openapi::SchemaType::Array)
5062 )
5063 && let Some(item_type) = self.array_param_item_type(schema)
5064 {
5065 query_serialization = Some(if form_exploded {
5073 QuerySerialization::FormExplodedArray { item_type }
5074 } else {
5075 QuerySerialization::FormArray { item_type }
5076 });
5077 } else if let Some(schema_type) = schema.schema_type() {
5078 let format = schema.details().format.clone();
5084 rust_type = match schema_type {
5085 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5086 crate::openapi::SchemaType::Integer => {
5087 self.type_mapper.integer_format(format.as_deref()).rust_type
5088 }
5089 crate::openapi::SchemaType::Number => {
5090 self.type_mapper.number_format(format.as_deref()).rust_type
5091 }
5092 crate::openapi::SchemaType::String => "String".to_string(),
5093 _ => "String".to_string(),
5094 };
5095
5096 if matches!(schema_type, crate::openapi::SchemaType::String) {
5097 let details = schema.details();
5098 if details.is_string_enum() {
5099 if let Some(values) = details.string_enum_values() {
5100 if !values.is_empty() {
5101 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5102 let param_pascal = name.to_pascal_case();
5103 rust_type = format!("{op_pascal}{param_pascal}");
5104 enum_values = Some(values);
5105 }
5106 }
5107 }
5108 }
5109 }
5110
5111 if is_query && query_serialization.is_none() {
5112 let referenced_name = schema
5113 .reference()
5114 .and_then(|reference| self.extract_schema_name(reference));
5115 let is_object = referenced_name
5116 .is_some_and(|name| self.referenced_schema_is_object(name))
5117 || Self::schema_is_inline_object(schema);
5118 let is_array = referenced_name
5119 .is_some_and(|name| self.referenced_schema_is_array(name))
5120 || matches!(
5121 schema.schema_type(),
5122 Some(crate::openapi::SchemaType::Array)
5123 );
5124 let is_composed = referenced_name
5125 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
5126 let reason = if param.style.as_deref() == Some("deepObject")
5127 && param.explode == Some(false)
5128 {
5129 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
5130 } else if param.style.as_deref() == Some("deepObject") && !is_object {
5131 Some("style=deepObject is defined only for object query parameters".to_string())
5132 } else if is_object {
5133 Some(format!(
5134 "object query parameters do not support style={}",
5135 param.style.as_deref().unwrap_or("form")
5136 ))
5137 } else if is_array && form_style {
5138 Some(
5139 "form array query parameters require scalar or string-enum items"
5140 .to_string(),
5141 )
5142 } else if is_array {
5143 Some(format!(
5144 "array query parameters do not yet support style={}",
5145 param.style.as_deref().unwrap_or("form")
5146 ))
5147 } else if is_composed {
5148 Some(
5149 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
5150 .to_string(),
5151 )
5152 } else {
5153 None
5154 };
5155 if let Some(reason) = reason {
5156 query_serialization = Some(QuerySerialization::Unsupported { reason });
5157 }
5158 }
5159 }
5160
5161 Ok(Some(ParameterInfo {
5162 name: name.to_string(),
5163 location: location.to_string(),
5164 required,
5165 schema_ref,
5166 rust_type,
5167 description: param.description.clone(),
5168 enum_values,
5169 rust_ident: None,
5170 query_serialization,
5171 validation_schema,
5172 }))
5173 }
5174
5175 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
5184 let items = schema.details().items.as_deref()?;
5185 if let Some(ref_str) = items.reference() {
5186 let name = self.extract_schema_name(ref_str)?;
5187 return self
5188 .referenced_schema_is_string_enum(name)
5189 .then(|| ArrayItemType::EnumRef(name.to_string()));
5190 }
5191 let format = items.details().format.clone();
5192 let scalar = match items.schema_type()? {
5193 crate::openapi::SchemaType::String => "String".to_string(),
5194 crate::openapi::SchemaType::Integer => {
5195 self.type_mapper.integer_format(format.as_deref()).rust_type
5196 }
5197 crate::openapi::SchemaType::Number => {
5198 self.type_mapper.number_format(format.as_deref()).rust_type
5199 }
5200 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5201 _ => return None,
5202 };
5203 Some(ArrayItemType::Scalar(scalar))
5204 }
5205
5206 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
5209 let schema = self.resolve_cached_schema(name)?;
5210 let SchemaType::Array { item_type } = &schema.schema_type else {
5211 return None;
5212 };
5213 self.analyzed_array_item_type(item_type)
5214 }
5215
5216 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
5217 match item_type {
5218 SchemaType::Primitive { rust_type, .. } => {
5219 Some(ArrayItemType::Scalar(rust_type.clone()))
5220 }
5221 SchemaType::Reference { target } => {
5222 let resolved = self.resolve_cached_schema(target)?;
5223 matches!(
5224 resolved.schema_type,
5225 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
5226 )
5227 .then(|| ArrayItemType::EnumRef(target.clone()))
5228 }
5229 _ => None,
5230 }
5231 }
5232
5233 fn referenced_schema_is_object(&self, name: &str) -> bool {
5237 self.resolve_cached_schema(name)
5238 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
5239 }
5240
5241 fn referenced_schema_is_array(&self, name: &str) -> bool {
5242 self.resolve_cached_schema(name)
5243 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
5244 }
5245
5246 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
5247 self.resolve_cached_schema(name).is_some_and(|schema| {
5248 matches!(
5249 schema.schema_type,
5250 SchemaType::Composition { .. }
5251 | SchemaType::Union { .. }
5252 | SchemaType::DiscriminatedUnion { .. }
5253 )
5254 })
5255 }
5256
5257 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
5258 let mut current = name;
5259 let mut visited = HashSet::new();
5260 loop {
5261 if !visited.insert(current) {
5262 return None;
5263 }
5264 let schema = self.resolved_cache.get(current)?;
5265 if let SchemaType::Reference { target } = &schema.schema_type {
5266 current = target;
5267 } else {
5268 return Some(schema);
5269 }
5270 }
5271 }
5272
5273 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
5275 match schema.schema_type() {
5276 Some(crate::openapi::SchemaType::Object) => true,
5277 None => schema.details().properties.is_some(),
5278 _ => false,
5279 }
5280 }
5281}