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 body: Option<OperationResponseBody>,
116 pub supports_streaming: bool,
118 pub has_content: bool,
120 pub unsupported_media_types: Vec<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
129#[serde(tag = "kind", rename_all = "snake_case")]
130pub enum OperationResponseBody {
131 Json {
132 schema_name: String,
133 media_type: String,
134 },
135 Text {
136 media_type: String,
137 },
138 Binary {
139 media_type: String,
140 wildcard: bool,
141 },
142}
143
144#[derive(Debug, Clone, Default)]
145pub struct ValidationContext {
146 pub openapi_version: String,
147 pub json_schema_dialect: Option<String>,
148 pub component_schemas: BTreeMap<String, Value>,
149}
150
151#[derive(Debug, Clone, Default)]
156pub struct EnumExtensions {
157 pub varnames: Vec<String>,
162 pub descriptions: Vec<String>,
164}
165
166#[derive(Debug, Clone)]
167pub struct AnalyzedSchema {
168 pub name: String,
169 pub original: Value,
170 pub schema_type: SchemaType,
171 pub dependencies: HashSet<String>,
172 pub nullable: bool,
173 pub description: Option<String>,
174 pub default: Option<serde_json::Value>,
175}
176
177#[derive(Debug, Clone)]
178pub enum SchemaType {
179 Primitive {
185 rust_type: String,
186 serde_with: Option<String>,
187 },
188 Object {
190 properties: BTreeMap<String, PropertyInfo>,
191 required: HashSet<String>,
192 additional_properties: ObjectAdditionalProperties,
193 },
194 DiscriminatedUnion {
196 discriminator_field: String,
197 variants: Vec<UnionVariant>,
198 },
199 Union { variants: Vec<SchemaRef> },
201 Array { item_type: Box<SchemaType> },
203 StringEnum { values: Vec<String> },
205 ExtensibleEnum { known_values: Vec<String> },
207 Composition { schemas: Vec<SchemaRef> },
209 Reference { target: String },
211}
212
213#[derive(Debug, Clone)]
218pub enum ObjectAdditionalProperties {
219 Forbidden,
222 Untyped,
225 Typed { value_type: Box<SchemaType> },
228}
229
230impl ObjectAdditionalProperties {
231 pub fn is_open(&self) -> bool {
234 !matches!(self, Self::Forbidden)
235 }
236}
237
238#[derive(Debug, Clone)]
239pub struct PropertyInfo {
240 pub schema_type: SchemaType,
241 pub nullable: bool,
242 pub description: Option<String>,
243 pub default: Option<serde_json::Value>,
244 pub serde_attrs: Vec<String>,
245 pub constraints: PropertyConstraints,
250}
251
252#[derive(Debug, Clone, Default)]
257pub struct PropertyConstraints {
258 pub minimum: Option<f64>,
259 pub maximum: Option<f64>,
260 pub exclusive_minimum: Option<f64>,
261 pub exclusive_maximum: Option<f64>,
262 pub multiple_of: Option<f64>,
263 pub min_length: Option<u64>,
264 pub max_length: Option<u64>,
265 pub pattern: Option<String>,
266 pub min_items: Option<u64>,
267 pub max_items: Option<u64>,
268 pub unique_items: Option<bool>,
269}
270
271impl PropertyConstraints {
272 pub fn is_empty(&self) -> bool {
273 self.minimum.is_none()
274 && self.maximum.is_none()
275 && self.exclusive_minimum.is_none()
276 && self.exclusive_maximum.is_none()
277 && self.multiple_of.is_none()
278 && self.min_length.is_none()
279 && self.max_length.is_none()
280 && self.pattern.is_none()
281 && self.min_items.is_none()
282 && self.max_items.is_none()
283 && self.unique_items.is_none()
284 }
285
286 pub fn from_schema_details(details: &crate::openapi::SchemaDetails) -> Self {
291 use crate::openapi::ExclusiveBound;
292 let exclusive_minimum = match &details.exclusive_minimum {
293 Some(ExclusiveBound::Number(v)) => Some(*v),
294 _ => None,
295 };
296 let exclusive_maximum = match &details.exclusive_maximum {
297 Some(ExclusiveBound::Number(v)) => Some(*v),
298 _ => None,
299 };
300 Self {
301 minimum: details.minimum,
302 maximum: details.maximum,
303 exclusive_minimum,
304 exclusive_maximum,
305 multiple_of: details.multiple_of,
306 min_length: details.min_length,
307 max_length: details.max_length,
308 pattern: details.pattern.clone(),
309 min_items: details.min_items,
310 max_items: details.max_items,
311 unique_items: details.unique_items,
312 }
313 }
314}
315
316#[derive(Debug, Clone)]
317pub struct UnionVariant {
318 pub rust_name: String,
319 pub type_name: String,
320 pub discriminator_value: String,
321 pub schema_ref: String,
322}
323
324#[derive(Debug, Clone)]
325pub struct SchemaRef {
326 pub target: String,
327 pub nullable: bool,
328}
329
330#[derive(Debug, Clone)]
331pub struct DependencyGraph {
332 pub edges: BTreeMap<String, HashSet<String>>,
333 pub recursive_schemas: HashSet<String>,
335}
336
337#[derive(Debug, Clone)]
338pub struct DetectedPatterns {
339 pub tagged_enum_schemas: HashSet<String>,
341 pub untagged_enum_schemas: HashSet<String>,
343 pub type_mappings: BTreeMap<String, BTreeMap<String, String>>,
345}
346
347#[derive(Debug, Clone, Default, serde::Serialize)]
349pub struct OperationInfo {
350 pub operation_id: String,
352 pub method: String,
354 pub path: String,
356 pub summary: Option<String>,
358 pub description: Option<String>,
360 pub request_body: Option<RequestBodyContent>,
362 pub request_body_required: bool,
365 pub response_schemas: BTreeMap<String, String>,
367 pub parameters: Vec<ParameterInfo>,
369 pub supports_streaming: bool,
371 pub stream_parameter: Option<String>,
373 pub tags: Vec<String>,
377}
378
379#[derive(Debug, Clone, serde::Serialize)]
381#[serde(tag = "kind")]
382pub enum RequestBodyContent {
383 Json {
384 schema_name: String,
385 media_type: String,
386 #[serde(skip)]
387 validation_schema: Value,
388 },
389 FormUrlEncoded {
390 schema_name: String,
391 media_type: String,
392 #[serde(skip)]
393 validation_schema: Value,
394 },
395 Multipart {
396 schema_name: String,
397 media_type: String,
398 #[serde(skip)]
399 validation_schema: Value,
400 },
401 OctetStream {
402 media_type: String,
403 },
404 Binary {
405 media_type: String,
406 },
407 TextPlain {
408 media_type: String,
409 },
410 SchemaLess {
414 media_type: String,
415 },
416 Unsupported {
417 media_types: Vec<String>,
418 },
419}
420
421impl RequestBodyContent {
422 pub fn schema_name(&self) -> Option<&str> {
424 match self {
425 Self::Json { schema_name, .. }
426 | Self::FormUrlEncoded { schema_name, .. }
427 | Self::Multipart { schema_name, .. } => Some(schema_name),
428 Self::OctetStream { .. }
429 | Self::Binary { .. }
430 | Self::TextPlain { .. }
431 | Self::SchemaLess { .. }
432 | Self::Unsupported { .. } => None,
433 }
434 }
435}
436
437fn base_param_ident(name: &str) -> String {
441 use heck::ToSnakeCase;
442 let suffix = if name.ends_with("<=") {
443 "_lte"
444 } else if name.ends_with(">=") {
445 "_gte"
446 } else if name.ends_with('<') {
447 "_lt"
448 } else if name.ends_with('>') {
449 "_gt"
450 } else {
451 ""
452 };
453 let stripped = name.trim_end_matches(['<', '>', '=']);
454 let mut snake = stripped.to_snake_case();
455 if snake.is_empty() {
456 snake.push_str("parameter");
457 } else if snake.starts_with(|character: char| character.is_ascii_digit()) {
458 snake.insert(0, '_');
459 }
460 snake.push_str(suffix);
461 snake
462}
463
464#[derive(Debug, Clone, serde::Serialize)]
466pub struct ParameterInfo {
467 pub name: String,
469 pub location: String,
471 pub required: bool,
473 pub schema_ref: Option<String>,
475 pub rust_type: String,
477 pub description: Option<String>,
479 #[serde(skip_serializing_if = "Option::is_none")]
485 pub enum_values: Option<Vec<String>>,
486 #[serde(skip_serializing_if = "Option::is_none")]
492 pub enum_varnames: Option<Vec<String>>,
493 #[serde(skip_serializing_if = "Option::is_none")]
501 pub rust_ident: Option<String>,
502 #[serde(skip_serializing_if = "Option::is_none")]
511 pub query_serialization: Option<QuerySerialization>,
512 #[serde(skip)]
515 pub validation_schema: Option<Value>,
516}
517
518#[derive(Debug, Clone, PartialEq, serde::Serialize)]
521pub enum QuerySerialization {
522 FormExplodedObject,
526 FormExplodedNestedObject {
532 properties: Vec<QueryStructProperty>,
533 },
534 FormObject,
537 DeepObject,
540 FormExplodedArray { item_type: ArrayItemType },
543 FormArray { item_type: ArrayItemType },
546 SimpleHeaderArray { item_type: ArrayItemType },
549 Unsupported { reason: String },
554}
555
556#[derive(Debug, Clone, PartialEq, serde::Serialize)]
563pub enum ArrayItemType {
564 Scalar(String),
566 SchemaRef(String),
568 FlatStructRef {
574 schema_name: String,
575 properties: Vec<QueryStructProperty>,
576 },
577 NestedStructRef {
581 schema_name: String,
582 properties: Vec<QueryStructProperty>,
583 },
584}
585
586#[derive(Debug, Clone, PartialEq, serde::Serialize)]
587pub struct QueryStructProperty {
588 pub wire_name: String,
589 pub required: bool,
590 pub value_type: QueryStructPropertyType,
591}
592
593#[derive(Debug, Clone, PartialEq, serde::Serialize)]
594pub enum QueryStructPropertyType {
595 Scalar(QueryScalarType),
596 Array {
597 item_type: ArrayItemType,
598 },
599 Object {
600 properties: Vec<QueryStructProperty>,
601 },
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
605pub enum QueryScalarType {
606 String,
607 Integer,
608 Number,
609 Boolean,
610}
611
612impl Default for DependencyGraph {
613 fn default() -> Self {
614 Self::new()
615 }
616}
617
618impl DependencyGraph {
619 pub fn new() -> Self {
620 Self {
621 edges: BTreeMap::new(),
622 recursive_schemas: HashSet::new(),
623 }
624 }
625
626 pub fn add_dependency(&mut self, from: String, to: String) {
627 self.edges.entry(from).or_default().insert(to);
628 }
629
630 pub fn topological_sort(&mut self) -> Result<Vec<String>> {
632 self.detect_recursive_schemas();
634
635 let mut temp_edges = self.edges.clone();
637 for (schema, deps) in &mut temp_edges {
638 deps.remove(schema); }
640
641 let mut visited = HashSet::new();
642 let mut temp_visited = HashSet::new();
643 let mut result = Vec::new();
644
645 let mut all_nodes: Vec<_> = temp_edges.keys().collect();
647 all_nodes.sort();
648 for node in all_nodes {
649 if !visited.contains(node) {
650 self.visit_node_recursive(
651 node,
652 &temp_edges,
653 &mut visited,
654 &mut temp_visited,
655 &mut result,
656 )?;
657 }
658 }
659
660 result.reverse();
661 Ok(result)
662 }
663
664 fn detect_recursive_schemas(&mut self) {
665 for (schema, deps) in &self.edges {
666 if deps.contains(schema) {
667 self.recursive_schemas.insert(schema.clone());
669 } else {
670 if self.has_cycle_from(schema, schema, &mut HashSet::new()) {
672 self.recursive_schemas.insert(schema.clone());
673 }
674 }
675 }
676
677 for (schema, deps) in &self.edges {
679 for dep in deps {
680 if let Some(dep_deps) = self.edges.get(dep) {
681 if dep_deps.contains(schema) {
682 self.recursive_schemas.insert(schema.clone());
684 self.recursive_schemas.insert(dep.clone());
685 }
686 }
687 }
688 }
689 }
690
691 fn has_cycle_from(&self, start: &str, current: &str, visited: &mut HashSet<String>) -> bool {
692 if visited.contains(current) {
693 return false; }
695
696 visited.insert(current.to_string());
697
698 if let Some(deps) = self.edges.get(current) {
699 for dep in deps {
700 if dep == start {
701 return true; }
703 if self.has_cycle_from(start, dep, visited) {
704 return true;
705 }
706 }
707 }
708
709 false
710 }
711
712 #[allow(clippy::only_used_in_recursion)]
713 fn visit_node_recursive(
714 &self,
715 node: &str,
716 temp_edges: &BTreeMap<String, HashSet<String>>,
717 visited: &mut HashSet<String>,
718 temp_visited: &mut HashSet<String>,
719 result: &mut Vec<String>,
720 ) -> Result<()> {
721 if temp_visited.contains(node) {
722 return Ok(());
724 }
725
726 if visited.contains(node) {
727 return Ok(());
728 }
729
730 temp_visited.insert(node.to_string());
731
732 if let Some(dependencies) = temp_edges.get(node) {
733 let mut sorted_deps: Vec<_> = dependencies.iter().collect();
735 sorted_deps.sort();
736 for dep in sorted_deps {
737 self.visit_node_recursive(dep, temp_edges, visited, temp_visited, result)?;
738 }
739 }
740
741 temp_visited.remove(node);
742 visited.insert(node.to_string());
743 result.push(node.to_string());
744
745 Ok(())
746 }
747}
748
749pub fn merge_schema_extensions(
752 main_spec: Value,
753 extension_paths: &[impl AsRef<Path>],
754) -> Result<Value> {
755 let mut result = main_spec;
756
757 for path in extension_paths {
758 let extension = load_extension_file(path.as_ref())?;
759 result = merge_json_objects_with_replacements(result, extension)?;
760 }
761
762 Ok(result)
763}
764
765fn normalize_operation_path(path: &str) -> String {
772 match path.split_once('#') {
773 Some((route, _fragment)) if route.starts_with('/') => route.to_string(),
774 _ => path.to_string(),
775 }
776}
777
778fn unwrap_annotation_allof(schema: &crate::openapi::Schema) -> &crate::openapi::Schema {
783 let crate::openapi::Schema::AllOf { all_of, .. } = schema else {
784 return schema;
785 };
786 let mut references = all_of.iter().filter(|s| s.reference().is_some());
787 let (Some(first), None) = (references.next(), references.next()) else {
788 return schema;
789 };
790 let others_annotation_only = all_of.iter().all(|member| {
791 if member.reference().is_some() {
792 return true;
793 }
794 serde_json::to_value(member)
795 .ok()
796 .and_then(|value| value.as_object().cloned())
797 .is_some_and(|object| {
798 object.keys().all(|key| {
799 matches!(
800 key.as_str(),
801 "title"
802 | "description"
803 | "deprecated"
804 | "readOnly"
805 | "writeOnly"
806 | "examples"
807 | "example"
808 | "externalDocs"
809 | "xml"
810 | "$comment"
811 ) || key.starts_with("x-")
812 })
813 })
814 });
815 if others_annotation_only {
816 first
817 } else {
818 schema
819 }
820}
821
822fn load_extension_file(path: &Path) -> Result<Value> {
826 let content = std::fs::read_to_string(path).map_err(|e| GeneratorError::FileError {
827 message: format!("Failed to read file {}: {}", path.display(), e),
828 })?;
829
830 let is_yaml = path
831 .extension()
832 .and_then(|extension| extension.to_str())
833 .is_some_and(|extension| {
834 extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml")
835 });
836
837 if is_yaml {
838 crate::spec_source::yaml_to_json_value(&content).map_err(|error| {
839 GeneratorError::FileError {
840 message: format!(
841 "Failed to parse schema extension {} as YAML: {}",
842 path.display(),
843 error
844 ),
845 }
846 })
847 } else {
848 serde_json::from_str(&content).map_err(|error| GeneratorError::FileError {
849 message: format!(
850 "Failed to parse schema extension {} as JSON: {}",
851 path.display(),
852 error
853 ),
854 })
855 }
856}
857
858fn merge_json_objects_with_replacements(main: Value, extension: Value) -> Result<Value> {
860 let replacements = extract_replacement_rules(&extension);
862
863 Ok(merge_json_objects_with_rules(
865 main,
866 extension,
867 &replacements,
868 ))
869}
870
871fn extract_replacement_rules(
873 extension: &Value,
874) -> std::collections::HashMap<String, (String, String)> {
875 let mut rules = std::collections::HashMap::new();
876
877 if let Some(x_replacements) = extension.get("x-replacements") {
878 if let Some(x_replacements_obj) = x_replacements.as_object() {
879 for (schema_name, replacement_rule) in x_replacements_obj {
880 if let Some(rule_obj) = replacement_rule.as_object() {
881 if let (Some(replace), Some(with)) = (
882 rule_obj.get("replace").and_then(|v| v.as_str()),
883 rule_obj.get("with").and_then(|v| v.as_str()),
884 ) {
885 rules.insert(schema_name.clone(), (replace.to_string(), with.to_string()));
886 }
888 }
889 }
890 }
891 }
892
893 rules
894}
895
896fn should_replace_variant(
898 schema_name: &str,
899 extension_refs: &[String],
900 replacements: &std::collections::HashMap<String, (String, String)>,
901) -> bool {
902 for (replace_schema, with_schema) in replacements.values() {
904 if schema_name == replace_schema {
905 let replacement_exists = extension_refs.iter().any(|ext_ref| {
907 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
908 ext_schema_name == with_schema
909 });
910
911 if replacement_exists {
912 return true;
913 }
914 }
915 }
916
917 extension_refs.iter().any(|ext_ref| {
919 let ext_schema_name = ext_ref.split('/').next_back().unwrap_or("");
920 schema_name == ext_schema_name
921 })
922}
923
924fn merge_json_objects_with_rules(
929 main: Value,
930 extension: Value,
931 replacements: &std::collections::HashMap<String, (String, String)>,
932) -> Value {
933 match (main, extension) {
934 (Value::Object(mut main_obj), Value::Object(ext_obj)) => {
936 let main_union_keyword = if main_obj.contains_key("oneOf") {
939 Some("oneOf")
940 } else if main_obj.contains_key("anyOf") {
941 Some("anyOf")
942 } else {
943 None
944 };
945 if let (Some(main_variants), Some(ext_variants)) = (
946 extract_schema_variants(&Value::Object(main_obj.clone())),
947 extract_schema_variants(&Value::Object(ext_obj.clone())),
948 ) {
949 let union_key = main_union_keyword.unwrap_or("oneOf");
950 println!(
951 "🔍 Merging union schemas ({union_key}): {} main variants, {} extension variants",
952 main_variants.len(),
953 ext_variants.len()
954 );
955 let mut merged_variants = Vec::new();
958 let extension_refs: Vec<String> = ext_variants
959 .iter()
960 .filter_map(|v| v.get("$ref").and_then(|r| r.as_str()))
961 .map(|s| s.to_string())
962 .collect();
963
964 for main_variant in main_variants {
966 if let Some(main_ref) = main_variant.get("$ref").and_then(|r| r.as_str()) {
967 let schema_name = main_ref.split('/').next_back().unwrap_or("");
969 let should_replace =
970 should_replace_variant(schema_name, &extension_refs, replacements);
971
972 if should_replace {
973 println!("🔄 REPLACING {} (explicit rule)", schema_name);
974 }
975
976 if !should_replace {
977 merged_variants.push(main_variant);
978 }
979 } else {
980 merged_variants.push(main_variant);
982 }
983 }
984
985 for ext_variant in ext_variants {
987 merged_variants.push(ext_variant);
988 }
989
990 main_obj.remove("oneOf");
992 main_obj.remove("anyOf");
993 main_obj.insert(union_key.to_string(), Value::Array(merged_variants));
994
995 for (key, ext_value) in ext_obj {
997 if key != "oneOf" && key != "anyOf" {
998 match main_obj.get(&key) {
999 Some(main_value) => {
1000 let merged_value = merge_json_objects_with_rules(
1001 main_value.clone(),
1002 ext_value,
1003 replacements,
1004 );
1005 main_obj.insert(key, merged_value);
1006 }
1007 None => {
1008 main_obj.insert(key, ext_value);
1009 }
1010 }
1011 }
1012 }
1013
1014 return Value::Object(main_obj);
1015 }
1016
1017 for (key, ext_value) in ext_obj {
1019 match main_obj.get(&key) {
1020 Some(main_value) => {
1021 let merged_value = merge_json_objects_with_rules(
1023 main_value.clone(),
1024 ext_value,
1025 replacements,
1026 );
1027 main_obj.insert(key, merged_value);
1028 }
1029 None => {
1030 main_obj.insert(key, ext_value);
1032 }
1033 }
1034 }
1035 Value::Object(main_obj)
1036 }
1037
1038 (Value::Array(mut main_arr), Value::Array(ext_arr)) => {
1040 main_arr.extend(ext_arr);
1041 Value::Array(main_arr)
1042 }
1043
1044 (_, extension) => extension,
1046 }
1047}
1048
1049fn extract_schema_variants(obj: &Value) -> Option<Vec<Value>> {
1051 if let Value::Object(map) = obj {
1052 if let Some(Value::Array(variants)) = map.get("oneOf") {
1053 return Some(variants.clone());
1054 }
1055 if let Some(Value::Array(variants)) = map.get("anyOf") {
1056 return Some(variants.clone());
1057 }
1058 }
1059 None
1060}
1061
1062pub struct SchemaAnalyzer {
1063 schemas: BTreeMap<String, Schema>,
1064 resolved_cache: BTreeMap<String, AnalyzedSchema>,
1065 openapi_spec: Value,
1066 current_schema_name: Option<String>,
1067 component_parameters: BTreeMap<String, crate::openapi::Parameter>,
1068 type_mapper: TypeMapper,
1073}
1074
1075impl SchemaAnalyzer {
1076 fn uses_aws_query_conventions(&self) -> bool {
1077 self.openapi_spec
1078 .pointer("/info/x-providerName")
1079 .and_then(Value::as_str)
1080 .is_some_and(|provider| provider.eq_ignore_ascii_case("amazonaws.com"))
1081 }
1082
1083 pub fn new(openapi_spec: Value) -> Result<Self> {
1087 Self::with_type_mapper(openapi_spec, TypeMapper::default())
1088 }
1089
1090 pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1094 disambiguate_component_schema_names(&mut openapi_spec);
1095 let spec: OpenApiSpec =
1096 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
1097 let schemas = Self::extract_schemas(&spec)?;
1098
1099 let component_parameters = spec
1100 .components
1101 .as_ref()
1102 .and_then(|c| c.parameters.as_ref())
1103 .cloned()
1104 .unwrap_or_default();
1105 Ok(Self {
1106 schemas,
1107 resolved_cache: BTreeMap::new(),
1108 openapi_spec,
1109 current_schema_name: None,
1110 component_parameters,
1111 type_mapper,
1112 })
1113 }
1114
1115 pub fn new_with_extensions(
1118 openapi_spec: Value,
1119 extension_paths: &[std::path::PathBuf],
1120 ) -> Result<Self> {
1121 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1122 Self::new(merged_spec)
1123 }
1124
1125 pub fn new_with_extensions_and_type_mapper(
1128 openapi_spec: Value,
1129 extension_paths: &[std::path::PathBuf],
1130 type_mapper: TypeMapper,
1131 ) -> Result<Self> {
1132 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1133 Self::with_type_mapper(merged_spec, type_mapper)
1134 }
1135
1136 pub fn type_mapper(&self) -> &TypeMapper {
1140 &self.type_mapper
1141 }
1142
1143 fn generate_context_aware_name(
1146 &self,
1147 base_context: &str,
1148 type_hint: &str,
1149 index: usize,
1150 schema: Option<&Schema>,
1151 ) -> String {
1152 if let Some(schema) = schema {
1154 if type_hint == "Array"
1156 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
1157 {
1158 if let Some(items_schema) = &schema.details().items {
1159 if let Some(item_type) = items_schema.schema_type() {
1161 match item_type {
1162 OpenApiSchemaType::Object => {
1163 return format!("{base_context}ItemArray");
1164 }
1165 OpenApiSchemaType::String => {
1166 return format!("{base_context}StringArray");
1167 }
1168 _ => {}
1169 }
1170 }
1171 }
1172 }
1173 }
1174
1175 match type_hint {
1177 "Array" => {
1178 format!("{base_context}Array")
1180 }
1181 "Variant" | "InlineVariant" => {
1182 if index == 0 {
1184 format!("{base_context}{type_hint}")
1185 } else {
1186 format!("{}{}{}", base_context, type_hint, index + 1)
1187 }
1188 }
1189 _ => {
1190 format!("{base_context}{type_hint}{index}")
1192 }
1193 }
1194 }
1195
1196 fn to_pascal_case(&self, s: &str) -> String {
1198 s.split(['_', '-'])
1199 .filter(|part| !part.is_empty())
1200 .map(|part| {
1201 let mut chars = part.chars();
1202 match chars.next() {
1203 None => String::new(),
1204 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1205 }
1206 })
1207 .collect()
1208 }
1209
1210 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
1211 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
1216 Ok(schemas
1217 .map(|m| {
1218 m.iter()
1219 .map(|(k, v)| (k.clone(), v.clone()))
1220 .collect::<BTreeMap<_, _>>()
1221 })
1222 .unwrap_or_default())
1223 }
1224
1225 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
1226 let validation_context = ValidationContext {
1227 openapi_version: self
1228 .openapi_spec
1229 .get("openapi")
1230 .and_then(Value::as_str)
1231 .unwrap_or_default()
1232 .to_string(),
1233 json_schema_dialect: self
1234 .openapi_spec
1235 .get("jsonSchemaDialect")
1236 .and_then(Value::as_str)
1237 .map(str::to_string),
1238 component_schemas: self
1239 .openapi_spec
1240 .pointer("/components/schemas")
1241 .and_then(Value::as_object)
1242 .map(|schemas| {
1243 schemas
1244 .iter()
1245 .map(|(name, schema)| (name.clone(), schema.clone()))
1246 .collect()
1247 })
1248 .unwrap_or_default(),
1249 };
1250 let mut analysis = SchemaAnalysis {
1251 schemas: BTreeMap::new(),
1252 dependencies: DependencyGraph::new(),
1253 patterns: DetectedPatterns {
1254 tagged_enum_schemas: HashSet::new(),
1255 untagged_enum_schemas: HashSet::new(),
1256 type_mappings: BTreeMap::new(),
1257 },
1258 operations: BTreeMap::new(),
1259 operation_responses: BTreeMap::new(),
1260 operation_id_aliases: BTreeMap::new(),
1261 used_type_features: crate::type_mapping::UsedFeatures::default(),
1262 enum_extensions: BTreeMap::new(),
1263 validation_context,
1264 };
1265
1266 self.detect_patterns(&mut analysis.patterns)?;
1268
1269 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
1271 for schema_name in schema_names {
1272 let analyzed = self.analyze_schema(&schema_name)?;
1273
1274 for dep in &analyzed.dependencies {
1276 analysis
1277 .dependencies
1278 .add_dependency(schema_name.clone(), dep.clone());
1279 }
1280
1281 analysis.schemas.insert(schema_name, analyzed);
1282 }
1283
1284 for (inline_name, inline_schema) in &self.resolved_cache {
1287 if !analysis.schemas.contains_key(inline_name) {
1288 analysis
1290 .schemas
1291 .insert(inline_name.clone(), inline_schema.clone());
1292
1293 for dep in &inline_schema.dependencies {
1295 analysis
1296 .dependencies
1297 .add_dependency(inline_name.clone(), dep.clone());
1298 }
1299
1300 let mut schemas_to_update = Vec::new();
1305 for (schema_name, schema) in &analysis.schemas {
1306 if schema_name == inline_name {
1308 continue;
1309 }
1310
1311 if schema.dependencies.contains(inline_name) {
1312 schemas_to_update.push(schema_name.clone());
1314 }
1315 }
1316
1317 for schema_name in schemas_to_update {
1319 analysis
1320 .dependencies
1321 .add_dependency(schema_name, inline_name.clone());
1322 }
1323 }
1324 }
1325
1326 self.analyze_operations(&mut analysis)?;
1328
1329 for (inline_name, inline_schema) in &self.resolved_cache {
1332 if !analysis.schemas.contains_key(inline_name) {
1333 analysis
1334 .schemas
1335 .insert(inline_name.clone(), inline_schema.clone());
1336
1337 for dep in &inline_schema.dependencies {
1339 analysis
1340 .dependencies
1341 .add_dependency(inline_name.clone(), dep.clone());
1342 }
1343 }
1344 }
1345
1346 disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
1347
1348 analysis.used_type_features = self.type_mapper.used_features();
1352
1353 for (name, analyzed) in &analysis.schemas {
1358 let enum_value_count = match &analyzed.schema_type {
1359 SchemaType::StringEnum { values } => values.len(),
1360 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1361 _ => continue,
1362 };
1363 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1364 analysis.enum_extensions.insert(name.clone(), ext);
1365 }
1366 }
1367
1368 Ok(analysis)
1369 }
1370
1371 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1372 for (schema_name, schema) in &self.schemas {
1373 if self.is_discriminated_union(schema) {
1375 patterns.tagged_enum_schemas.insert(schema_name.clone());
1376
1377 if let Some(mappings) = self.extract_type_mappings(schema)? {
1379 patterns.type_mappings.insert(schema_name.clone(), mappings);
1380 }
1381 }
1382 else if self.is_simple_union(schema) {
1384 patterns.untagged_enum_schemas.insert(schema_name.clone());
1385 }
1386 }
1387
1388 Ok(())
1389 }
1390
1391 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1392 if schema.is_discriminated_union() {
1394 return true;
1395 }
1396
1397 if let Some(variants) = schema.union_variants() {
1399 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1400 }
1401
1402 false
1403 }
1404
1405 fn all_variants_have_unique_const_values(&self, variants: &[Schema], field_name: &str) -> bool {
1406 let mut values = HashSet::new();
1407
1408 variants.iter().all(|variant| {
1409 let schema = if let Some(ref_str) = variant.reference() {
1410 let Some(schema_name) = self.extract_schema_name(ref_str) else {
1411 return false;
1412 };
1413 let Some(schema) = self.schemas.get(schema_name) else {
1414 return false;
1415 };
1416 schema
1417 } else {
1418 variant
1419 };
1420
1421 self.extract_discriminator_value_for_field(schema, field_name)
1422 .is_some_and(|value| values.insert(value))
1423 })
1424 }
1425
1426 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1435 if let Some(ref_str) = schema.reference() {
1437 return match self
1438 .extract_schema_name(ref_str)
1439 .and_then(|n| self.schemas.get(n))
1440 {
1441 Some(target) => self.branch_resolves_to_object(target),
1442 None => false,
1443 };
1444 }
1445 if matches!(
1448 schema,
1449 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1450 ) {
1451 return true;
1452 }
1453 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1454 return true;
1455 }
1456 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1457 return true;
1458 }
1459 false
1462 }
1463
1464 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1468 if variants.is_empty() {
1469 return None;
1470 }
1471
1472 let first_variant = &variants[0];
1474 let first_schema = if let Some(ref_str) = first_variant.reference() {
1475 let schema_name = self.extract_schema_name(ref_str)?;
1476 self.schemas.get(schema_name)?
1477 } else {
1478 first_variant
1479 };
1480
1481 let properties = first_schema.details().properties.as_ref()?;
1482 let mut candidates: Vec<String> = Vec::new();
1483
1484 for (field_name, field_schema) in properties {
1485 let details = field_schema.details();
1486 let is_const = details.const_value.is_some()
1487 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1488 || details.extra.contains_key("const");
1489 if is_const {
1490 candidates.push(field_name.clone());
1491 }
1492 }
1493
1494 if candidates.is_empty() {
1495 return None;
1496 }
1497
1498 candidates.sort_by(|a, b| {
1500 if a == "type" {
1501 std::cmp::Ordering::Less
1502 } else if b == "type" {
1503 std::cmp::Ordering::Greater
1504 } else {
1505 a.cmp(b)
1506 }
1507 });
1508
1509 for candidate in &candidates {
1515 if self.all_variants_have_unique_const_values(variants, candidate) {
1516 return Some(candidate.clone());
1517 }
1518 }
1519
1520 None
1521 }
1522
1523 fn is_simple_union(&self, schema: &Schema) -> bool {
1524 if let Some(variants) = schema.union_variants() {
1525 if variants.len() > 1 && !schema.is_nullable_pattern() {
1527 let has_refs = variants.iter().any(|v| v.is_reference());
1528 return has_refs;
1529 }
1530 }
1531 false
1532 }
1533
1534 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1535 let variants = schema.union_variants().ok_or_else(|| {
1536 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1537 })?;
1538
1539 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1541 discriminator.property_name.clone()
1542 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1543 detected
1544 } else {
1545 "type".to_string() };
1547
1548 let mut mappings = BTreeMap::new();
1549
1550 for variant in variants {
1551 if let Some(ref_str) = variant.reference() {
1552 if let Some(type_name) = self.extract_schema_name(ref_str) {
1553 if let Some(variant_schema) = self.schemas.get(type_name) {
1554 if let Some(discriminator_value) = self
1555 .extract_discriminator_value_for_field(
1556 variant_schema,
1557 &discriminator_field,
1558 )
1559 {
1560 mappings.insert(type_name.to_string(), discriminator_value);
1561 }
1562 }
1563 }
1564 }
1565 }
1566
1567 if mappings.is_empty() {
1568 Ok(None)
1569 } else {
1570 Ok(Some(mappings))
1571 }
1572 }
1573
1574 #[allow(dead_code)]
1575 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1576 self.extract_discriminator_value_for_field(schema, "type")
1577 }
1578
1579 fn extract_discriminator_value_for_field(
1580 &self,
1581 schema: &Schema,
1582 field_name: &str,
1583 ) -> Option<String> {
1584 if let Some(properties) = &schema.details().properties {
1585 if let Some(type_field) = properties.get(field_name) {
1586 if let Some(const_value) = &type_field.details().const_value {
1588 if let Some(value) = const_value.as_str() {
1589 return Some(value.to_string());
1590 }
1591 }
1592 if let Some(enum_values) = &type_field.details().enum_values {
1594 if enum_values.len() == 1 {
1595 return enum_values[0].as_str().map(|s| s.to_string());
1596 }
1597 }
1598 if let Some(const_value) = type_field.details().extra.get("const") {
1600 return const_value.as_str().map(|s| s.to_string());
1601 }
1602 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1604 if stainless_const.as_bool() == Some(true) {
1605 if let Some(default_value) = &type_field.details().default {
1606 if let Some(value) = default_value.as_str() {
1607 return Some(value.to_string());
1608 }
1609 }
1610 }
1611 }
1612 }
1613 }
1614 None
1615 }
1616
1617 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1618 schema.reference().or_else(|| schema.recursive_reference())
1619 }
1620
1621 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1622 if ref_str == "#" {
1623 return None; }
1625
1626 let parts: Vec<&str> = ref_str.split('/').collect();
1627
1628 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1630 return Some(parts[3]);
1631 }
1632
1633 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1636 return Some(parts[2]);
1637 }
1638
1639 let last = parts.last()?;
1645 if last.is_empty()
1646 || last.chars().all(|c| c.is_ascii_digit())
1647 || matches!(
1648 *last,
1649 "schema" | "properties" | "items" | "additionalProperties"
1650 )
1651 {
1652 return None;
1653 }
1654 let first = last.chars().next().unwrap_or(' ');
1655 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1656 return None;
1657 }
1658 Some(last)
1659 }
1660
1661 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1662 if let Some(cached) = self.resolved_cache.get(schema_name) {
1664 return Ok(cached.clone());
1665 }
1666
1667 self.current_schema_name = Some(schema_name.to_string());
1669
1670 let schema = self
1671 .schemas
1672 .get(schema_name)
1673 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1674 .clone();
1675
1676 self.resolved_cache.insert(
1678 schema_name.to_string(),
1679 AnalyzedSchema {
1680 name: schema_name.to_string(),
1681 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1682 schema_type: SchemaType::Reference {
1683 target: "placeholder".to_string(),
1684 },
1685 dependencies: HashSet::new(),
1686 nullable: false,
1687 description: None,
1688 default: None,
1689 },
1690 );
1691
1692 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1693
1694 self.resolved_cache
1696 .insert(schema_name.to_string(), analyzed.clone());
1697
1698 Ok(analyzed)
1699 }
1700
1701 fn analyze_schema_value(
1702 &mut self,
1703 schema: &Schema,
1704 schema_name: &str,
1705 ) -> Result<AnalyzedSchema> {
1706 let details = schema.details();
1707 let description = details.description.clone();
1708 let nullable = details.is_nullable() || schema.type_array_contains_null();
1710 let mut dependencies = HashSet::new();
1711
1712 let schema_type = match schema {
1713 Schema::Reference { reference, .. } => {
1714 match self.extract_schema_name(reference) {
1719 Some(name) => {
1720 let target = name.to_string();
1721 dependencies.insert(target.clone());
1722 SchemaType::Reference { target }
1723 }
1724 None => {
1725 eprintln!(
1726 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1727 reference
1728 );
1729 SchemaType::Primitive {
1730 rust_type: "serde_json::Value".to_string(),
1731 serde_with: None,
1732 }
1733 }
1734 }
1735 }
1736 Schema::RecursiveRef { recursive_ref, .. }
1737 | Schema::DynamicRef {
1738 dynamic_ref: recursive_ref,
1739 ..
1740 } => {
1741 if recursive_ref == "#" {
1747 dependencies.insert(schema_name.to_string());
1748 SchemaType::Reference {
1749 target: schema_name.to_string(),
1750 }
1751 } else {
1752 let target = self
1753 .extract_schema_name(recursive_ref)
1754 .unwrap_or(schema_name)
1755 .to_string();
1756 dependencies.insert(target.clone());
1757 SchemaType::Reference { target }
1758 }
1759 }
1760 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1761 let primary = schema
1762 .schema_type()
1763 .cloned()
1764 .unwrap_or(OpenApiSchemaType::Object);
1765 let format = details.format.as_deref();
1766 match primary {
1767 OpenApiSchemaType::String => {
1768 if let Some(values) = details.string_enum_values() {
1769 SchemaType::StringEnum { values }
1770 } else {
1771 SchemaType::Primitive {
1772 rust_type: self.type_mapper.string_format(format).rust_type,
1773 serde_with: None,
1774 }
1775 }
1776 }
1777 OpenApiSchemaType::Integer => SchemaType::Primitive {
1778 rust_type: self.type_mapper.integer_format(format).rust_type,
1779 serde_with: None,
1780 },
1781 OpenApiSchemaType::Number => SchemaType::Primitive {
1782 rust_type: self.type_mapper.number_format(format).rust_type,
1783 serde_with: None,
1784 },
1785 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1786 rust_type: self.type_mapper.boolean().rust_type,
1787 serde_with: None,
1788 },
1789 OpenApiSchemaType::Array => {
1790 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1792 }
1793 OpenApiSchemaType::Object => {
1794 if self.should_use_dynamic_json(schema) {
1796 SchemaType::Primitive {
1797 rust_type: self.type_mapper.dynamic_json().rust_type,
1798 serde_with: None,
1799 }
1800 } else {
1801 self.analyze_object_schema(schema, &mut dependencies)?
1803 }
1804 }
1805 _ => SchemaType::Primitive {
1806 rust_type: self.type_mapper.dynamic_json().rust_type,
1807 serde_with: None,
1808 },
1809 }
1810 }
1811 Schema::AnyOf {
1812 any_of,
1813 discriminator,
1814 ..
1815 } => {
1816 self.analyze_anyof_union(
1818 any_of,
1819 discriminator.as_ref(),
1820 &mut dependencies,
1821 schema_name,
1822 )?
1823 }
1824 Schema::OneOf {
1825 one_of,
1826 discriminator,
1827 ..
1828 } => {
1829 self.analyze_oneof_union(
1831 one_of,
1832 discriminator.as_ref(),
1833 schema_name,
1834 &mut dependencies,
1835 )?
1836 }
1837 Schema::AllOf { all_of, .. } => {
1838 self.analyze_allof_composition(all_of, &mut dependencies)?
1840 }
1841 Schema::Untyped { .. } => {
1842 if let Some(inferred) = schema.inferred_type() {
1844 match inferred {
1845 OpenApiSchemaType::Object => {
1846 if self.should_use_dynamic_json(schema) {
1847 SchemaType::Primitive {
1848 rust_type: "serde_json::Value".to_string(),
1849 serde_with: None,
1850 }
1851 } else {
1852 self.analyze_object_schema(schema, &mut dependencies)?
1853 }
1854 }
1855 OpenApiSchemaType::String if details.is_string_enum() => {
1856 SchemaType::StringEnum {
1857 values: details.string_enum_values().unwrap_or_default(),
1858 }
1859 }
1860 _ => SchemaType::Primitive {
1861 rust_type: "serde_json::Value".to_string(),
1862 serde_with: None,
1863 },
1864 }
1865 } else {
1866 SchemaType::Primitive {
1867 rust_type: "serde_json::Value".to_string(),
1868 serde_with: None,
1869 }
1870 }
1871 }
1872 };
1873
1874 Ok(AnalyzedSchema {
1875 name: schema_name.to_string(),
1876 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1878 dependencies,
1879 nullable,
1880 description,
1881 default: details.default.clone(),
1882 })
1883 }
1884
1885 fn analyze_object_schema(
1886 &mut self,
1887 schema: &Schema,
1888 dependencies: &mut HashSet<String>,
1889 ) -> Result<SchemaType> {
1890 let details = schema.details();
1891 let properties = &details.properties;
1892 let required = details
1893 .required
1894 .as_ref()
1895 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1896 .unwrap_or_default();
1897
1898 let mut property_info = BTreeMap::new();
1899
1900 if let Some(props) = properties {
1901 for (prop_name, prop_schema) in props {
1902 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1904 if self.should_use_dynamic_json(prop_schema) {
1906 SchemaType::Primitive {
1908 rust_type: "serde_json::Value".to_string(),
1909 serde_with: None,
1910 }
1911 } else if prop_schema.is_nullable_pattern()
1912 && let Some(non_null) = prop_schema.non_null_variant()
1913 {
1914 self.analyze_property_schema_with_context(
1922 non_null,
1923 Some(prop_name),
1924 dependencies,
1925 )?
1926 } else {
1927 let context_name = self
1930 .current_schema_name
1931 .clone()
1932 .unwrap_or_else(|| "Unknown".to_string());
1933
1934 let prop_pascal = self.to_pascal_case(prop_name);
1936 let mut union_type_name = format!("{context_name}{prop_pascal}");
1937
1938 if self.schemas.contains_key(&union_type_name)
1941 || self.resolved_cache.contains_key(&union_type_name)
1942 {
1943 let mut suffix = 2;
1944 loop {
1945 let candidate = format!("{union_type_name}Union{suffix}");
1946 if !self.schemas.contains_key(&candidate)
1947 && !self.resolved_cache.contains_key(&candidate)
1948 {
1949 union_type_name = candidate;
1950 break;
1951 }
1952 suffix += 1;
1953 if suffix > 1000 {
1954 break;
1955 }
1956 }
1957 }
1958
1959 let union_schema_type = self.analyze_anyof_union(
1961 any_of,
1962 prop_schema.discriminator(),
1963 dependencies,
1964 &union_type_name,
1965 )?;
1966
1967 self.resolved_cache.insert(
1969 union_type_name.clone(),
1970 AnalyzedSchema {
1971 name: union_type_name.clone(),
1972 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1973 schema_type: union_schema_type,
1974 dependencies: HashSet::new(),
1975 nullable: false,
1976 description: prop_schema.details().description.clone(),
1977 default: None,
1978 },
1979 );
1980
1981 dependencies.insert(union_type_name.clone());
1983 SchemaType::Reference {
1984 target: union_type_name,
1985 }
1986 }
1987 } else if let Schema::OneOf {
1988 one_of,
1989 discriminator,
1990 ..
1991 } = prop_schema
1992 {
1993 if prop_schema.is_nullable_pattern()
2000 && let Some(non_null) = prop_schema.non_null_variant()
2001 {
2002 let unwrapped = self.analyze_property_schema_with_context(
2003 non_null,
2004 Some(prop_name),
2005 dependencies,
2006 )?;
2007 let prop_details = prop_schema.details();
2008 let prop_nullable = true;
2009 let prop_description = prop_details.description.clone();
2010 let prop_default = prop_details.default.clone();
2011 property_info.insert(
2012 prop_name.clone(),
2013 PropertyInfo {
2014 schema_type: unwrapped,
2015 nullable: prop_nullable,
2016 description: prop_description,
2017 default: prop_default,
2018 serde_attrs: Vec::new(),
2019 constraints: PropertyConstraints::from_schema_details(prop_details),
2020 },
2021 );
2022 continue;
2023 }
2024
2025 let context_name = self
2027 .current_schema_name
2028 .clone()
2029 .unwrap_or_else(|| "Unknown".to_string());
2030 let prop_pascal = self.to_pascal_case(prop_name);
2031 let mut union_type_name = format!("{context_name}{prop_pascal}");
2032 if self.schemas.contains_key(&union_type_name)
2034 || self.resolved_cache.contains_key(&union_type_name)
2035 {
2036 let mut suffix = 2;
2037 loop {
2038 let candidate = format!("{union_type_name}Union{suffix}");
2039 if !self.schemas.contains_key(&candidate)
2040 && !self.resolved_cache.contains_key(&candidate)
2041 {
2042 union_type_name = candidate;
2043 break;
2044 }
2045 suffix += 1;
2046 if suffix > 1000 {
2047 break;
2048 }
2049 }
2050 }
2051
2052 let union_schema_type = self.analyze_oneof_union(
2054 one_of,
2055 discriminator.as_ref(),
2056 &union_type_name,
2057 dependencies,
2058 )?;
2059
2060 self.resolved_cache.insert(
2062 union_type_name.clone(),
2063 AnalyzedSchema {
2064 name: union_type_name.clone(),
2065 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2066 schema_type: union_schema_type,
2067 dependencies: HashSet::new(),
2068 nullable: false,
2069 description: prop_schema.details().description.clone(),
2070 default: None,
2071 },
2072 );
2073
2074 dependencies.insert(union_type_name.clone());
2076 SchemaType::Reference {
2077 target: union_type_name,
2078 }
2079 } else {
2080 self.analyze_property_schema_with_context(
2082 prop_schema,
2083 Some(prop_name),
2084 dependencies,
2085 )?
2086 };
2087
2088 let prop_details = prop_schema.details();
2089 let prop_nullable = prop_schema.is_nullable_any();
2091 let prop_description = prop_details.description.clone();
2092 let prop_default = prop_details.default.clone();
2093
2094 property_info.insert(
2095 prop_name.clone(),
2096 PropertyInfo {
2097 schema_type: prop_type,
2098 nullable: prop_nullable,
2099 description: prop_description,
2100 default: prop_default,
2101 serde_attrs: Vec::new(),
2102 constraints: PropertyConstraints::from_schema_details(prop_details),
2103 },
2104 );
2105 }
2106 }
2107
2108 let typed_enabled = self
2116 .type_mapper
2117 .config()
2118 .shape
2119 .as_ref()
2120 .and_then(|s| s.additional_properties_typed)
2121 .unwrap_or(true);
2122
2123 let additional_properties = match &details.additional_properties {
2124 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
2125 ObjectAdditionalProperties::Untyped
2126 }
2127 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
2128 ObjectAdditionalProperties::Forbidden
2129 }
2130 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
2131 let analyzed =
2132 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
2133 ObjectAdditionalProperties::Typed {
2134 value_type: Box::new(analyzed),
2135 }
2136 }
2137 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
2138 ObjectAdditionalProperties::Untyped
2140 }
2141 None => ObjectAdditionalProperties::Forbidden,
2142 };
2143
2144 Ok(SchemaType::Object {
2145 properties: property_info,
2146 required,
2147 additional_properties,
2148 })
2149 }
2150
2151 fn analyze_property_schema_with_context(
2152 &mut self,
2153 schema: &Schema,
2154 property_name: Option<&str>,
2155 dependencies: &mut HashSet<String>,
2156 ) -> Result<SchemaType> {
2157 if let Some(ref_str) = self.get_any_reference(schema) {
2158 let target_opt = if ref_str == "#" {
2159 Some(
2160 self.find_recursive_anchor_schema()
2161 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2162 )
2163 } else {
2164 self.extract_schema_name(ref_str).map(|s| s.to_string())
2165 };
2166 match target_opt {
2167 Some(target) => {
2168 dependencies.insert(target.clone());
2169 return Ok(SchemaType::Reference { target });
2170 }
2171 None => {
2172 eprintln!(
2173 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2174 ref_str
2175 );
2176 return Ok(SchemaType::Primitive {
2177 rust_type: "serde_json::Value".to_string(),
2178 serde_with: None,
2179 });
2180 }
2181 }
2182 }
2183
2184 if let Some(schema_type) = schema.schema_type() {
2185 match schema_type {
2186 OpenApiSchemaType::String => {
2187 if let Some(enum_values) = schema.details().string_enum_values() {
2189 let context_name = self
2192 .current_schema_name
2193 .clone()
2194 .unwrap_or_else(|| "Unknown".to_string());
2195
2196 let primary_name = if let Some(prop_name) = property_name {
2198 let prop_pascal = self.to_pascal_case(prop_name);
2200 format!("{context_name}{prop_pascal}")
2201 } else {
2202 let suffix = if !enum_values.is_empty() {
2205 let first_value = self.to_pascal_case(&enum_values[0]);
2206 format!("{first_value}Enum")
2207 } else {
2208 "StringEnum".to_string()
2209 };
2210 format!("{context_name}{suffix}")
2211 };
2212
2213 return Ok(self.hoist_inline_string_enum(
2214 schema,
2215 enum_values,
2216 primary_name,
2217 dependencies,
2218 ));
2219 } else {
2220 let mapped = self
2226 .type_mapper
2227 .string_format(schema.details().format.as_deref());
2228 return Ok(SchemaType::Primitive {
2229 rust_type: mapped.rust_type,
2230 serde_with: mapped.serde_with,
2231 });
2232 }
2233 }
2234 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2235 let details = schema.details();
2236 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2237 return Ok(SchemaType::Primitive {
2238 rust_type,
2239 serde_with: None,
2240 });
2241 }
2242 OpenApiSchemaType::Boolean => {
2243 return Ok(SchemaType::Primitive {
2244 rust_type: "bool".to_string(),
2245 serde_with: None,
2246 });
2247 }
2248 OpenApiSchemaType::Array => {
2249 let context_name = if let Some(prop_name) = property_name {
2251 let prop_pascal = self.to_pascal_case(prop_name);
2253 format!(
2254 "{}{}",
2255 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2256 prop_pascal
2257 )
2258 } else {
2259 "ArrayItem".to_string()
2261 };
2262 return self.analyze_array_schema(schema, &context_name, dependencies);
2263 }
2264 OpenApiSchemaType::Object => {
2265 if self.should_use_dynamic_json(schema) {
2267 return Ok(SchemaType::Primitive {
2268 rust_type: "serde_json::Value".to_string(),
2269 serde_with: None,
2270 });
2271 }
2272 let object_type_name = if let Some(prop_name) = property_name {
2274 let prop_pascal = self.to_pascal_case(prop_name);
2276 format!(
2277 "{}{}",
2278 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2279 prop_pascal
2280 )
2281 } else {
2282 format!(
2284 "{}Object",
2285 self.current_schema_name.as_deref().unwrap_or("Unknown")
2286 )
2287 };
2288
2289 let object_type = self.analyze_object_schema(schema, dependencies)?;
2291
2292 let inline_schema = AnalyzedSchema {
2294 name: object_type_name.clone(),
2295 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2296 schema_type: object_type,
2297 dependencies: dependencies.clone(),
2298 nullable: false,
2299 description: schema.details().description.clone(),
2300 default: None,
2301 };
2302
2303 self.resolved_cache
2305 .insert(object_type_name.clone(), inline_schema);
2306 dependencies.insert(object_type_name.clone());
2307
2308 return Ok(SchemaType::Reference {
2310 target: object_type_name,
2311 });
2312 }
2313 _ => {
2314 return Ok(SchemaType::Primitive {
2315 rust_type: "serde_json::Value".to_string(),
2316 serde_with: None,
2317 });
2318 }
2319 }
2320 }
2321
2322 if schema.is_nullable_pattern() {
2324 if let Some(non_null) = schema.non_null_variant() {
2325 return self.analyze_property_schema_with_context(
2326 non_null,
2327 property_name,
2328 dependencies,
2329 );
2330 }
2331 }
2332
2333 if self.should_use_dynamic_json(schema) {
2335 return Ok(SchemaType::Primitive {
2336 rust_type: "serde_json::Value".to_string(),
2337 serde_with: None,
2338 });
2339 }
2340
2341 if let Schema::AllOf { all_of, .. } = schema {
2343 return self.analyze_allof_composition(all_of, dependencies);
2344 }
2345
2346 if let Some(variants) = schema.union_variants() {
2348 match variants.len().cmp(&1) {
2349 std::cmp::Ordering::Equal => {
2350 return self.analyze_property_schema_with_context(
2352 &variants[0],
2353 property_name,
2354 dependencies,
2355 );
2356 }
2357 std::cmp::Ordering::Greater => {
2358 let union_name = if let Some(prop_name) = property_name {
2361 let prop_pascal = self.to_pascal_case(prop_name);
2363 format!(
2364 "{}{}",
2365 self.current_schema_name.as_deref().unwrap_or(""),
2366 prop_pascal
2367 )
2368 } else {
2369 "UnionType".to_string()
2370 };
2371
2372 if let Schema::OneOf {
2374 one_of,
2375 discriminator,
2376 ..
2377 } = schema
2378 {
2379 let oneof_result = self.analyze_oneof_union(
2381 one_of,
2382 discriminator.as_ref(),
2383 &union_name,
2384 dependencies,
2385 )?;
2386
2387 if let SchemaType::Union {
2389 variants: _union_variants,
2390 } = &oneof_result
2391 {
2392 self.resolved_cache.insert(
2394 union_name.clone(),
2395 AnalyzedSchema {
2396 name: union_name.clone(),
2397 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2398 schema_type: oneof_result.clone(),
2399 dependencies: dependencies.clone(),
2400 nullable: false,
2401 description: schema.details().description.clone(),
2402 default: None,
2403 },
2404 );
2405
2406 dependencies.insert(union_name.clone());
2408 return Ok(SchemaType::Reference { target: union_name });
2409 }
2410
2411 return Ok(oneof_result);
2412 } else if let Schema::AnyOf {
2413 any_of,
2414 discriminator,
2415 ..
2416 } = schema
2417 {
2418 let union_analysis = self.analyze_anyof_union(
2420 any_of,
2421 discriminator.as_ref(),
2422 dependencies,
2423 &union_name,
2424 )?;
2425 return Ok(union_analysis);
2426 } else {
2427 let mut union_variants = Vec::new();
2430 for variant in variants {
2431 if let Some(ref_str) = variant.reference() {
2432 if let Some(target) = self.extract_schema_name(ref_str) {
2433 dependencies.insert(target.to_string());
2434 union_variants.push(SchemaRef {
2435 target: target.to_string(),
2436 nullable: false,
2437 });
2438 }
2439 }
2440 }
2441 return Ok(SchemaType::Union {
2442 variants: union_variants,
2443 });
2444 }
2445 }
2446 std::cmp::Ordering::Less => {}
2447 }
2448 }
2449
2450 if let Some(inferred_type) = schema.inferred_type() {
2452 match inferred_type {
2453 OpenApiSchemaType::Object => {
2454 if self.should_use_dynamic_json(schema) {
2456 return Ok(SchemaType::Primitive {
2457 rust_type: "serde_json::Value".to_string(),
2458 serde_with: None,
2459 });
2460 }
2461 return self.analyze_object_schema(schema, dependencies);
2462 }
2463 OpenApiSchemaType::Array => {
2464 let context_name = if let Some(prop_name) = property_name {
2465 let prop_pascal = self.to_pascal_case(prop_name);
2467 format!(
2468 "{}{}",
2469 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2470 prop_pascal
2471 )
2472 } else {
2473 "ArrayItem".to_string()
2475 };
2476 return self.analyze_array_schema(schema, &context_name, dependencies);
2477 }
2478 OpenApiSchemaType::String => {
2479 if let Some(enum_values) = schema.details().string_enum_values() {
2480 return Ok(SchemaType::StringEnum {
2481 values: enum_values,
2482 });
2483 } else {
2484 return Ok(SchemaType::Primitive {
2485 rust_type: "String".to_string(),
2486 serde_with: None,
2487 });
2488 }
2489 }
2490 _ => {
2491 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2493 return Ok(SchemaType::Primitive {
2494 rust_type,
2495 serde_with: None,
2496 });
2497 }
2498 }
2499 }
2500
2501 Ok(SchemaType::Primitive {
2502 rust_type: "serde_json::Value".to_string(),
2503 serde_with: None,
2504 })
2505 }
2506
2507 fn analyze_allof_composition(
2508 &mut self,
2509 all_of_schemas: &[Schema],
2510 dependencies: &mut HashSet<String>,
2511 ) -> Result<SchemaType> {
2512 let referenced_targets = all_of_schemas
2517 .iter()
2518 .filter_map(|schema| schema.reference())
2519 .filter_map(|reference| self.extract_schema_name(reference))
2520 .collect::<Vec<_>>();
2521 let only_reference_and_annotations = all_of_schemas.iter().all(|schema| {
2522 if schema.reference().is_some() {
2523 return true;
2524 }
2525 serde_json::to_value(schema)
2526 .ok()
2527 .and_then(|value| value.as_object().cloned())
2528 .is_some_and(|object| {
2529 object.keys().all(|key| {
2530 matches!(
2531 key.as_str(),
2532 "title"
2533 | "description"
2534 | "deprecated"
2535 | "readOnly"
2536 | "writeOnly"
2537 | "examples"
2538 | "example"
2539 | "externalDocs"
2540 | "xml"
2541 | "$comment"
2542 ) || key.starts_with("x-")
2543 })
2544 })
2545 });
2546 if referenced_targets.len() == 1 && only_reference_and_annotations {
2547 let target = referenced_targets[0];
2548 dependencies.insert(target.to_string());
2549 return Ok(SchemaType::Reference {
2550 target: target.to_string(),
2551 });
2552 }
2553
2554 let mut merged_properties = BTreeMap::new();
2556 let mut merged_required = HashSet::new();
2557 let mut descriptions = Vec::new();
2558
2559 let current_context = self.current_schema_name.clone();
2561
2562 for schema in all_of_schemas {
2563 match schema {
2564 Schema::Reference { reference, .. } => {
2565 if let Some(target) = self.extract_schema_name(reference) {
2567 dependencies.insert(target.to_string());
2568
2569 let analyzed_ref = self.analyze_schema(target)?;
2571
2572 match &analyzed_ref.schema_type {
2574 SchemaType::Object {
2575 properties,
2576 required,
2577 ..
2578 } => {
2579 for (prop_name, prop_info) in properties {
2581 merged_properties.insert(prop_name.clone(), prop_info.clone());
2582 }
2583 for req in required {
2585 merged_required.insert(req.clone());
2586 }
2587 }
2588 _ => {
2589 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2591 self.merge_schema_into_properties(
2592 &ref_schema,
2593 &mut merged_properties,
2594 &mut merged_required,
2595 dependencies,
2596 )?;
2597 }
2598 }
2599 }
2600 }
2601 }
2602 Schema::Typed {
2603 schema_type: OpenApiSchemaType::Object,
2604 ..
2605 }
2606 | Schema::Untyped { .. } => {
2607 let saved_context = self.current_schema_name.clone();
2609 self.current_schema_name = current_context.clone();
2610
2611 self.merge_schema_into_properties(
2613 schema,
2614 &mut merged_properties,
2615 &mut merged_required,
2616 dependencies,
2617 )?;
2618
2619 self.current_schema_name = saved_context;
2621 }
2622 _ => {
2623 self.merge_schema_into_properties(
2626 schema,
2627 &mut merged_properties,
2628 &mut merged_required,
2629 dependencies,
2630 )?;
2631 }
2632 }
2633
2634 if let Some(desc) = &schema.details().description {
2636 descriptions.push(desc.clone());
2637 }
2638 }
2639
2640 if !merged_properties.is_empty() {
2642 Ok(SchemaType::Object {
2643 properties: merged_properties,
2644 required: merged_required,
2645 additional_properties: ObjectAdditionalProperties::Forbidden,
2646 })
2647 } else {
2648 Ok(SchemaType::Composition {
2650 schemas: all_of_schemas
2651 .iter()
2652 .filter_map(|s| {
2653 if let Some(ref_str) = s.reference() {
2654 if let Some(target) = self.extract_schema_name(ref_str) {
2655 dependencies.insert(target.to_string());
2656 Some(SchemaRef {
2657 target: target.to_string(),
2658 nullable: false,
2659 })
2660 } else {
2661 None
2662 }
2663 } else {
2664 None
2665 }
2666 })
2667 .collect(),
2668 })
2669 }
2670 }
2671
2672 fn merge_schema_into_properties(
2673 &mut self,
2674 schema: &Schema,
2675 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2676 merged_required: &mut HashSet<String>,
2677 dependencies: &mut HashSet<String>,
2678 ) -> Result<()> {
2679 let details = schema.details();
2680
2681 if let Some(properties) = &details.properties {
2683 for (prop_name, prop_schema) in properties {
2684 let prop_type = self.analyze_property_schema_with_context(
2685 prop_schema,
2686 Some(prop_name),
2687 dependencies,
2688 )?;
2689 let prop_details = prop_schema.details();
2690
2691 let nullable = prop_schema.is_nullable_any();
2698 merged_properties.insert(
2699 prop_name.clone(),
2700 PropertyInfo {
2701 schema_type: prop_type,
2702 nullable,
2703 description: prop_details.description.clone(),
2704 default: prop_details.default.clone(),
2705 serde_attrs: Vec::new(),
2706 constraints: PropertyConstraints::from_schema_details(prop_details),
2707 },
2708 );
2709 }
2710 }
2711
2712 if let Some(required) = &details.required {
2714 for field in required {
2715 merged_required.insert(field.clone());
2716 }
2717 }
2718
2719 Ok(())
2720 }
2721
2722 fn analyze_oneof_union(
2723 &mut self,
2724 one_of_schemas: &[Schema],
2725 discriminator: Option<&crate::openapi::Discriminator>,
2726 parent_name: &str,
2727 dependencies: &mut HashSet<String>,
2728 ) -> Result<SchemaType> {
2729 if one_of_schemas.len() == 2 {
2732 let null_count = one_of_schemas
2733 .iter()
2734 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2735 .count();
2736 if null_count == 1 {
2737 if let Some(non_null) = one_of_schemas
2738 .iter()
2739 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2740 {
2741 return self
2742 .analyze_schema_value(non_null, parent_name)
2743 .map(|a| a.schema_type);
2744 }
2745 }
2746 }
2747
2748 if discriminator.is_none() {
2750 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2752 }
2753
2754 if one_of_schemas
2760 .iter()
2761 .any(|s| !self.branch_resolves_to_object(s))
2762 {
2763 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2764 }
2765
2766 let discriminator_field = discriminator
2768 .ok_or_else(|| {
2769 GeneratorError::InvalidDiscriminator(
2770 "expected discriminator after guard check".to_string(),
2771 )
2772 })?
2773 .property_name
2774 .clone();
2775
2776 let mut variants = Vec::new();
2777 let mut used_variant_names = std::collections::HashSet::new();
2778
2779 for variant_schema in one_of_schemas {
2780 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2782 Some((ref_str, false))
2783 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2784 Some((recursive_ref, true))
2785 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2786 if all_of.len() == 1 {
2788 if let Some(ref_str) = all_of[0].reference() {
2789 Some((ref_str, false))
2790 } else {
2791 all_of[0]
2792 .recursive_reference()
2793 .map(|recursive_ref| (recursive_ref, true))
2794 }
2795 } else {
2796 None
2797 }
2798 } else {
2799 None
2800 };
2801
2802 if let Some((ref_str, is_recursive)) = ref_info {
2803 let schema_name = if is_recursive && ref_str == "#" {
2804 self.find_recursive_anchor_schema()
2806 .or_else(|| self.current_schema_name.clone())
2807 .unwrap_or_else(|| "CompoundFilter".to_string())
2808 } else {
2809 self.extract_schema_name(ref_str)
2810 .map(|s| s.to_string())
2811 .unwrap_or_else(|| "UnknownRef".to_string())
2812 };
2813
2814 if !schema_name.is_empty() {
2815 dependencies.insert(schema_name.clone());
2816
2817 let discriminator_value = if let Some(disc) = discriminator {
2822 if let Some(mappings) = &disc.mapping {
2823 mappings
2826 .iter()
2827 .find(|(_, target_ref)| {
2828 target_ref.as_str() == ref_str
2830 || self
2831 .extract_schema_name(target_ref)
2832 .map(|s| s.to_string())
2833 == Some(schema_name.clone())
2834 })
2835 .map(|(key, _)| key.clone())
2836 .unwrap_or_else(|| {
2837 self.fallback_discriminator_value_for_field(
2838 &schema_name,
2839 &discriminator_field,
2840 )
2841 })
2842 } else {
2843 self.fallback_discriminator_value_for_field(
2844 &schema_name,
2845 &discriminator_field,
2846 )
2847 }
2848 } else {
2849 self.fallback_discriminator_value_for_field(
2850 &schema_name,
2851 &discriminator_field,
2852 )
2853 };
2854
2855 let base_name = self.to_rust_variant_name(&schema_name);
2857 let rust_name =
2858 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2859
2860 let final_discriminator_value = discriminator_value;
2862
2863 variants.push(UnionVariant {
2864 rust_name,
2865 type_name: schema_name,
2866 discriminator_value: final_discriminator_value,
2867 schema_ref: ref_str.to_string(),
2868 });
2869 }
2870 } else {
2871 let variant_index = variants.len();
2873 let inline_type_name =
2874 self.generate_inline_type_name(variant_schema, variant_index);
2875
2876 let discriminator_value = if let Some(disc) = discriminator {
2878 if let Some(mappings) = &disc.mapping {
2879 mappings
2881 .iter()
2882 .find(|(_, target_ref)| {
2883 target_ref.contains(&format!("variant_{variant_index}"))
2884 })
2885 .map(|(key, _)| key.clone())
2886 .unwrap_or_else(|| {
2887 self.extract_inline_discriminator_value(
2888 variant_schema,
2889 &discriminator_field,
2890 variant_index,
2891 )
2892 })
2893 } else {
2894 self.extract_inline_discriminator_value(
2895 variant_schema,
2896 &discriminator_field,
2897 variant_index,
2898 )
2899 }
2900 } else {
2901 self.extract_inline_discriminator_value(
2902 variant_schema,
2903 &discriminator_field,
2904 variant_index,
2905 )
2906 };
2907
2908 let base_name = if discriminator_value.starts_with("variant_") {
2910 format!("Variant{variant_index}")
2911 } else {
2912 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2914 self.to_rust_variant_name(&clean_name)
2915 };
2916 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2917
2918 let final_discriminator_value = discriminator_value;
2920
2921 variants.push(UnionVariant {
2922 rust_name,
2923 type_name: inline_type_name.clone(),
2924 discriminator_value: final_discriminator_value,
2925 schema_ref: format!("inline_{variant_index}"),
2926 });
2927
2928 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2930 }
2931 }
2932
2933 if variants.is_empty() {
2934 let mut union_variants = Vec::new();
2937
2938 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2939 if let Some(ref_str) = variant_schema.reference() {
2941 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2942 dependencies.insert(schema_name.to_string());
2943 union_variants.push(SchemaRef {
2944 target: schema_name.to_string(),
2945 nullable: false,
2946 });
2947 }
2948 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2949 let schema_name = if recursive_ref == "#" {
2950 self.find_recursive_anchor_schema()
2952 .or_else(|| self.current_schema_name.clone())
2953 .unwrap_or_else(|| "CompoundFilter".to_string())
2954 } else {
2955 self.extract_schema_name(recursive_ref)
2956 .map(|s| s.to_string())
2957 .unwrap_or_else(|| "RecursiveType".to_string())
2958 };
2959 dependencies.insert(schema_name.clone());
2960 union_variants.push(SchemaRef {
2961 target: schema_name,
2962 nullable: false,
2963 });
2964 } else {
2965 let inline_name = self.generate_context_aware_name(
2967 parent_name,
2968 "InlineVariant",
2969 variant_index,
2970 Some(variant_schema),
2971 );
2972 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2973 let variant_type = analyzed.schema_type;
2974
2975 for dep in &analyzed.dependencies {
2977 dependencies.insert(dep.clone());
2978 }
2979
2980 match &variant_type {
2981 SchemaType::Primitive { rust_type, .. } => {
2983 union_variants.push(SchemaRef {
2984 target: rust_type.clone(),
2985 nullable: false,
2986 });
2987 }
2988 SchemaType::Array { item_type } => {
2990 match item_type.as_ref() {
2991 SchemaType::Primitive { rust_type, .. } => {
2992 let type_name = format!("Vec<{rust_type}>");
2993 union_variants.push(SchemaRef {
2994 target: type_name,
2995 nullable: false,
2996 });
2997 }
2998 SchemaType::Reference { target } => {
2999 let type_name = format!("Vec<{target}>");
3000 union_variants.push(SchemaRef {
3001 target: type_name,
3002 nullable: false,
3003 });
3004 }
3005 _ => {
3006 let inline_type_name = self.generate_context_aware_name(
3008 parent_name,
3009 "Variant",
3010 variant_index,
3011 None,
3012 );
3013 self.add_inline_schema(
3014 &inline_type_name,
3015 variant_schema,
3016 dependencies,
3017 )?;
3018 union_variants.push(SchemaRef {
3019 target: inline_type_name,
3020 nullable: false,
3021 });
3022 }
3023 }
3024 }
3025 SchemaType::Reference { target } => {
3027 union_variants.push(SchemaRef {
3028 target: target.clone(),
3029 nullable: false,
3030 });
3031 }
3032 _ => {
3034 let inline_type_name =
3035 format!("{}Variant{}", parent_name, variant_index + 1);
3036 self.add_inline_schema(
3037 &inline_type_name,
3038 variant_schema,
3039 dependencies,
3040 )?;
3041 union_variants.push(SchemaRef {
3042 target: inline_type_name,
3043 nullable: false,
3044 });
3045 }
3046 }
3047 }
3048 }
3049
3050 if !union_variants.is_empty() {
3051 return Ok(SchemaType::Union {
3052 variants: union_variants,
3053 });
3054 }
3055
3056 return Ok(SchemaType::Primitive {
3058 rust_type: "serde_json::Value".to_string(),
3059 serde_with: None,
3060 });
3061 }
3062
3063 Ok(SchemaType::DiscriminatedUnion {
3064 discriminator_field,
3065 variants,
3066 })
3067 }
3068
3069 fn analyze_untagged_oneof_union(
3070 &mut self,
3071 one_of_schemas: &[Schema],
3072 parent_name: &str,
3073 dependencies: &mut HashSet<String>,
3074 ) -> Result<SchemaType> {
3075 let filtered: Vec<&Schema> = one_of_schemas
3079 .iter()
3080 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3081 .collect();
3082
3083 if filtered.len() == 1 {
3085 return self
3086 .analyze_schema_value(filtered[0], parent_name)
3087 .map(|a| a.schema_type);
3088 }
3089
3090 let mut union_variants = Vec::new();
3091
3092 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
3093 if let Some(ref_str) = variant_schema.reference() {
3095 if let Some(schema_name) = self.extract_schema_name(ref_str) {
3096 dependencies.insert(schema_name.to_string());
3097 union_variants.push(SchemaRef {
3098 target: schema_name.to_string(),
3099 nullable: false,
3100 });
3101 }
3102 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3103 let schema_name = if recursive_ref == "#" {
3104 self.find_recursive_anchor_schema()
3106 .or_else(|| self.current_schema_name.clone())
3107 .unwrap_or_else(|| "CompoundFilter".to_string())
3108 } else {
3109 self.extract_schema_name(recursive_ref)
3110 .map(|s| s.to_string())
3111 .unwrap_or_else(|| "RecursiveType".to_string())
3112 };
3113 dependencies.insert(schema_name.clone());
3114 union_variants.push(SchemaRef {
3115 target: schema_name,
3116 nullable: false,
3117 });
3118 } else {
3119 let inline_name = self.generate_context_aware_name(
3121 parent_name,
3122 "InlineVariant",
3123 variant_index,
3124 Some(variant_schema),
3125 );
3126 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
3127 let variant_type = analyzed.schema_type;
3128
3129 for dep in &analyzed.dependencies {
3131 dependencies.insert(dep.clone());
3132 }
3133
3134 match &variant_type {
3135 SchemaType::Primitive { rust_type, .. } => {
3137 union_variants.push(SchemaRef {
3138 target: rust_type.clone(),
3139 nullable: false,
3140 });
3141 }
3142 SchemaType::Array { item_type } => {
3144 match item_type.as_ref() {
3145 SchemaType::Primitive { rust_type, .. } => {
3146 let type_name = format!("Vec<{rust_type}>");
3147 union_variants.push(SchemaRef {
3148 target: type_name,
3149 nullable: false,
3150 });
3151 }
3152 SchemaType::Reference { target } => {
3153 let type_name = format!("Vec<{target}>");
3154 union_variants.push(SchemaRef {
3155 target: type_name,
3156 nullable: false,
3157 });
3158 }
3159 SchemaType::Array {
3161 item_type: inner_item_type,
3162 } => {
3163 match inner_item_type.as_ref() {
3164 SchemaType::Primitive { rust_type, .. } => {
3165 let type_name = format!("Vec<Vec<{rust_type}>>");
3166 union_variants.push(SchemaRef {
3167 target: type_name,
3168 nullable: false,
3169 });
3170 }
3171 SchemaType::Reference { target } => {
3172 let type_name = format!("Vec<Vec<{target}>>");
3173 union_variants.push(SchemaRef {
3174 target: type_name,
3175 nullable: false,
3176 });
3177 }
3178 _ => {
3179 let inline_type_name = self.generate_context_aware_name(
3181 parent_name,
3182 "Variant",
3183 variant_index,
3184 None,
3185 );
3186 self.add_inline_schema(
3187 &inline_type_name,
3188 variant_schema,
3189 dependencies,
3190 )?;
3191 union_variants.push(SchemaRef {
3192 target: inline_type_name,
3193 nullable: false,
3194 });
3195 }
3196 }
3197 }
3198 _ => {
3199 let inline_type_name = self.generate_context_aware_name(
3201 parent_name,
3202 "Variant",
3203 variant_index,
3204 None,
3205 );
3206 self.add_inline_schema(
3207 &inline_type_name,
3208 variant_schema,
3209 dependencies,
3210 )?;
3211 union_variants.push(SchemaRef {
3212 target: inline_type_name,
3213 nullable: false,
3214 });
3215 }
3216 }
3217 }
3218 SchemaType::Reference { target } => {
3220 union_variants.push(SchemaRef {
3221 target: target.clone(),
3222 nullable: false,
3223 });
3224 }
3225 _ => {
3227 let inline_type_name = self.generate_context_aware_name(
3228 parent_name,
3229 "Variant",
3230 variant_index,
3231 None,
3232 );
3233 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3234 union_variants.push(SchemaRef {
3235 target: inline_type_name,
3236 nullable: false,
3237 });
3238 }
3239 }
3240 }
3241 }
3242
3243 if !union_variants.is_empty() {
3244 return Ok(SchemaType::Union {
3245 variants: union_variants,
3246 });
3247 }
3248
3249 Ok(SchemaType::Primitive {
3251 rust_type: "serde_json::Value".to_string(),
3252 serde_with: None,
3253 })
3254 }
3255
3256 fn add_inline_schema(
3257 &mut self,
3258 type_name: &str,
3259 schema: &Schema,
3260 dependencies: &mut HashSet<String>,
3261 ) -> Result<()> {
3262 if let Some(schema_type) = schema.schema_type() {
3264 match schema_type {
3265 OpenApiSchemaType::String
3266 | OpenApiSchemaType::Integer
3267 | OpenApiSchemaType::Number
3268 | OpenApiSchemaType::Boolean => {
3269 let rust_type =
3270 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3271
3272 self.resolved_cache.insert(
3274 type_name.to_string(),
3275 AnalyzedSchema {
3276 name: type_name.to_string(),
3277 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3278 schema_type: SchemaType::Primitive {
3279 rust_type,
3280 serde_with: None,
3281 },
3282 dependencies: HashSet::new(),
3283 nullable: false,
3284 description: schema.details().description.clone(),
3285 default: None,
3286 },
3287 );
3288 return Ok(());
3289 }
3290 _ => {}
3291 }
3292 }
3293
3294 let previous_schema_name = self.current_schema_name.take();
3298 self.current_schema_name = Some(type_name.to_string());
3299 let analyzed = self.analyze_schema_value(schema, type_name)?;
3300 self.current_schema_name = previous_schema_name;
3301
3302 self.resolved_cache.insert(type_name.to_string(), analyzed);
3304
3305 if let Some(cached) = self.resolved_cache.get(type_name) {
3307 for dep in &cached.dependencies {
3308 dependencies.insert(dep.clone());
3309 }
3310 }
3311
3312 Ok(())
3313 }
3314
3315 fn extract_inline_discriminator_value(
3316 &self,
3317 schema: &Schema,
3318 discriminator_field: &str,
3319 variant_index: usize,
3320 ) -> String {
3321 if let Some(properties) = &schema.details().properties {
3323 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3324 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3326 if enum_values.len() == 1 {
3327 if let Some(value) = enum_values[0].as_str() {
3328 return value.to_string();
3329 }
3330 }
3331 }
3332 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3334 if let Some(value) = const_value.as_str() {
3335 return value.to_string();
3336 }
3337 }
3338 if let Some(const_value) = &discriminator_prop.details().const_value {
3340 if let Some(value) = const_value.as_str() {
3341 return value.to_string();
3342 }
3343 }
3344 }
3345 }
3346
3347 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3349 return inferred_name;
3350 }
3351
3352 format!("variant_{variant_index}")
3354 }
3355
3356 fn infer_variant_name_from_structure(
3357 &self,
3358 schema: &Schema,
3359 _variant_index: usize,
3360 ) -> Option<String> {
3361 let details = schema.details();
3362
3363 if let Some(properties) = &details.properties {
3365 if properties.contains_key("text") && properties.len() <= 3 {
3367 return Some("text".to_string());
3368 }
3369 if properties.contains_key("image") || properties.contains_key("source") {
3370 return Some("image".to_string());
3371 }
3372 if properties.contains_key("document") {
3373 return Some("document".to_string());
3374 }
3375 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3376 return Some("tool_result".to_string());
3377 }
3378 if properties.contains_key("content") && properties.contains_key("is_error") {
3379 return Some("tool_result".to_string());
3380 }
3381 if properties.contains_key("partial_json") {
3382 return Some("partial_json".to_string());
3383 }
3384
3385 let property_names: Vec<&String> = properties.keys().collect();
3387
3388 for prop_name in &property_names {
3390 if prop_name.contains("result") {
3391 return Some("result".to_string());
3392 }
3393 if prop_name.contains("error") {
3394 return Some("error".to_string());
3395 }
3396 if prop_name.contains("content") && property_names.len() <= 2 {
3397 return Some("content".to_string());
3398 }
3399 }
3400
3401 let significant_props = property_names
3403 .iter()
3404 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3405 .collect::<Vec<_>>();
3406
3407 if significant_props.len() == 1 {
3408 return Some((*significant_props[0]).clone());
3409 }
3410 }
3411
3412 if let Some(description) = &details.description {
3414 let desc_lower = description.to_lowercase();
3415 if desc_lower.contains("text") && desc_lower.len() < 100 {
3416 return Some("text".to_string());
3417 }
3418 if desc_lower.contains("image") {
3419 return Some("image".to_string());
3420 }
3421 if desc_lower.contains("document") {
3422 return Some("document".to_string());
3423 }
3424 if desc_lower.contains("tool") && desc_lower.contains("result") {
3425 return Some("tool_result".to_string());
3426 }
3427 }
3428
3429 None
3430 }
3431
3432 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3433 if discriminator.is_empty() {
3435 return "Variant".to_string();
3436 }
3437
3438 let mut result = String::new();
3439 let mut next_upper = true;
3440
3441 for c in discriminator.chars() {
3442 match c {
3443 'a'..='z' => {
3444 if next_upper {
3445 result.push(c.to_ascii_uppercase());
3446 next_upper = false;
3447 } else {
3448 result.push(c);
3449 }
3450 }
3451 'A'..='Z' => {
3452 result.push(c);
3453 next_upper = false;
3454 }
3455 '0'..='9' => {
3456 result.push(c);
3457 next_upper = false;
3458 }
3459 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3460 next_upper = true;
3462 }
3463 _ => {
3464 next_upper = true;
3466 }
3467 }
3468 }
3469
3470 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3472 result = format!("Variant{result}");
3473 }
3474
3475 result
3476 }
3477
3478 fn ensure_unique_variant_name(
3479 &self,
3480 base_name: String,
3481 used_names: &mut std::collections::HashSet<String>,
3482 ) -> String {
3483 let mut candidate = base_name.clone();
3484 let mut counter = 1;
3485
3486 while used_names.contains(&candidate) {
3487 counter += 1;
3488 candidate = format!("{base_name}{counter}");
3489 }
3490
3491 used_names.insert(candidate.clone());
3492 candidate
3493 }
3494
3495 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3496 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3498 return meaningful_name;
3499 }
3500
3501 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3503 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3504 }
3505
3506 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3507 let details = schema.details();
3508
3509 if let Some(description) = &details.description {
3511 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3512 return Some(name_from_desc);
3513 }
3514 }
3515
3516 if let Some(properties) = &details.properties {
3518 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3519 return Some(format!("{name_from_props}Block"));
3520 }
3521 }
3522
3523 None
3524 }
3525
3526 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3527 if description.len() > 100 || description.contains('\n') {
3529 return None;
3530 }
3531
3532 let words: Vec<&str> = description
3534 .split_whitespace()
3535 .take(2) .filter(|word| {
3537 let w = word.to_lowercase();
3538 word.len() > 2
3539 && ![
3540 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3541 ]
3542 .contains(&w.as_str())
3543 })
3544 .collect();
3545
3546 if words.is_empty() {
3547 return None;
3548 }
3549
3550 let combined = words.join("_");
3552 let pascal_name = self.discriminator_to_variant_name(&combined);
3553
3554 if !pascal_name.ends_with("Content")
3556 && !pascal_name.ends_with("Block")
3557 && !pascal_name.ends_with("Type")
3558 {
3559 Some(format!("{pascal_name}Content"))
3560 } else {
3561 Some(pascal_name)
3562 }
3563 }
3564
3565 fn extract_type_name_from_properties(
3566 &self,
3567 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3568 ) -> Option<String> {
3569 let significant_props: Vec<&String> = properties
3571 .keys()
3572 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3573 .collect();
3574
3575 if significant_props.is_empty() {
3576 return None;
3577 }
3578
3579 if significant_props.len() == 1 {
3581 let prop_name = significant_props[0];
3582 return Some(self.discriminator_to_variant_name(prop_name));
3583 }
3584
3585 let mut sorted_props = significant_props.clone();
3588 sorted_props.sort();
3589 if let Some(first_prop) = sorted_props.first() {
3590 return Some(self.discriminator_to_variant_name(first_prop));
3591 }
3592
3593 None
3594 }
3595
3596 fn openapi_type_to_rust_type(
3597 &self,
3598 openapi_type: OpenApiSchemaType,
3599 details: &crate::openapi::SchemaDetails,
3600 ) -> String {
3601 self.type_mapper.map(openapi_type, details).rust_type
3606 }
3607
3608 #[allow(dead_code)]
3609 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3610 self.fallback_discriminator_value_for_field(schema_name, "type")
3611 }
3612
3613 fn fallback_discriminator_value_for_field(
3614 &self,
3615 schema_name: &str,
3616 field_name: &str,
3617 ) -> String {
3618 if let Some(ref_schema) = self.schemas.get(schema_name) {
3620 if let Some(extracted) =
3621 self.extract_discriminator_value_for_field(ref_schema, field_name)
3622 {
3623 return extracted;
3624 }
3625 }
3626
3627 self.generate_discriminator_value_from_name(schema_name)
3629 }
3630
3631 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3632 let mut result = String::new();
3634 let mut chars = schema_name.chars().peekable();
3635 let mut first = true;
3636
3637 while let Some(c) = chars.next() {
3638 if c.is_uppercase()
3639 && !first
3640 && chars
3641 .peek()
3642 .map(|&next| next.is_lowercase())
3643 .unwrap_or(false)
3644 {
3645 result.push('.');
3646 }
3647 result.push(c.to_ascii_lowercase());
3648 first = false;
3649 }
3650
3651 if result.ends_with("event") {
3653 result = result[..result.len() - 5].to_string();
3654 }
3655
3656 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3658 result = format!("response.{}", result.trim_start_matches("response"));
3659 }
3660
3661 result
3662 }
3663
3664 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3665 let mut name = schema_name;
3667
3668 if name.starts_with("Response") && name.len() > 8 {
3670 name = &name[8..]; }
3672
3673 if name.ends_with("Event") && name.len() > 5 {
3675 name = &name[..name.len() - 5]; }
3677
3678 name = name.trim_matches('_');
3680
3681 if name.is_empty() {
3683 schema_name.to_string()
3684 } else {
3685 self.discriminator_to_variant_name(name)
3687 }
3688 }
3689
3690 fn hoist_inline_string_enum(
3714 &mut self,
3715 schema: &Schema,
3716 enum_values: Vec<String>,
3717 primary_name: String,
3718 dependencies: &mut HashSet<String>,
3719 ) -> SchemaType {
3720 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3721 matches!(
3722 &existing.schema_type,
3723 SchemaType::StringEnum { values: existing_values }
3724 if existing_values == values
3725 )
3726 }
3727
3728 let mut enum_type_name = primary_name.clone();
3729 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3730 None => true,
3731 Some(existing) if matches_values(existing, &enum_values) => false,
3732 Some(_) => {
3733 let suffix = enum_values
3736 .first()
3737 .map(|v| self.to_pascal_case(v))
3738 .unwrap_or_else(|| "Variant".to_string());
3739 let candidate = format!("{primary_name}{suffix}");
3740
3741 let resolved = match self.resolved_cache.get(&candidate) {
3742 None => Some((candidate.clone(), true)),
3743 Some(existing) if matches_values(existing, &enum_values) => {
3744 Some((candidate.clone(), false))
3745 }
3746 Some(_) => {
3747 let mut found = None;
3750 for n in 2..1000 {
3751 let numbered = format!("{candidate}_{n}");
3752 match self.resolved_cache.get(&numbered) {
3753 None => {
3754 found = Some((numbered, true));
3755 break;
3756 }
3757 Some(existing) if matches_values(existing, &enum_values) => {
3758 found = Some((numbered, false));
3759 break;
3760 }
3761 Some(_) => continue,
3762 }
3763 }
3764 found
3765 }
3766 };
3767
3768 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3769 enum_type_name = resolved_name;
3770 insert
3771 }
3772 };
3773
3774 if should_insert {
3777 self.resolved_cache.insert(
3778 enum_type_name.clone(),
3779 AnalyzedSchema {
3780 name: enum_type_name.clone(),
3781 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3782 schema_type: SchemaType::StringEnum {
3783 values: enum_values,
3784 },
3785 dependencies: HashSet::new(),
3786 nullable: false,
3787 description: schema.details().description.clone(),
3788 default: schema.details().default.clone(),
3789 },
3790 );
3791 }
3792
3793 dependencies.insert(enum_type_name.clone());
3795 SchemaType::Reference {
3796 target: enum_type_name,
3797 }
3798 }
3799
3800 fn analyze_array_schema(
3801 &mut self,
3802 schema: &Schema,
3803 parent_schema_name: &str,
3804 dependencies: &mut HashSet<String>,
3805 ) -> Result<SchemaType> {
3806 let details = schema.details();
3807
3808 if let Some(items_schema) = &details.items {
3810 let item_type = match items_schema.as_ref() {
3812 Schema::Reference { reference, .. } => {
3813 let target = self
3815 .extract_schema_name(reference)
3816 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3817 .to_string();
3818 dependencies.insert(target.clone());
3819 SchemaType::Reference { target }
3820 }
3821 Schema::RecursiveRef { recursive_ref, .. } => {
3822 if recursive_ref == "#" {
3824 let target = self
3826 .find_recursive_anchor_schema()
3827 .unwrap_or_else(|| parent_schema_name.to_string());
3828 dependencies.insert(target.clone());
3829 SchemaType::Reference { target }
3830 } else {
3831 let target = self
3832 .extract_schema_name(recursive_ref)
3833 .unwrap_or("RecursiveType")
3834 .to_string();
3835 dependencies.insert(target.clone());
3836 SchemaType::Reference { target }
3837 }
3838 }
3839 Schema::Typed { schema_type, .. } => {
3840 match schema_type {
3842 OpenApiSchemaType::String => {
3843 match items_schema
3847 .details()
3848 .string_enum_values()
3849 .filter(|values| !values.is_empty())
3850 {
3851 Some(values) => self.hoist_inline_string_enum(
3852 items_schema,
3853 values,
3854 format!("{parent_schema_name}Item"),
3855 dependencies,
3856 ),
3857 None => SchemaType::Primitive {
3858 rust_type: "String".to_string(),
3859 serde_with: None,
3860 },
3861 }
3862 }
3863 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3864 let details = items_schema.details();
3865 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3866 SchemaType::Primitive {
3867 rust_type,
3868 serde_with: None,
3869 }
3870 }
3871 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3872 rust_type: "bool".to_string(),
3873 serde_with: None,
3874 },
3875 OpenApiSchemaType::Object => {
3876 let object_type_name = format!("{parent_schema_name}Item");
3878
3879 let object_type =
3881 self.analyze_object_schema(items_schema, dependencies)?;
3882
3883 let inline_schema = AnalyzedSchema {
3885 name: object_type_name.clone(),
3886 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3887 schema_type: object_type,
3888 dependencies: dependencies.clone(),
3889 nullable: false,
3890 description: items_schema.details().description.clone(),
3891 default: None,
3892 };
3893
3894 self.resolved_cache
3896 .insert(object_type_name.clone(), inline_schema);
3897 dependencies.insert(object_type_name.clone());
3898
3899 SchemaType::Reference {
3901 target: object_type_name,
3902 }
3903 }
3904 OpenApiSchemaType::Array => {
3905 self.analyze_array_schema(
3907 items_schema,
3908 parent_schema_name,
3909 dependencies,
3910 )?
3911 }
3912 _ => SchemaType::Primitive {
3913 rust_type: "serde_json::Value".to_string(),
3914 serde_with: None,
3915 },
3916 }
3917 }
3918 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3919 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3921
3922 match &analyzed.schema_type {
3924 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3925 let union_name = format!("{parent_schema_name}ItemUnion");
3928
3929 let mut union_schema = analyzed;
3931 union_schema.name = union_name.clone();
3932
3933 self.resolved_cache.insert(union_name.clone(), union_schema);
3935
3936 dependencies.insert(union_name.clone());
3938
3939 SchemaType::Reference { target: union_name }
3941 }
3942 _ => analyzed.schema_type,
3943 }
3944 }
3945 Schema::Untyped { .. } => {
3946 if let Some(inferred) = items_schema.inferred_type() {
3948 match inferred {
3949 OpenApiSchemaType::Object => {
3950 let object_type_name = format!("{parent_schema_name}Item");
3952
3953 let object_type =
3955 self.analyze_object_schema(items_schema, dependencies)?;
3956
3957 let inline_schema = AnalyzedSchema {
3959 name: object_type_name.clone(),
3960 original: serde_json::to_value(items_schema)
3961 .unwrap_or(Value::Null),
3962 schema_type: object_type,
3963 dependencies: dependencies.clone(),
3964 nullable: false,
3965 description: items_schema.details().description.clone(),
3966 default: None,
3967 };
3968
3969 self.resolved_cache
3971 .insert(object_type_name.clone(), inline_schema);
3972 dependencies.insert(object_type_name.clone());
3973
3974 SchemaType::Reference {
3976 target: object_type_name,
3977 }
3978 }
3979 OpenApiSchemaType::String => {
3980 match items_schema
3983 .details()
3984 .string_enum_values()
3985 .filter(|values| !values.is_empty())
3986 {
3987 Some(values) => self.hoist_inline_string_enum(
3988 items_schema,
3989 values,
3990 format!("{parent_schema_name}Item"),
3991 dependencies,
3992 ),
3993 None => SchemaType::Primitive {
3994 rust_type: "String".to_string(),
3995 serde_with: None,
3996 },
3997 }
3998 }
3999 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
4000 let details = items_schema.details();
4001 let rust_type = self.get_number_rust_type(inferred, details);
4002 SchemaType::Primitive {
4003 rust_type,
4004 serde_with: None,
4005 }
4006 }
4007 OpenApiSchemaType::Boolean => SchemaType::Primitive {
4008 rust_type: "bool".to_string(),
4009 serde_with: None,
4010 },
4011 _ => SchemaType::Primitive {
4012 rust_type: "serde_json::Value".to_string(),
4013 serde_with: None,
4014 },
4015 }
4016 } else {
4017 SchemaType::Primitive {
4018 rust_type: "serde_json::Value".to_string(),
4019 serde_with: None,
4020 }
4021 }
4022 }
4023 _ => SchemaType::Primitive {
4024 rust_type: "serde_json::Value".to_string(),
4025 serde_with: None,
4026 },
4027 };
4028
4029 Ok(SchemaType::Array {
4030 item_type: Box::new(item_type),
4031 })
4032 } else {
4033 Ok(SchemaType::Primitive {
4035 rust_type: "Vec<serde_json::Value>".to_string(),
4036 serde_with: None,
4037 })
4038 }
4039 }
4040
4041 fn get_number_rust_type(
4042 &self,
4043 schema_type: OpenApiSchemaType,
4044 details: &crate::openapi::SchemaDetails,
4045 ) -> String {
4046 let format = details.format.as_deref();
4050 match schema_type {
4051 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
4052 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
4053 _ => self.type_mapper.dynamic_json().rust_type,
4054 }
4055 }
4056
4057 fn analyze_anyof_union(
4058 &mut self,
4059 any_of_schemas: &[Schema],
4060 discriminator: Option<&Discriminator>,
4061 dependencies: &mut HashSet<String>,
4062 context_name: &str,
4063 ) -> Result<SchemaType> {
4064 let filtered_owned: Vec<Schema>;
4069 let any_of_schemas: &[Schema] = if any_of_schemas
4070 .iter()
4071 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4072 {
4073 filtered_owned = any_of_schemas
4074 .iter()
4075 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4076 .cloned()
4077 .collect();
4078 if filtered_owned.is_empty() {
4079 return Ok(SchemaType::Primitive {
4080 rust_type: "serde_json::Value".to_string(),
4081 serde_with: None,
4082 });
4083 }
4084 if filtered_owned.len() == 1 {
4085 return self
4086 .analyze_schema_value(&filtered_owned[0], context_name)
4087 .map(|a| a.schema_type);
4088 }
4089 &filtered_owned
4090 } else {
4091 any_of_schemas
4092 };
4093
4094 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
4096 let has_objects = any_of_schemas.iter().any(|s| {
4097 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
4098 || s.inferred_type() == Some(OpenApiSchemaType::Object)
4099 });
4100 let has_arrays = any_of_schemas
4101 .iter()
4102 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
4103
4104 let all_string_like = any_of_schemas.iter().all(|s| {
4107 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
4108 || s.details().const_value.is_some()
4109 });
4110
4111 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
4112 if let Some(disc) = discriminator {
4114 return self.analyze_oneof_union(
4116 any_of_schemas,
4117 Some(disc),
4118 context_name,
4119 dependencies,
4120 );
4121 }
4122
4123 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
4125 return self.analyze_oneof_union(
4126 any_of_schemas,
4127 Some(&Discriminator {
4128 property_name: disc_field,
4129 mapping: None,
4130 default_mapping: None,
4131 extensions: crate::extensions::Extensions::default(),
4132 }),
4133 context_name,
4134 dependencies,
4135 );
4136 }
4137
4138 let mut variants = Vec::new();
4140
4141 for schema in any_of_schemas {
4142 if let Some(ref_str) = schema.reference() {
4143 if let Some(target) = self.extract_schema_name(ref_str) {
4144 dependencies.insert(target.to_string());
4145 variants.push(SchemaRef {
4146 target: target.to_string(),
4147 nullable: false,
4148 });
4149 }
4150 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
4151 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
4152 {
4153 let inline_index = variants.len();
4155 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
4156
4157 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
4159
4160 variants.push(SchemaRef {
4161 target: inline_type_name,
4162 nullable: false,
4163 });
4164 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
4165 let array_type =
4167 self.analyze_array_schema(schema, context_name, dependencies)?;
4168
4169 let array_type_name = if let Some(items_schema) = &schema.details().items {
4171 if let Some(ref_str) = items_schema.reference() {
4172 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
4173 dependencies.insert(item_type_name.to_string());
4174 format!("{item_type_name}Array")
4175 } else {
4176 self.generate_context_aware_name(
4177 context_name,
4178 "Array",
4179 variants.len(),
4180 Some(schema),
4181 )
4182 }
4183 } else {
4184 self.generate_context_aware_name(
4185 context_name,
4186 "Array",
4187 variants.len(),
4188 Some(schema),
4189 )
4190 }
4191 } else {
4192 self.generate_context_aware_name(
4193 context_name,
4194 "Array",
4195 variants.len(),
4196 Some(schema),
4197 )
4198 };
4199
4200 self.resolved_cache.insert(
4202 array_type_name.clone(),
4203 AnalyzedSchema {
4204 name: array_type_name.clone(),
4205 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4206 schema_type: array_type,
4207 dependencies: HashSet::new(),
4208 nullable: false,
4209 description: Some("Array variant in union".to_string()),
4210 default: None,
4211 },
4212 );
4213
4214 dependencies.insert(array_type_name.clone());
4216
4217 variants.push(SchemaRef {
4218 target: array_type_name,
4219 nullable: false,
4220 });
4221 } else if let Some(schema_type) = schema.schema_type() {
4222 let primitive_unions = self
4232 .type_mapper
4233 .config_shape_primitive_unions()
4234 .unwrap_or(true);
4235
4236 if primitive_unions {
4237 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
4238 variants.push(SchemaRef {
4239 target: mapped.rust_type,
4240 nullable: false,
4241 });
4242 } else {
4243 let inline_index = variants.len();
4244 let inline_type_name = match schema_type {
4245 OpenApiSchemaType::String => {
4246 if inline_index == 0 {
4247 format!("{context_name}String")
4248 } else {
4249 format!("{context_name}StringVariant{inline_index}")
4250 }
4251 }
4252 OpenApiSchemaType::Number => {
4253 if inline_index == 0 {
4254 format!("{context_name}Number")
4255 } else {
4256 format!("{context_name}NumberVariant{inline_index}")
4257 }
4258 }
4259 OpenApiSchemaType::Integer => {
4260 if inline_index == 0 {
4261 format!("{context_name}Integer")
4262 } else {
4263 format!("{context_name}IntegerVariant{inline_index}")
4264 }
4265 }
4266 OpenApiSchemaType::Boolean => {
4267 if inline_index == 0 {
4268 format!("{context_name}Boolean")
4269 } else {
4270 format!("{context_name}BooleanVariant{inline_index}")
4271 }
4272 }
4273 _ => format!("{context_name}Variant{inline_index}"),
4274 };
4275
4276 let rust_type =
4277 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4278
4279 self.resolved_cache.insert(
4280 inline_type_name.clone(),
4281 AnalyzedSchema {
4282 name: inline_type_name.clone(),
4283 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4284 schema_type: SchemaType::Primitive {
4285 rust_type,
4286 serde_with: None,
4287 },
4288 dependencies: HashSet::new(),
4289 nullable: false,
4290 description: schema.details().description.clone(),
4291 default: None,
4292 },
4293 );
4294
4295 dependencies.insert(inline_type_name.clone());
4296
4297 variants.push(SchemaRef {
4298 target: inline_type_name,
4299 nullable: false,
4300 });
4301 }
4302 }
4303 }
4304
4305 if !variants.is_empty() {
4306 return Ok(SchemaType::Union { variants });
4307 }
4308 }
4309
4310 let all_strings = any_of_schemas.iter().all(|schema| {
4312 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4313 || schema.details().const_value.is_some()
4314 });
4315
4316 if all_strings {
4317 let mut enum_values = Vec::new();
4319 let mut has_open_string = false;
4320
4321 for schema in any_of_schemas {
4322 if let Some(const_val) = &schema.details().const_value {
4323 if let Some(const_str) = const_val.as_str() {
4324 enum_values.push(const_str.to_string());
4325 }
4326 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4327 has_open_string = true;
4328 }
4329 }
4330
4331 if !enum_values.is_empty() {
4332 if has_open_string {
4333 return Ok(SchemaType::ExtensibleEnum {
4336 known_values: enum_values,
4337 });
4338 } else {
4339 return Ok(SchemaType::StringEnum {
4341 values: enum_values,
4342 });
4343 }
4344 }
4345 }
4346
4347 Ok(SchemaType::Primitive {
4349 rust_type: "serde_json::Value".to_string(),
4350 serde_with: None,
4351 })
4352 }
4353
4354 fn find_recursive_anchor_schema(&self) -> Option<String> {
4356 for (schema_name, schema) in &self.schemas {
4358 let details = schema.details();
4359 if details.recursive_anchor == Some(true) {
4360 return Some(schema_name.clone());
4361 }
4362 }
4363
4364 None
4368 }
4369
4370 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4373 if let Schema::AnyOf { any_of, .. } = schema {
4375 if any_of.len() == 2 {
4376 let has_null = any_of
4377 .iter()
4378 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4379 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4380
4381 if has_null && has_empty_object {
4382 return true;
4383 }
4384 }
4385 }
4386
4387 self.is_dynamic_object_pattern(schema)
4389 }
4390
4391 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4393 let is_object = match schema.schema_type() {
4395 Some(OpenApiSchemaType::Object) => true,
4396 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4397 _ => false,
4398 };
4399
4400 if !is_object {
4401 return false;
4402 }
4403
4404 let details = schema.details();
4405
4406 if self.has_explicit_additional_properties(schema) {
4409 return false;
4410 }
4411
4412 let no_properties = details
4414 .properties
4415 .as_ref()
4416 .map(|props| props.is_empty())
4417 .unwrap_or(true);
4418
4419 if no_properties {
4420 let has_structural_constraints = details
4423 .required
4424 .as_ref()
4425 .map(|req| req.iter().any(|r| r != "type"))
4426 .unwrap_or(false)
4427 || details.pattern_properties.is_some()
4428 || details.property_names.is_some()
4429 || details.min_properties.is_some()
4430 || details.max_properties.is_some()
4431 || details.dependent_required.is_some()
4432 || details.dependent_schemas.is_some()
4433 || details.if_schema.is_some()
4434 || details.then_schema.is_some()
4435 || details.else_schema.is_some();
4436
4437 return !has_structural_constraints;
4438 }
4439
4440 false
4441 }
4442
4443 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4445 let details = schema.details();
4446
4447 matches!(
4449 &details.additional_properties,
4450 Some(crate::openapi::AdditionalProperties::Boolean(true))
4451 | Some(crate::openapi::AdditionalProperties::Schema(_))
4452 )
4453 }
4454
4455 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4457 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4458 .map_err(GeneratorError::ParseError)?;
4459 let mut canonical_operation_ids = HashSet::new();
4464
4465 if let Some(paths) = &spec.paths {
4466 for (path, path_item) in paths {
4467 let resolved = self.resolve_path_item(path_item, &spec)?;
4469 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4470 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4471 }
4472 }
4473 if let Some(webhooks) = &spec.webhooks {
4480 for (name, path_item) in webhooks {
4481 let synthetic_path = format!("/__webhook__/{name}");
4482 self.ingest_path_item_operations(
4483 &synthetic_path,
4484 path_item,
4485 analysis,
4486 &mut canonical_operation_ids,
4487 )?;
4488 }
4489 }
4490 Ok(())
4491 }
4492
4493 fn resolve_path_item(
4497 &self,
4498 path_item: &crate::openapi::PathItem,
4499 spec: &crate::openapi::OpenApiSpec,
4500 ) -> Result<Option<crate::openapi::PathItem>> {
4501 let Some(reference) = &path_item.reference else {
4502 return Ok(None);
4503 };
4504 let target_name = reference
4505 .strip_prefix("#/components/pathItems/")
4506 .ok_or_else(|| {
4507 GeneratorError::UnresolvedReference(format!(
4508 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4509 ))
4510 })?;
4511 let pi = spec
4512 .components
4513 .as_ref()
4514 .and_then(|c| c.path_items.as_ref())
4515 .and_then(|map| map.get(target_name))
4516 .ok_or_else(|| {
4517 GeneratorError::UnresolvedReference(format!(
4518 "Path Item ref {reference} not found in components/pathItems"
4519 ))
4520 })?;
4521 Ok(Some(pi.clone()))
4522 }
4523
4524 fn ingest_path_item_operations(
4525 &mut self,
4526 path: &str,
4527 path_item: &crate::openapi::PathItem,
4528 analysis: &mut SchemaAnalysis,
4529 canonical_operation_ids: &mut HashSet<String>,
4530 ) -> Result<()> {
4531 for (method, operation) in path_item.operations() {
4532 let raw_operation_id = operation
4534 .operation_id
4535 .clone()
4536 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4537
4538 let operation_id = if canonical_operation_ids
4549 .contains(&Self::canonical_operation_id(&raw_operation_id))
4550 {
4551 let method_lower = method.to_lowercase();
4552 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4553 let mut suffix = 2;
4554 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4555 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4556 suffix += 1;
4557 }
4558 eprintln!(
4559 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4560 raw_operation_id, method, path, candidate
4561 );
4562 candidate
4563 } else {
4564 raw_operation_id.clone()
4565 };
4566
4567 let (op_info, responses) = self.analyze_single_operation(
4568 &operation_id,
4569 method,
4570 path,
4571 operation,
4572 path_item.parameters.as_ref(),
4573 analysis,
4574 )?;
4575 analysis
4576 .operation_id_aliases
4577 .entry(raw_operation_id)
4578 .or_default()
4579 .push(operation_id.clone());
4580 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4581 analysis
4582 .operation_responses
4583 .insert(operation_id.clone(), responses);
4584 analysis.operations.insert(operation_id, op_info);
4585 }
4586 Ok(())
4587 }
4588
4589 fn canonical_operation_id(operation_id: &str) -> String {
4590 use heck::ToPascalCase;
4591 operation_id.replace('.', "_").to_pascal_case()
4592 }
4593
4594 fn generate_operation_id(method: &str, path: &str) -> String {
4597 let mut operation_id = method.to_lowercase();
4599
4600 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4602
4603 for part in path_parts {
4604 if part.is_empty() {
4605 continue;
4606 }
4607
4608 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4610 &part[1..part.len() - 1]
4611 } else {
4612 part
4613 };
4614
4615 let pascal_case_part = cleaned_part
4617 .split(&['-', '_'][..])
4618 .map(|s| {
4619 let mut chars = s.chars();
4620 match chars.next() {
4621 None => String::new(),
4622 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4623 }
4624 })
4625 .collect::<String>();
4626
4627 operation_id.push_str(&pascal_case_part);
4628 }
4629
4630 operation_id
4631 }
4632
4633 fn analyze_single_operation(
4635 &mut self,
4636 operation_id: &str,
4637 method: &str,
4638 path: &str,
4639 operation: &crate::openapi::Operation,
4640 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4641 _analysis: &mut SchemaAnalysis,
4642 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
4643 let raw_path_item = self
4644 .openapi_spec
4645 .get("paths")
4646 .and_then(|paths| paths.get(path))
4647 .cloned();
4648 let raw_operation = raw_path_item
4649 .as_ref()
4650 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
4651 .cloned();
4652 let request_body = operation
4653 .request_body
4654 .as_ref()
4655 .map(|request_body| self.resolve_request_body(request_body))
4656 .transpose()?;
4657 let mut op_info = OperationInfo {
4658 operation_id: operation_id.to_string(),
4659 method: method.to_uppercase(),
4660 path: normalize_operation_path(path),
4661 summary: operation.summary.clone(),
4662 description: operation.description.clone(),
4663 request_body: None,
4664 request_body_required: request_body
4666 .as_ref()
4667 .and_then(|rb| rb.required)
4668 .unwrap_or(false),
4669 response_schemas: BTreeMap::new(),
4670 parameters: Vec::new(),
4671 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4674 };
4675 let mut operation_responses = BTreeMap::new();
4676
4677 if let Some(request_body) = &request_body {
4679 use crate::openapi::{
4680 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
4681 media_type_essence,
4682 };
4683 if let Some((content_type, maybe_schema)) = request_body.best_content() {
4684 op_info.request_body = if is_json_media_type(content_type) {
4685 match maybe_schema {
4686 Some(s) => {
4687 let validation_schema = self
4688 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4689 .unwrap_or(
4690 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4691 );
4692 Some(
4693 self.resolve_or_inline_schema(s, operation_id, "Request")
4694 .map(|name| RequestBodyContent::Json {
4695 schema_name: name,
4696 media_type: content_type.to_string(),
4697 validation_schema,
4698 })?,
4699 )
4700 }
4701 None => Some(RequestBodyContent::SchemaLess {
4702 media_type: content_type.to_string(),
4703 }),
4704 }
4705 } else if is_form_urlencoded_media_type(content_type) {
4706 match maybe_schema {
4707 Some(s) => {
4708 let validation_schema = self
4709 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4710 .unwrap_or(
4711 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4712 );
4713 Some(
4714 self.resolve_or_inline_schema(s, operation_id, "Request")
4715 .map(|name| RequestBodyContent::FormUrlEncoded {
4716 schema_name: name,
4717 media_type: content_type.to_string(),
4718 validation_schema,
4719 })?,
4720 )
4721 }
4722 None => Some(RequestBodyContent::SchemaLess {
4723 media_type: content_type.to_string(),
4724 }),
4725 }
4726 } else if media_type_essence(content_type)
4727 .eq_ignore_ascii_case("multipart/form-data")
4728 {
4729 match maybe_schema {
4730 Some(schema) => {
4731 let validation_schema = self
4732 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4733 .unwrap_or(
4734 serde_json::to_value(schema)
4735 .map_err(GeneratorError::ParseError)?,
4736 );
4737 Some(
4738 self.resolve_or_inline_schema(schema, operation_id, "Request")
4739 .map(|schema_name| RequestBodyContent::Multipart {
4740 schema_name,
4741 media_type: content_type.to_string(),
4742 validation_schema,
4743 })?,
4744 )
4745 }
4746 None => Some(RequestBodyContent::SchemaLess {
4747 media_type: content_type.to_string(),
4748 }),
4749 }
4750 } else if is_binary_media_type(content_type, maybe_schema) {
4751 if media_type_essence(content_type)
4752 .eq_ignore_ascii_case("application/octet-stream")
4753 {
4754 Some(RequestBodyContent::OctetStream {
4755 media_type: content_type.to_string(),
4756 })
4757 } else {
4758 Some(RequestBodyContent::Binary {
4759 media_type: content_type.to_string(),
4760 })
4761 }
4762 } else if crate::openapi::is_text_media_type(content_type) {
4763 Some(RequestBodyContent::TextPlain {
4768 media_type: content_type.to_string(),
4769 })
4770 } else {
4771 None
4772 };
4773 }
4774 if op_info.request_body.is_none() {
4775 let mut media_types = request_body
4776 .content
4777 .as_ref()
4778 .map(|content| content.keys().cloned().collect::<Vec<_>>())
4779 .unwrap_or_default();
4780 media_types.sort();
4781 if !media_types.is_empty() {
4782 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
4783 }
4784 }
4785 }
4786
4787 if let Some(responses) = &operation.responses {
4789 for (status_code, response) in responses {
4790 let response = self.resolve_response(response)?;
4791 let supports_streaming = response.content.as_ref().is_some_and(|content| {
4797 content
4798 .keys()
4799 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
4800 });
4801 if supports_streaming {
4802 op_info.supports_streaming = true;
4803 }
4804
4805 let mut response_info = OperationResponse {
4806 supports_streaming,
4807 has_content: response
4808 .content
4809 .as_ref()
4810 .is_some_and(|content| !content.is_empty()),
4811 ..Default::default()
4812 };
4813 if let Some((media_type, schema)) = response.json_content() {
4814 if let Some(schema_ref) = schema.reference() {
4815 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4817 op_info
4818 .response_schemas
4819 .insert(status_code.clone(), schema_name.to_string());
4820 response_info.schema_name = Some(schema_name.to_string());
4821 response_info.media_type = Some(media_type.to_string());
4822 response_info.body = Some(OperationResponseBody::Json {
4823 schema_name: schema_name.to_string(),
4824 media_type: media_type.to_string(),
4825 });
4826 }
4827 } else {
4828 let synthetic_name =
4830 self.generate_inline_response_type_name(operation_id, status_code);
4831
4832 let mut deps = HashSet::new();
4834 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4835
4836 op_info
4837 .response_schemas
4838 .insert(status_code.clone(), synthetic_name.clone());
4839 response_info.body = Some(OperationResponseBody::Json {
4840 schema_name: synthetic_name.clone(),
4841 media_type: media_type.to_string(),
4842 });
4843 response_info.schema_name = Some(synthetic_name);
4844 response_info.media_type = Some(media_type.to_string());
4845 }
4846 }
4847 if response_info.body.is_none()
4848 && let Some(content) = response.content.as_ref()
4849 {
4850 let selected = content
4851 .iter()
4852 .find(|(media_type, media)| {
4853 matches!(
4854 crate::openapi::classify_response_media_type(
4855 media_type,
4856 media.schema.as_ref()
4857 ),
4858 crate::openapi::ResponseMediaKind::Text
4859 )
4860 })
4861 .or_else(|| {
4862 content.iter().find(|(media_type, media)| {
4863 matches!(
4864 crate::openapi::classify_response_media_type(
4865 media_type,
4866 media.schema.as_ref()
4867 ),
4868 crate::openapi::ResponseMediaKind::Binary
4869 ) && !crate::openapi::is_wildcard_media_type(media_type)
4870 })
4871 })
4872 .or_else(|| {
4873 content.iter().find(|(media_type, media)| {
4874 matches!(
4875 crate::openapi::classify_response_media_type(
4876 media_type,
4877 media.schema.as_ref()
4878 ),
4879 crate::openapi::ResponseMediaKind::Binary
4880 )
4881 })
4882 });
4883 if let Some((media_type, media)) = selected {
4884 response_info.body = match crate::openapi::classify_response_media_type(
4885 media_type,
4886 media.schema.as_ref(),
4887 ) {
4888 crate::openapi::ResponseMediaKind::Text => {
4889 Some(OperationResponseBody::Text {
4890 media_type: media_type.clone(),
4891 })
4892 }
4893 crate::openapi::ResponseMediaKind::Binary => {
4894 Some(OperationResponseBody::Binary {
4895 media_type: media_type.clone(),
4896 wildcard: crate::openapi::is_wildcard_media_type(media_type),
4897 })
4898 }
4899 _ => None,
4900 };
4901 }
4902 }
4903 response_info.unsupported_media_types = response
4904 .content
4905 .as_ref()
4906 .into_iter()
4907 .flat_map(|content| content.iter())
4908 .filter(|(media_type, content)| {
4909 match crate::openapi::classify_response_media_type(
4910 media_type,
4911 content.schema.as_ref(),
4912 ) {
4913 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
4914 crate::openapi::ResponseMediaKind::Unsupported => true,
4915 crate::openapi::ResponseMediaKind::EventStream
4916 | crate::openapi::ResponseMediaKind::Text
4917 | crate::openapi::ResponseMediaKind::Binary => false,
4918 }
4919 })
4920 .map(|(media_type, _)| media_type.clone())
4921 .collect();
4922 operation_responses.insert(status_code.clone(), response_info);
4923 }
4924 }
4925
4926 if op_info.supports_streaming
4929 && let Some(parameters) = &operation.parameters
4930 {
4931 for param in parameters {
4932 if let Some(name) = param.name.as_deref() {
4933 if name.eq_ignore_ascii_case("stream") {
4934 op_info.stream_parameter = Some(name.to_string());
4935 break;
4936 }
4937 }
4938 }
4939 }
4940
4941 if let Some(parameters) = &operation.parameters {
4943 for (index, param) in parameters.iter().enumerate() {
4944 let resolved = self.resolve_parameter(param).into_owned();
4948 let validation_schema = raw_operation
4949 .as_ref()
4950 .and_then(|operation| operation.get("parameters"))
4951 .and_then(Value::as_array)
4952 .and_then(|parameters| parameters.get(index))
4953 .and_then(|parameter| self.raw_parameter_schema(parameter));
4954 if let Some(param_info) =
4955 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4956 {
4957 op_info.parameters.push(param_info);
4958 }
4959 }
4960 }
4961
4962 if let Some(path_params) = path_item_parameters {
4964 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4965 .parameters
4966 .iter()
4967 .map(|p| (p.name.clone(), p.location.clone()))
4968 .collect();
4969 for (index, param) in path_params.iter().enumerate() {
4970 let resolved = self.resolve_parameter(param).into_owned();
4971 let validation_schema = raw_path_item
4972 .as_ref()
4973 .and_then(|path_item| path_item.get("parameters"))
4974 .and_then(Value::as_array)
4975 .and_then(|parameters| parameters.get(index))
4976 .and_then(|parameter| self.raw_parameter_schema(parameter));
4977 if let Some(param_info) =
4978 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4979 {
4980 if !existing_keys
4981 .contains(&(param_info.name.clone(), param_info.location.clone()))
4982 {
4983 op_info.parameters.push(param_info);
4984 }
4985 }
4986 }
4987 }
4988
4989 let mut declared_path_names: std::collections::HashSet<String> = op_info
4997 .parameters
4998 .iter()
4999 .filter(|p| p.location == "path")
5000 .map(|p| p.name.clone())
5001 .collect();
5002 let bytes = path.as_bytes().iter();
5003 let mut current = String::new();
5004 let mut in_brace = false;
5005 let mut synthesized: Vec<String> = Vec::new();
5006 for b in bytes {
5007 match *b {
5008 b'{' => {
5009 in_brace = true;
5010 current.clear();
5011 }
5012 b'}' if in_brace => {
5013 in_brace = false;
5014 if !current.is_empty() && !declared_path_names.contains(¤t) {
5015 synthesized.push(current.clone());
5016 declared_path_names.insert(current.clone());
5017 }
5018 }
5019 _ if in_brace => current.push(*b as char),
5020 _ => {}
5021 }
5022 }
5023 for name in synthesized {
5024 eprintln!(
5025 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
5026 path, name
5027 );
5028 op_info.parameters.push(ParameterInfo {
5029 name,
5030 location: "path".to_string(),
5031 required: true,
5032 schema_ref: None,
5033 rust_type: "String".to_string(),
5034 description: None,
5035 enum_values: None,
5036 enum_varnames: None,
5037 rust_ident: None,
5038 query_serialization: None,
5039 validation_schema: None,
5040 });
5041 }
5042
5043 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
5051 for p in op_info.parameters.iter_mut() {
5052 let raw = base_param_ident(&p.name);
5053 let mut chosen = raw.clone();
5054 let mut suffix = 2;
5055 while !used.insert(chosen.clone()) {
5056 chosen = format!("{raw}_{suffix}");
5057 suffix += 1;
5058 }
5059 p.rust_ident = Some(chosen);
5060 }
5061
5062 Ok((op_info, operation_responses))
5063 }
5064
5065 fn resolve_request_body(
5067 &self,
5068 request_body: &crate::openapi::RequestBody,
5069 ) -> Result<crate::openapi::RequestBody> {
5070 let mut current = request_body.clone();
5071 let mut visited = HashSet::new();
5072 while let Some(reference) = current.reference.clone() {
5073 if !visited.insert(reference.clone()) {
5074 return Err(GeneratorError::CircularDependency(format!(
5075 "request body reference {reference}"
5076 )));
5077 }
5078
5079 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5080 GeneratorError::UnresolvedReference(format!(
5081 "external request body reference `{reference}` is not supported"
5082 ))
5083 })?;
5084 if !pointer.is_empty() && !pointer.starts_with('/') {
5085 return Err(GeneratorError::UnresolvedReference(format!(
5086 "request body reference `{reference}` is not a local JSON Pointer"
5087 )));
5088 }
5089 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5090 GeneratorError::UnresolvedReference(format!(
5091 "request body reference `{reference}` does not exist"
5092 ))
5093 })?;
5094 let object = value.as_object().ok_or_else(|| {
5095 GeneratorError::InvalidSchema(format!(
5096 "request body reference `{reference}` must target an object"
5097 ))
5098 })?;
5099 if !["$ref", "description", "required", "content"]
5100 .iter()
5101 .any(|field| object.contains_key(*field))
5102 {
5103 return Err(GeneratorError::InvalidSchema(format!(
5104 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
5105 )));
5106 }
5107 current = serde_json::from_value(value.clone()).map_err(|error| {
5108 GeneratorError::InvalidSchema(format!(
5109 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
5110 ))
5111 })?;
5112 }
5113 Ok(current)
5114 }
5115
5116 fn resolve_response(
5123 &self,
5124 response: &crate::openapi::Response,
5125 ) -> Result<crate::openapi::Response> {
5126 let mut current = response.clone();
5127 let mut visited = HashSet::new();
5128 while let Some(reference) = current.reference.clone() {
5129 if !visited.insert(reference.clone()) {
5130 return Err(GeneratorError::CircularDependency(format!(
5131 "response reference {reference}"
5132 )));
5133 }
5134
5135 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5136 GeneratorError::UnresolvedReference(format!(
5137 "external response reference `{reference}` is not supported"
5138 ))
5139 })?;
5140 if !pointer.is_empty() && !pointer.starts_with('/') {
5141 return Err(GeneratorError::UnresolvedReference(format!(
5142 "response reference `{reference}` is not a local JSON Pointer"
5143 )));
5144 }
5145 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5146 GeneratorError::UnresolvedReference(format!(
5147 "response reference `{reference}` does not exist"
5148 ))
5149 })?;
5150 let object = value.as_object().ok_or_else(|| {
5151 GeneratorError::InvalidSchema(format!(
5152 "response reference `{reference}` must target an object"
5153 ))
5154 })?;
5155 if !["$ref", "description", "headers", "content", "links"]
5156 .iter()
5157 .any(|field| object.contains_key(*field))
5158 {
5159 return Err(GeneratorError::InvalidSchema(format!(
5160 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
5161 )));
5162 }
5163 current = serde_json::from_value(value.clone()).map_err(|error| {
5164 GeneratorError::InvalidSchema(format!(
5165 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
5166 ))
5167 })?;
5168 }
5169 Ok(current)
5170 }
5171
5172 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
5179 use heck::ToPascalCase;
5180 let base_name = operation_id.replace('.', "_").to_pascal_case();
5181 let suffix = Self::status_code_suffix(status_code);
5182 format!("{}Response{}", base_name, suffix)
5183 }
5184
5185 fn status_code_suffix(status_code: &str) -> String {
5192 match status_code {
5193 "" | "200" => String::new(),
5194 "default" | "Default" => "Default".to_string(),
5195 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
5196 other => other.to_ascii_lowercase(),
5197 }
5198 }
5199
5200 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
5202 use heck::ToPascalCase;
5203 let base_name = operation_id.replace('.', "_").to_pascal_case();
5207 format!("{}Request", base_name)
5208 }
5209
5210 fn resolve_or_inline_schema(
5213 &mut self,
5214 schema: &crate::openapi::Schema,
5215 operation_id: &str,
5216 suffix: &str,
5217 ) -> Result<String> {
5218 if let Some(schema_ref) = schema.reference()
5219 && let Some(schema_name) = self.extract_schema_name(schema_ref)
5220 {
5221 return Ok(schema_name.to_string());
5222 }
5223 let synthetic_name = if suffix == "Request" {
5225 self.generate_inline_request_type_name(operation_id)
5226 } else {
5227 self.generate_inline_response_type_name(operation_id, "")
5228 };
5229 let mut deps = HashSet::new();
5230 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5231 Ok(synthetic_name)
5232 }
5233
5234 fn resolve_parameter<'a>(
5237 &'a self,
5238 param: &'a crate::openapi::Parameter,
5239 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
5240 if let Some(ref_str) = param.reference.as_deref() {
5241 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
5242 if let Some(resolved) = self.component_parameters.get(param_name) {
5243 return std::borrow::Cow::Borrowed(resolved);
5244 }
5245 }
5246 }
5247 std::borrow::Cow::Borrowed(param)
5248 }
5249
5250 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
5263 if self.resolve_cached_schema(name).is_some_and(|schema| {
5264 matches!(
5265 schema.schema_type,
5266 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
5267 )
5268 }) {
5269 return true;
5270 }
5271 let Some(schema_value) = self
5272 .openapi_spec
5273 .get("components")
5274 .and_then(|c| c.get("schemas"))
5275 .and_then(|s| s.get(name))
5276 else {
5277 return false;
5278 };
5279 let is_string_type = schema_value
5280 .get("type")
5281 .and_then(|v| v.as_str())
5282 .map(|s| s == "string")
5283 .unwrap_or(false);
5284 let has_enum_or_const =
5285 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
5286 is_string_type && has_enum_or_const
5287 }
5288
5289 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
5290 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
5291 return Some(value.clone());
5292 };
5293 let pointer = reference.strip_prefix('#')?;
5294 self.openapi_spec.pointer(pointer).cloned()
5295 }
5296
5297 fn raw_request_body_schema(
5298 &self,
5299 operation: Option<&Value>,
5300 content_type: &str,
5301 ) -> Option<Value> {
5302 let request_body = operation?.get("requestBody")?;
5303 self.resolve_raw_local_reference(request_body)?
5304 .get("content")?
5305 .get(content_type)?
5306 .get("schema")
5307 .cloned()
5308 }
5309
5310 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
5311 self.resolve_raw_local_reference(parameter)?
5312 .get("schema")
5313 .cloned()
5314 }
5315
5316 fn analyze_parameter(
5317 &mut self,
5318 param: &crate::openapi::Parameter,
5319 operation_id: &str,
5320 raw_validation_schema: Option<Value>,
5321 ) -> Result<Option<ParameterInfo>> {
5322 use heck::ToPascalCase;
5323
5324 let name = param.name.as_deref().unwrap_or("");
5325 let location = param.location.as_deref().unwrap_or("");
5326 let required = param.required.unwrap_or(false);
5327 let validation_schema = match raw_validation_schema {
5328 Some(schema) => Some(schema),
5329 None => param
5330 .schema
5331 .as_ref()
5332 .map(serde_json::to_value)
5333 .transpose()
5334 .map_err(GeneratorError::ParseError)?,
5335 };
5336
5337 let mut rust_type = "String".to_string();
5338 let mut schema_ref = None;
5339 let mut enum_values: Option<Vec<String>> = None;
5340 let mut enum_varnames: Option<Vec<String>> = None;
5341 let mut query_serialization: Option<QuerySerialization> = None;
5342
5343 let is_query = location == "query";
5349 let is_simple_header = location == "header"
5350 && matches!(param.style.as_deref(), None | Some("simple"))
5351 && param.explode != Some(true);
5352 let form_style = matches!(param.style.as_deref(), None | Some("form"));
5353 let form_exploded = form_style && param.explode.unwrap_or(true);
5354 let deep_object =
5355 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
5356
5357 let object_serialization = if !is_query {
5358 None
5359 } else if deep_object {
5360 Some(QuerySerialization::DeepObject)
5361 } else if form_exploded {
5362 Some(QuerySerialization::FormExplodedObject)
5363 } else if form_style {
5364 Some(QuerySerialization::FormObject)
5365 } else {
5366 None
5367 };
5368
5369 if let Some(schema) = ¶m.schema {
5370 if let Some(ref_str) = schema.reference() {
5371 if let Some(name) = self.extract_schema_name(ref_str) {
5377 if self.referenced_schema_is_string_enum(name) {
5378 schema_ref = Some(name.to_string());
5379 } else if object_serialization.is_some()
5380 && self.referenced_schema_is_object(name)
5381 {
5382 schema_ref = Some(name.to_string());
5383 query_serialization = if form_exploded && self.uses_aws_query_conventions()
5384 {
5385 match self.referenced_array_struct_item_type(name, 1) {
5386 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5387 Some(QuerySerialization::FormExplodedNestedObject {
5388 properties,
5389 })
5390 }
5391 _ => object_serialization.clone(),
5392 }
5393 } else {
5394 object_serialization.clone()
5395 };
5396 } else if (is_query && form_style || is_simple_header)
5397 && let Some(item_type) = self.referenced_array_param_item_type(name)
5398 {
5399 schema_ref = Some(name.to_string());
5405 query_serialization = Some(if is_simple_header {
5406 QuerySerialization::SimpleHeaderArray { item_type }
5407 } else if form_exploded {
5408 QuerySerialization::FormExplodedArray { item_type }
5409 } else {
5410 QuerySerialization::FormArray { item_type }
5411 });
5412 }
5413 }
5414 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
5415 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5420 let param_pascal = name.to_pascal_case();
5421 let synthetic_name = format!("{op_pascal}{param_pascal}");
5422 let mut deps = HashSet::new();
5423 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5424 schema_ref = Some(synthetic_name.clone());
5425 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
5426 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
5427 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5428 Some(QuerySerialization::FormExplodedNestedObject { properties })
5429 }
5430 _ => object_serialization.clone(),
5431 }
5432 } else {
5433 object_serialization.clone()
5434 };
5435 } else if (is_query && form_style || is_simple_header)
5436 && matches!(
5437 schema.schema_type(),
5438 Some(crate::openapi::SchemaType::Array)
5439 )
5440 && let Some(item_type) = self.array_param_item_type(schema)
5441 {
5442 query_serialization = Some(if is_simple_header {
5450 QuerySerialization::SimpleHeaderArray { item_type }
5451 } else if form_exploded {
5452 QuerySerialization::FormExplodedArray { item_type }
5453 } else {
5454 QuerySerialization::FormArray { item_type }
5455 });
5456 } else if let Some(schema_type) = schema.schema_type() {
5457 let format = schema.details().format.clone();
5463 rust_type = match schema_type {
5464 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5465 crate::openapi::SchemaType::Integer => {
5466 self.type_mapper.integer_format(format.as_deref()).rust_type
5467 }
5468 crate::openapi::SchemaType::Number => {
5469 self.type_mapper.number_format(format.as_deref()).rust_type
5470 }
5471 crate::openapi::SchemaType::String => "String".to_string(),
5472 _ => "String".to_string(),
5473 };
5474
5475 if matches!(schema_type, crate::openapi::SchemaType::String) {
5476 let details = schema.details();
5477 if details.is_string_enum() {
5478 if let Some(values) = details.string_enum_values() {
5479 if !values.is_empty() {
5480 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5481 let param_pascal = name.to_pascal_case();
5482 rust_type = format!("{op_pascal}{param_pascal}");
5483 enum_varnames = details
5488 .extra
5489 .get("x-enum-varnames")
5490 .and_then(Value::as_array)
5491 .map(|raw| {
5492 raw.iter()
5493 .filter_map(Value::as_str)
5494 .map(str::to_owned)
5495 .collect::<Vec<_>>()
5496 })
5497 .filter(|names| names.len() == values.len());
5498 enum_values = Some(values);
5499 }
5500 }
5501 }
5502 }
5503 }
5504
5505 if is_query && query_serialization.is_none() {
5506 let referenced_name = schema
5507 .reference()
5508 .and_then(|reference| self.extract_schema_name(reference));
5509 let is_object = referenced_name
5510 .is_some_and(|name| self.referenced_schema_is_object(name))
5511 || Self::schema_is_inline_object(schema);
5512 let is_array = referenced_name
5513 .is_some_and(|name| self.referenced_schema_is_array(name))
5514 || matches!(
5515 schema.schema_type(),
5516 Some(crate::openapi::SchemaType::Array)
5517 );
5518 let is_composed = referenced_name
5519 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
5520 let reason = if param.style.as_deref() == Some("deepObject")
5521 && param.explode == Some(false)
5522 {
5523 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
5524 } else if param.style.as_deref() == Some("deepObject") && !is_object {
5525 Some("style=deepObject is defined only for object query parameters".to_string())
5526 } else if is_object {
5527 Some(format!(
5528 "object query parameters do not support style={}",
5529 param.style.as_deref().unwrap_or("form")
5530 ))
5531 } else if is_array && form_style {
5532 Some(
5533 "form array query parameter exceeds the supported nesting bound or contains a non-scalar leaf; supported shapes are scalar arrays, arrays of flat scalar objects, and one nested scalar-object array"
5534 .to_string(),
5535 )
5536 } else if is_array {
5537 Some(format!(
5538 "array query parameters do not yet support style={}",
5539 param.style.as_deref().unwrap_or("form")
5540 ))
5541 } else if is_composed {
5542 Some(
5543 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
5544 .to_string(),
5545 )
5546 } else {
5547 None
5548 };
5549 if let Some(reason) = reason {
5550 query_serialization = Some(QuerySerialization::Unsupported { reason });
5551 }
5552 }
5553 }
5554
5555 Ok(Some(ParameterInfo {
5556 name: name.to_string(),
5557 location: location.to_string(),
5558 required,
5559 schema_ref,
5560 rust_type,
5561 description: param.description.clone(),
5562 enum_values,
5563 enum_varnames,
5564 rust_ident: None,
5565 query_serialization,
5566 validation_schema,
5567 }))
5568 }
5569
5570 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
5579 let items = schema.details().items.as_deref()?;
5580 let unwrapped = unwrap_annotation_allof(items);
5584 if let Some(ref_str) = unwrapped.reference() {
5585 let name = self.extract_schema_name(ref_str)?;
5586 return self
5587 .referenced_array_scalar_item_type(name)
5588 .or_else(|| self.referenced_array_struct_item_type(name, 1));
5589 }
5590 let format = unwrapped.details().format.clone();
5591 let scalar = match unwrapped.schema_type()? {
5592 crate::openapi::SchemaType::String => "String".to_string(),
5593 crate::openapi::SchemaType::Integer => {
5594 self.type_mapper.integer_format(format.as_deref()).rust_type
5595 }
5596 crate::openapi::SchemaType::Number => {
5597 self.type_mapper.number_format(format.as_deref()).rust_type
5598 }
5599 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5600 _ => return None,
5601 };
5602 Some(ArrayItemType::Scalar(scalar))
5603 }
5604
5605 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
5608 let schema = self.resolve_cached_schema(name)?;
5609 let SchemaType::Array { item_type } = &schema.schema_type else {
5610 return None;
5611 };
5612 self.analyzed_array_item_type(item_type)
5613 }
5614
5615 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
5616 self.analyzed_array_item_type_at_depth(item_type, 1)
5617 }
5618
5619 fn referenced_array_struct_item_type(
5624 &self,
5625 name: &str,
5626 nested_array_depth: usize,
5627 ) -> Option<ArrayItemType> {
5628 let resolved = self.resolve_cached_schema(name)?;
5629 let SchemaType::Object {
5630 properties,
5631 required,
5632 additional_properties,
5633 } = &resolved.schema_type
5634 else {
5635 return None;
5636 };
5637 if properties.is_empty()
5638 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5639 {
5640 return None;
5641 }
5642 let mut projected = Vec::with_capacity(properties.len());
5643 let mut has_array = false;
5644 for (wire_name, property) in properties {
5645 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
5646 QueryStructPropertyType::Scalar(scalar)
5647 } else {
5648 if nested_array_depth == 0 {
5649 return None;
5650 }
5651 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
5652 let item_type =
5653 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
5654 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
5655 return None;
5656 }
5657 has_array = true;
5658 QueryStructPropertyType::Array { item_type }
5659 } else {
5660 has_array = true;
5661 QueryStructPropertyType::Object {
5662 properties: self.query_flat_object_properties(&property.schema_type)?,
5663 }
5664 }
5665 };
5666 projected.push(QueryStructProperty {
5667 wire_name: wire_name.clone(),
5668 required: required.contains(wire_name),
5669 value_type,
5670 });
5671 }
5672 if has_array {
5673 Some(ArrayItemType::NestedStructRef {
5674 schema_name: name.to_string(),
5675 properties: projected,
5676 })
5677 } else {
5678 Some(ArrayItemType::FlatStructRef {
5679 schema_name: name.to_string(),
5680 properties: projected,
5681 })
5682 }
5683 }
5684
5685 fn analyzed_array_item_type_at_depth(
5686 &self,
5687 item_type: &SchemaType,
5688 nested_array_depth: usize,
5689 ) -> Option<ArrayItemType> {
5690 match item_type {
5691 SchemaType::Primitive { rust_type, .. } => {
5692 Some(ArrayItemType::Scalar(rust_type.clone()))
5693 }
5694 SchemaType::Reference { target } => self
5695 .referenced_array_scalar_item_type(target)
5696 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
5697 _ => None,
5698 }
5699 }
5700
5701 fn resolve_query_array_type<'a>(
5702 &'a self,
5703 schema_type: &'a SchemaType,
5704 ) -> Option<&'a SchemaType> {
5705 match schema_type {
5706 SchemaType::Array { item_type } => Some(item_type),
5707 SchemaType::Reference { target } => {
5708 let resolved = self.resolve_cached_schema(target)?;
5709 let SchemaType::Array { item_type } = &resolved.schema_type else {
5710 return None;
5711 };
5712 Some(item_type)
5713 }
5714 _ => None,
5715 }
5716 }
5717
5718 fn query_flat_object_properties(
5719 &self,
5720 schema_type: &SchemaType,
5721 ) -> Option<Vec<QueryStructProperty>> {
5722 let schema_type = match schema_type {
5723 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
5724 other => other,
5725 };
5726 let SchemaType::Object {
5727 properties,
5728 required,
5729 additional_properties,
5730 } = schema_type
5731 else {
5732 return None;
5733 };
5734 if properties.is_empty()
5735 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5736 {
5737 return None;
5738 }
5739 properties
5740 .iter()
5741 .map(|(wire_name, property)| {
5742 Some(QueryStructProperty {
5743 wire_name: wire_name.clone(),
5744 required: required.contains(wire_name),
5745 value_type: QueryStructPropertyType::Scalar(
5746 self.query_scalar_type(&property.schema_type)?,
5747 ),
5748 })
5749 })
5750 .collect()
5751 }
5752
5753 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
5754 match schema_type {
5755 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
5756 "String" => Some(QueryScalarType::String),
5757 "bool" => Some(QueryScalarType::Boolean),
5758 value if value.starts_with('i') || value.starts_with('u') => {
5759 Some(QueryScalarType::Integer)
5760 }
5761 value if value.starts_with('f') => Some(QueryScalarType::Number),
5762 "serde_json::Value" => None,
5763 _ => Some(QueryScalarType::String),
5764 },
5765 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
5766 Some(QueryScalarType::String)
5767 }
5768 SchemaType::Reference { target } => {
5769 let resolved = self.resolve_cached_schema(target)?;
5770 self.query_scalar_type(&resolved.schema_type)
5771 }
5772 _ => None,
5773 }
5774 }
5775
5776 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
5784 let resolved = self.resolve_cached_schema(name)?;
5785 let supported = match &resolved.schema_type {
5786 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
5787 SchemaType::Primitive { .. } => resolved
5788 .original
5789 .get("type")
5790 .is_some_and(Self::query_scalar_type_value),
5791 _ => false,
5792 };
5793 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
5794 }
5795
5796 fn query_scalar_type_value(value: &Value) -> bool {
5797 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
5798 if let Some(value) = value.as_str() {
5799 return SCALARS.contains(&value);
5800 }
5801 let Some(values) = value.as_array() else {
5802 return false;
5803 };
5804 if !values.iter().all(Value::is_string) {
5805 return false;
5806 }
5807 let mut non_null = values
5808 .iter()
5809 .filter_map(Value::as_str)
5810 .filter(|value| *value != "null");
5811 let Some(scalar) = non_null.next() else {
5812 return false;
5813 };
5814 non_null.next().is_none() && SCALARS.contains(&scalar)
5815 }
5816
5817 fn referenced_schema_is_object(&self, name: &str) -> bool {
5821 self.resolve_cached_schema(name)
5822 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
5823 }
5824
5825 fn referenced_schema_is_array(&self, name: &str) -> bool {
5826 self.resolve_cached_schema(name)
5827 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
5828 }
5829
5830 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
5831 self.resolve_cached_schema(name).is_some_and(|schema| {
5832 matches!(
5833 schema.schema_type,
5834 SchemaType::Composition { .. }
5835 | SchemaType::Union { .. }
5836 | SchemaType::DiscriminatedUnion { .. }
5837 )
5838 })
5839 }
5840
5841 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
5842 let mut current = name;
5843 let mut visited = HashSet::new();
5844 loop {
5845 if !visited.insert(current) {
5846 return None;
5847 }
5848 let schema = self.resolved_cache.get(current)?;
5849 if let SchemaType::Reference { target } = &schema.schema_type {
5850 current = target;
5851 } else {
5852 return Some(schema);
5853 }
5854 }
5855 }
5856
5857 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
5859 match schema.schema_type() {
5860 Some(crate::openapi::SchemaType::Object) => true,
5861 None => schema.details().properties.is_some(),
5862 _ => false,
5863 }
5864 }
5865}
5866
5867fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
5868 let Some(schemas) = openapi_spec
5869 .pointer_mut("/components/schemas")
5870 .and_then(Value::as_object_mut)
5871 else {
5872 return;
5873 };
5874
5875 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
5876 for name in schemas.keys() {
5877 names_by_rust_name
5878 .entry(crate::generator::rust_type_name(name))
5879 .or_default()
5880 .push(name.clone());
5881 }
5882
5883 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
5886 let mut aliases = BTreeMap::<String, String>::new();
5887
5888 for (rust_name, mut names) in names_by_rust_name {
5889 if names.len() < 2 {
5890 continue;
5891 }
5892
5893 names.sort_by_key(|name| (name != &rust_name, name.clone()));
5896 for source_name in names.into_iter().skip(1) {
5897 let mut suffix = 2;
5898 let replacement = loop {
5899 let candidate = format!("{rust_name}{suffix}");
5900 if claimed_rust_names.insert(candidate.clone()) {
5901 break candidate;
5902 }
5903 suffix += 1;
5904 };
5905
5906 eprintln!(
5907 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
5908 );
5909 aliases.insert(source_name, replacement);
5910 }
5911 }
5912
5913 if aliases.is_empty() {
5914 return;
5915 }
5916
5917 let original_schemas = std::mem::take(schemas);
5918 for (name, schema) in original_schemas {
5919 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
5920 }
5921
5922 rewrite_component_schema_references(openapi_spec, &aliases);
5923}
5924
5925fn disambiguate_analyzed_schema_names(
5926 analysis: &mut SchemaAnalysis,
5927 component_schemas: &BTreeMap<String, Schema>,
5928) {
5929 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
5930 for name in analysis.schemas.keys() {
5931 names_by_rust_name
5932 .entry(crate::generator::rust_type_name(name))
5933 .or_default()
5934 .push(name.clone());
5935 }
5936
5937 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
5938 let mut aliases = BTreeMap::<String, String>::new();
5939
5940 for (rust_name, mut names) in names_by_rust_name {
5941 if names.len() < 2 {
5942 continue;
5943 }
5944 names.sort_by_key(|name| {
5945 (
5946 !component_schemas.contains_key(name),
5947 name != &rust_name,
5948 name.clone(),
5949 )
5950 });
5951
5952 for source_name in names.into_iter().skip(1) {
5953 let mut suffix = 2;
5954 let replacement = loop {
5955 let candidate = format!("{rust_name}{suffix}");
5956 if claimed_rust_names.insert(candidate.clone()) {
5957 break candidate;
5958 }
5959 suffix += 1;
5960 };
5961 eprintln!(
5962 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
5963 );
5964 aliases.insert(source_name, replacement);
5965 }
5966 }
5967
5968 if aliases.is_empty() {
5969 return;
5970 }
5971
5972 let original_schemas = std::mem::take(&mut analysis.schemas);
5973 for (name, mut schema) in original_schemas {
5974 schema.name = renamed_schema_name(&schema.name, &aliases);
5975 schema.dependencies = schema
5976 .dependencies
5977 .into_iter()
5978 .map(|name| renamed_schema_name(&name, &aliases))
5979 .collect();
5980 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
5981 analysis
5982 .schemas
5983 .insert(renamed_schema_name(&name, &aliases), schema);
5984 }
5985
5986 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
5987 for (name, dependencies) in original_edges {
5988 analysis.dependencies.edges.insert(
5989 renamed_schema_name(&name, &aliases),
5990 dependencies
5991 .into_iter()
5992 .map(|name| renamed_schema_name(&name, &aliases))
5993 .collect(),
5994 );
5995 }
5996 analysis.dependencies.recursive_schemas = analysis
5997 .dependencies
5998 .recursive_schemas
5999 .iter()
6000 .map(|name| renamed_schema_name(name, &aliases))
6001 .collect();
6002
6003 analysis.patterns.tagged_enum_schemas = analysis
6004 .patterns
6005 .tagged_enum_schemas
6006 .iter()
6007 .map(|name| renamed_schema_name(name, &aliases))
6008 .collect();
6009 analysis.patterns.untagged_enum_schemas = analysis
6010 .patterns
6011 .untagged_enum_schemas
6012 .iter()
6013 .map(|name| renamed_schema_name(name, &aliases))
6014 .collect();
6015 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
6016 .into_iter()
6017 .map(|(name, mappings)| {
6018 (
6019 renamed_schema_name(&name, &aliases),
6020 mappings
6021 .into_iter()
6022 .map(|(value, schema_name)| {
6023 (value, renamed_schema_name(&schema_name, &aliases))
6024 })
6025 .collect(),
6026 )
6027 })
6028 .collect();
6029
6030 for operation in analysis.operations.values_mut() {
6031 if let Some(request_body) = &mut operation.request_body {
6032 rewrite_request_body_schema_name(request_body, &aliases);
6033 }
6034 for schema_name in operation.response_schemas.values_mut() {
6035 *schema_name = renamed_schema_name(schema_name, &aliases);
6036 }
6037 for parameter in &mut operation.parameters {
6038 if let Some(schema_name) = &mut parameter.schema_ref {
6039 *schema_name = renamed_schema_name(schema_name, &aliases);
6040 }
6041 if let Some(serialization) = &mut parameter.query_serialization {
6042 rewrite_query_serialization_schema_names(serialization, &aliases);
6043 }
6044 }
6045 }
6046
6047 for responses in analysis.operation_responses.values_mut() {
6048 for response in responses.values_mut() {
6049 if let Some(schema_name) = &mut response.schema_name {
6050 *schema_name = renamed_schema_name(schema_name, &aliases);
6051 }
6052 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
6053 *schema_name = renamed_schema_name(schema_name, &aliases);
6054 }
6055 }
6056 }
6057}
6058
6059fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
6060 aliases
6061 .get(name)
6062 .cloned()
6063 .unwrap_or_else(|| name.to_string())
6064}
6065
6066fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
6067 match schema_type {
6068 SchemaType::Object {
6069 properties,
6070 additional_properties,
6071 ..
6072 } => {
6073 for property in properties.values_mut() {
6074 rewrite_schema_type_names(&mut property.schema_type, aliases);
6075 }
6076 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
6077 rewrite_schema_type_names(value_type, aliases);
6078 }
6079 }
6080 SchemaType::DiscriminatedUnion { variants, .. } => {
6081 for variant in variants {
6082 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
6083 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
6084 }
6085 }
6086 SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
6087 for variant in variants {
6088 variant.target = renamed_schema_name(&variant.target, aliases);
6089 }
6090 }
6091 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
6092 SchemaType::Reference { target } => {
6093 *target = renamed_schema_name(target, aliases);
6094 }
6095 SchemaType::Primitive { .. }
6096 | SchemaType::StringEnum { .. }
6097 | SchemaType::ExtensibleEnum { .. } => {}
6098 }
6099}
6100
6101fn rewrite_request_body_schema_name(
6102 request_body: &mut RequestBodyContent,
6103 aliases: &BTreeMap<String, String>,
6104) {
6105 match request_body {
6106 RequestBodyContent::Json { schema_name, .. }
6107 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
6108 | RequestBodyContent::Multipart { schema_name, .. } => {
6109 *schema_name = renamed_schema_name(schema_name, aliases);
6110 }
6111 _ => {}
6112 }
6113}
6114
6115fn rewrite_query_serialization_schema_names(
6116 serialization: &mut QuerySerialization,
6117 aliases: &BTreeMap<String, String>,
6118) {
6119 match serialization {
6120 QuerySerialization::FormExplodedArray { item_type }
6121 | QuerySerialization::FormArray { item_type }
6122 | QuerySerialization::SimpleHeaderArray { item_type } => {
6123 rewrite_array_item_type_schema_names(item_type, aliases);
6124 }
6125 QuerySerialization::FormExplodedNestedObject { properties } => {
6126 for property in properties {
6127 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6128 }
6129 }
6130 _ => {}
6131 }
6132}
6133
6134fn rewrite_array_item_type_schema_names(
6135 item_type: &mut ArrayItemType,
6136 aliases: &BTreeMap<String, String>,
6137) {
6138 match item_type {
6139 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
6140 ArrayItemType::FlatStructRef {
6141 schema_name,
6142 properties,
6143 }
6144 | ArrayItemType::NestedStructRef {
6145 schema_name,
6146 properties,
6147 } => {
6148 *schema_name = renamed_schema_name(schema_name, aliases);
6149 for property in properties {
6150 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6151 }
6152 }
6153 ArrayItemType::Scalar(_) => {}
6154 }
6155}
6156
6157fn rewrite_query_property_type_schema_names(
6158 property_type: &mut QueryStructPropertyType,
6159 aliases: &BTreeMap<String, String>,
6160) {
6161 match property_type {
6162 QueryStructPropertyType::Array { item_type } => {
6163 rewrite_array_item_type_schema_names(item_type, aliases)
6164 }
6165 QueryStructPropertyType::Object { properties } => {
6166 for property in properties {
6167 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6168 }
6169 }
6170 QueryStructPropertyType::Scalar(_) => {}
6171 }
6172}
6173
6174fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
6175 match value {
6176 Value::Array(values) => {
6177 for value in values {
6178 rewrite_component_schema_references(value, aliases);
6179 }
6180 }
6181 Value::Object(object) => {
6182 if let Some(Value::String(reference)) = object.get_mut("$ref") {
6183 rewrite_component_schema_reference(reference, aliases);
6184 }
6185
6186 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
6187 for target_value in mapping.values_mut() {
6188 let Some(target) = target_value.as_str() else {
6189 continue;
6190 };
6191 let replacement = aliases.get(target).cloned().or_else(|| {
6192 let mut target = target.to_string();
6193 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
6194 });
6195 if let Some(replacement) = replacement {
6196 *target_value = Value::String(replacement);
6197 }
6198 }
6199 }
6200
6201 for value in object.values_mut() {
6202 rewrite_component_schema_references(value, aliases);
6203 }
6204 }
6205 _ => {}
6206 }
6207}
6208
6209fn rewrite_component_schema_reference(
6210 reference: &mut String,
6211 aliases: &BTreeMap<String, String>,
6212) -> bool {
6213 const PREFIX: &str = "#/components/schemas/";
6214 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
6215 return false;
6216 };
6217 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
6218
6219 for (source, replacement) in aliases {
6220 let encoded_source = source.replace('~', "~0").replace('/', "~1");
6221 if encoded_name == encoded_source {
6222 reference.replace_range(
6223 PREFIX.len()..PREFIX.len() + encoded_source.len(),
6224 replacement,
6225 );
6226 return true;
6227 }
6228 }
6229
6230 false
6231}