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 if let Some(non_null_types) = schema.non_null_schema_types() {
1762 let mut variants = Vec::with_capacity(non_null_types.len());
1763 for t in non_null_types {
1764 variants.push(self.build_typed_multi_union_variant(
1765 t,
1766 schema,
1767 schema_name,
1768 &mut dependencies,
1769 )?);
1770 }
1771 SchemaType::Union { variants }
1772 } else {
1773 self.analyze_single_typed_schema(
1774 schema,
1775 schema_name,
1776 details,
1777 &mut dependencies,
1778 )?
1779 }
1780 }
1781 Schema::AnyOf {
1782 any_of,
1783 discriminator,
1784 ..
1785 } => {
1786 self.analyze_anyof_union(
1788 any_of,
1789 discriminator.as_ref(),
1790 &mut dependencies,
1791 schema_name,
1792 )?
1793 }
1794 Schema::OneOf {
1795 one_of,
1796 discriminator,
1797 ..
1798 } => {
1799 self.analyze_oneof_union(
1801 one_of,
1802 discriminator.as_ref(),
1803 schema_name,
1804 &mut dependencies,
1805 )?
1806 }
1807 Schema::AllOf { all_of, .. } => {
1808 self.analyze_allof_composition(all_of, &mut dependencies)?
1810 }
1811 Schema::Untyped { .. } => {
1812 if let Some(inferred) = schema.inferred_type() {
1814 match inferred {
1815 OpenApiSchemaType::Object => {
1816 if self.should_use_dynamic_json(schema) {
1817 SchemaType::Primitive {
1818 rust_type: "serde_json::Value".to_string(),
1819 serde_with: None,
1820 }
1821 } else {
1822 self.analyze_object_schema(schema, &mut dependencies)?
1823 }
1824 }
1825 OpenApiSchemaType::String if details.is_string_enum() => {
1826 SchemaType::StringEnum {
1827 values: details.string_enum_values().unwrap_or_default(),
1828 }
1829 }
1830 _ => SchemaType::Primitive {
1831 rust_type: "serde_json::Value".to_string(),
1832 serde_with: None,
1833 },
1834 }
1835 } else {
1836 SchemaType::Primitive {
1837 rust_type: "serde_json::Value".to_string(),
1838 serde_with: None,
1839 }
1840 }
1841 }
1842 };
1843
1844 Ok(AnalyzedSchema {
1845 name: schema_name.to_string(),
1846 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1848 dependencies,
1849 nullable,
1850 description,
1851 default: details.default.clone(),
1852 })
1853 }
1854
1855 fn analyze_single_typed_schema(
1861 &mut self,
1862 schema: &Schema,
1863 schema_name: &str,
1864 details: &crate::openapi::SchemaDetails,
1865 dependencies: &mut HashSet<String>,
1866 ) -> Result<SchemaType> {
1867 let primary = schema
1868 .schema_type()
1869 .cloned()
1870 .unwrap_or(OpenApiSchemaType::Object);
1871 let format = details.format.as_deref();
1872 Ok(match primary {
1873 OpenApiSchemaType::String => {
1874 if let Some(values) = details.string_enum_values() {
1875 SchemaType::StringEnum { values }
1876 } else {
1877 SchemaType::Primitive {
1878 rust_type: self.type_mapper.string_format(format).rust_type,
1879 serde_with: None,
1880 }
1881 }
1882 }
1883 OpenApiSchemaType::Integer => SchemaType::Primitive {
1884 rust_type: self.type_mapper.integer_format(format).rust_type,
1885 serde_with: None,
1886 },
1887 OpenApiSchemaType::Number => SchemaType::Primitive {
1888 rust_type: self.type_mapper.number_format(format).rust_type,
1889 serde_with: None,
1890 },
1891 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1892 rust_type: self.type_mapper.boolean().rust_type,
1893 serde_with: None,
1894 },
1895 OpenApiSchemaType::Array => {
1896 self.analyze_array_schema(schema, schema_name, dependencies)?
1897 }
1898 OpenApiSchemaType::Object => {
1899 if self.should_use_dynamic_json(schema) {
1900 SchemaType::Primitive {
1901 rust_type: self.type_mapper.dynamic_json().rust_type,
1902 serde_with: None,
1903 }
1904 } else {
1905 self.analyze_object_schema(schema, dependencies)?
1906 }
1907 }
1908 _ => SchemaType::Primitive {
1909 rust_type: self.type_mapper.dynamic_json().rust_type,
1910 serde_with: None,
1911 },
1912 })
1913 }
1914
1915 fn analyze_object_schema(
1916 &mut self,
1917 schema: &Schema,
1918 dependencies: &mut HashSet<String>,
1919 ) -> Result<SchemaType> {
1920 let details = schema.details();
1921 let properties = &details.properties;
1922 let required = details
1923 .required
1924 .as_ref()
1925 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1926 .unwrap_or_default();
1927
1928 let mut property_info = BTreeMap::new();
1929
1930 if let Some(props) = properties {
1931 for (prop_name, prop_schema) in props {
1932 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1934 if self.should_use_dynamic_json(prop_schema) {
1936 SchemaType::Primitive {
1938 rust_type: "serde_json::Value".to_string(),
1939 serde_with: None,
1940 }
1941 } else if prop_schema.is_nullable_pattern()
1942 && let Some(non_null) = prop_schema.non_null_variant()
1943 {
1944 self.analyze_property_schema_with_context(
1952 non_null,
1953 Some(prop_name),
1954 dependencies,
1955 )?
1956 } else {
1957 let context_name = self
1960 .current_schema_name
1961 .clone()
1962 .unwrap_or_else(|| "Unknown".to_string());
1963
1964 let prop_pascal = self.to_pascal_case(prop_name);
1966 let mut union_type_name = format!("{context_name}{prop_pascal}");
1967
1968 if self.schemas.contains_key(&union_type_name)
1971 || self.resolved_cache.contains_key(&union_type_name)
1972 {
1973 let mut suffix = 2;
1974 loop {
1975 let candidate = format!("{union_type_name}Union{suffix}");
1976 if !self.schemas.contains_key(&candidate)
1977 && !self.resolved_cache.contains_key(&candidate)
1978 {
1979 union_type_name = candidate;
1980 break;
1981 }
1982 suffix += 1;
1983 if suffix > 1000 {
1984 break;
1985 }
1986 }
1987 }
1988
1989 let union_schema_type = self.analyze_anyof_union(
1991 any_of,
1992 prop_schema.discriminator(),
1993 dependencies,
1994 &union_type_name,
1995 )?;
1996
1997 self.resolved_cache.insert(
1999 union_type_name.clone(),
2000 AnalyzedSchema {
2001 name: union_type_name.clone(),
2002 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2003 schema_type: union_schema_type,
2004 dependencies: HashSet::new(),
2005 nullable: false,
2006 description: prop_schema.details().description.clone(),
2007 default: None,
2008 },
2009 );
2010
2011 dependencies.insert(union_type_name.clone());
2013 SchemaType::Reference {
2014 target: union_type_name,
2015 }
2016 }
2017 } else if let Schema::OneOf {
2018 one_of,
2019 discriminator,
2020 ..
2021 } = prop_schema
2022 {
2023 if prop_schema.is_nullable_pattern()
2030 && let Some(non_null) = prop_schema.non_null_variant()
2031 {
2032 let unwrapped = self.analyze_property_schema_with_context(
2033 non_null,
2034 Some(prop_name),
2035 dependencies,
2036 )?;
2037 let prop_details = prop_schema.details();
2038 let prop_nullable = true;
2039 let prop_description = prop_details.description.clone();
2040 let prop_default = prop_details.default.clone();
2041 property_info.insert(
2042 prop_name.clone(),
2043 PropertyInfo {
2044 schema_type: unwrapped,
2045 nullable: prop_nullable,
2046 description: prop_description,
2047 default: prop_default,
2048 serde_attrs: Vec::new(),
2049 constraints: PropertyConstraints::from_schema_details(prop_details),
2050 },
2051 );
2052 continue;
2053 }
2054
2055 let context_name = self
2057 .current_schema_name
2058 .clone()
2059 .unwrap_or_else(|| "Unknown".to_string());
2060 let prop_pascal = self.to_pascal_case(prop_name);
2061 let mut union_type_name = format!("{context_name}{prop_pascal}");
2062 if self.schemas.contains_key(&union_type_name)
2064 || self.resolved_cache.contains_key(&union_type_name)
2065 {
2066 let mut suffix = 2;
2067 loop {
2068 let candidate = format!("{union_type_name}Union{suffix}");
2069 if !self.schemas.contains_key(&candidate)
2070 && !self.resolved_cache.contains_key(&candidate)
2071 {
2072 union_type_name = candidate;
2073 break;
2074 }
2075 suffix += 1;
2076 if suffix > 1000 {
2077 break;
2078 }
2079 }
2080 }
2081
2082 let union_schema_type = self.analyze_oneof_union(
2084 one_of,
2085 discriminator.as_ref(),
2086 &union_type_name,
2087 dependencies,
2088 )?;
2089
2090 self.resolved_cache.insert(
2092 union_type_name.clone(),
2093 AnalyzedSchema {
2094 name: union_type_name.clone(),
2095 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2096 schema_type: union_schema_type,
2097 dependencies: HashSet::new(),
2098 nullable: false,
2099 description: prop_schema.details().description.clone(),
2100 default: None,
2101 },
2102 );
2103
2104 dependencies.insert(union_type_name.clone());
2106 SchemaType::Reference {
2107 target: union_type_name,
2108 }
2109 } else {
2110 self.analyze_property_schema_with_context(
2112 prop_schema,
2113 Some(prop_name),
2114 dependencies,
2115 )?
2116 };
2117
2118 let prop_details = prop_schema.details();
2119 let prop_nullable = prop_schema.is_nullable_any();
2121 let prop_description = prop_details.description.clone();
2122 let prop_default = prop_details.default.clone();
2123
2124 property_info.insert(
2125 prop_name.clone(),
2126 PropertyInfo {
2127 schema_type: prop_type,
2128 nullable: prop_nullable,
2129 description: prop_description,
2130 default: prop_default,
2131 serde_attrs: Vec::new(),
2132 constraints: PropertyConstraints::from_schema_details(prop_details),
2133 },
2134 );
2135 }
2136 }
2137
2138 let typed_enabled = self
2146 .type_mapper
2147 .config()
2148 .shape
2149 .as_ref()
2150 .and_then(|s| s.additional_properties_typed)
2151 .unwrap_or(true);
2152
2153 let additional_properties = match &details.additional_properties {
2154 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
2155 ObjectAdditionalProperties::Untyped
2156 }
2157 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
2158 ObjectAdditionalProperties::Forbidden
2159 }
2160 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
2161 let analyzed =
2162 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
2163 ObjectAdditionalProperties::Typed {
2164 value_type: Box::new(analyzed),
2165 }
2166 }
2167 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
2168 ObjectAdditionalProperties::Untyped
2170 }
2171 None => ObjectAdditionalProperties::Forbidden,
2172 };
2173
2174 Ok(SchemaType::Object {
2175 properties: property_info,
2176 required,
2177 additional_properties,
2178 })
2179 }
2180
2181 fn build_typed_multi_union_variant(
2191 &mut self,
2192 member_type: OpenApiSchemaType,
2193 schema: &Schema,
2194 union_type_name: &str,
2195 dependencies: &mut HashSet<String>,
2196 ) -> Result<SchemaRef> {
2197 match member_type {
2198 OpenApiSchemaType::Array => {
2199 let array_type_name = format!("{union_type_name}Array");
2200 let array_type =
2201 self.analyze_array_schema(schema, &array_type_name, dependencies)?;
2202 self.resolved_cache.insert(
2203 array_type_name.clone(),
2204 AnalyzedSchema {
2205 name: array_type_name.clone(),
2206 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2207 schema_type: array_type,
2208 dependencies: HashSet::new(),
2209 nullable: false,
2210 description: Some("Array variant in union".to_string()),
2211 default: None,
2212 },
2213 );
2214 dependencies.insert(array_type_name.clone());
2215 Ok(SchemaRef {
2216 target: array_type_name,
2217 nullable: false,
2218 })
2219 }
2220 OpenApiSchemaType::Object => {
2221 let object_type_name = format!("{union_type_name}Object");
2222 let object_type = self.analyze_object_schema(schema, dependencies)?;
2223 self.resolved_cache.insert(
2224 object_type_name.clone(),
2225 AnalyzedSchema {
2226 name: object_type_name.clone(),
2227 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2228 schema_type: object_type,
2229 dependencies: dependencies.clone(),
2230 nullable: false,
2231 description: schema.details().description.clone(),
2232 default: None,
2233 },
2234 );
2235 dependencies.insert(object_type_name.clone());
2236 Ok(SchemaRef {
2237 target: object_type_name,
2238 nullable: false,
2239 })
2240 }
2241 _ => Ok(SchemaRef {
2242 target: self
2243 .type_mapper
2244 .map(member_type, schema.details())
2245 .rust_type,
2246 nullable: false,
2247 }),
2248 }
2249 }
2250
2251 fn analyze_property_schema_with_context(
2252 &mut self,
2253 schema: &Schema,
2254 property_name: Option<&str>,
2255 dependencies: &mut HashSet<String>,
2256 ) -> Result<SchemaType> {
2257 if let Some(ref_str) = self.get_any_reference(schema) {
2258 let target_opt = if ref_str == "#" {
2259 Some(
2260 self.find_recursive_anchor_schema()
2261 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2262 )
2263 } else {
2264 self.extract_schema_name(ref_str).map(|s| s.to_string())
2265 };
2266 match target_opt {
2267 Some(target) => {
2268 dependencies.insert(target.clone());
2269 return Ok(SchemaType::Reference { target });
2270 }
2271 None => {
2272 eprintln!(
2273 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2274 ref_str
2275 );
2276 return Ok(SchemaType::Primitive {
2277 rust_type: "serde_json::Value".to_string(),
2278 serde_with: None,
2279 });
2280 }
2281 }
2282 }
2283
2284 if let Some(non_null_types) = schema.non_null_schema_types() {
2288 let context_name = self
2289 .current_schema_name
2290 .clone()
2291 .unwrap_or_else(|| "Unknown".to_string());
2292 let prop_pascal = property_name
2293 .map(|name| self.to_pascal_case(name))
2294 .unwrap_or_default();
2295 let mut union_type_name = format!("{context_name}{prop_pascal}");
2296 if self.schemas.contains_key(&union_type_name)
2297 || self.resolved_cache.contains_key(&union_type_name)
2298 {
2299 let mut suffix = 2;
2300 loop {
2301 let candidate = format!("{union_type_name}Union{suffix}");
2302 if !self.schemas.contains_key(&candidate)
2303 && !self.resolved_cache.contains_key(&candidate)
2304 {
2305 union_type_name = candidate;
2306 break;
2307 }
2308 suffix += 1;
2309 if suffix > 1000 {
2310 break;
2311 }
2312 }
2313 }
2314
2315 let details = schema.details();
2316 let mut variants = Vec::with_capacity(non_null_types.len());
2317 for t in non_null_types {
2318 variants.push(self.build_typed_multi_union_variant(
2319 t,
2320 schema,
2321 &union_type_name,
2322 dependencies,
2323 )?);
2324 }
2325
2326 self.resolved_cache.insert(
2327 union_type_name.clone(),
2328 AnalyzedSchema {
2329 name: union_type_name.clone(),
2330 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2331 schema_type: SchemaType::Union { variants },
2332 dependencies: HashSet::new(),
2333 nullable: false,
2334 description: details.description.clone(),
2335 default: None,
2336 },
2337 );
2338
2339 dependencies.insert(union_type_name.clone());
2340 return Ok(SchemaType::Reference {
2341 target: union_type_name,
2342 });
2343 }
2344
2345 if let Some(schema_type) = schema.schema_type() {
2346 match schema_type {
2347 OpenApiSchemaType::String => {
2348 if let Some(enum_values) = schema.details().string_enum_values() {
2350 let context_name = self
2353 .current_schema_name
2354 .clone()
2355 .unwrap_or_else(|| "Unknown".to_string());
2356
2357 let primary_name = if let Some(prop_name) = property_name {
2359 let prop_pascal = self.to_pascal_case(prop_name);
2361 format!("{context_name}{prop_pascal}")
2362 } else {
2363 let suffix = if !enum_values.is_empty() {
2366 let first_value = self.to_pascal_case(&enum_values[0]);
2367 format!("{first_value}Enum")
2368 } else {
2369 "StringEnum".to_string()
2370 };
2371 format!("{context_name}{suffix}")
2372 };
2373
2374 return Ok(self.hoist_inline_string_enum(
2375 schema,
2376 enum_values,
2377 primary_name,
2378 dependencies,
2379 ));
2380 } else {
2381 let mapped = self
2387 .type_mapper
2388 .string_format(schema.details().format.as_deref());
2389 return Ok(SchemaType::Primitive {
2390 rust_type: mapped.rust_type,
2391 serde_with: mapped.serde_with,
2392 });
2393 }
2394 }
2395 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2396 let details = schema.details();
2397 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2398 return Ok(SchemaType::Primitive {
2399 rust_type,
2400 serde_with: None,
2401 });
2402 }
2403 OpenApiSchemaType::Boolean => {
2404 return Ok(SchemaType::Primitive {
2405 rust_type: "bool".to_string(),
2406 serde_with: None,
2407 });
2408 }
2409 OpenApiSchemaType::Array => {
2410 let context_name = if let Some(prop_name) = property_name {
2412 let prop_pascal = self.to_pascal_case(prop_name);
2414 format!(
2415 "{}{}",
2416 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2417 prop_pascal
2418 )
2419 } else {
2420 "ArrayItem".to_string()
2422 };
2423 return self.analyze_array_schema(schema, &context_name, dependencies);
2424 }
2425 OpenApiSchemaType::Object => {
2426 if self.should_use_dynamic_json(schema) {
2428 return Ok(SchemaType::Primitive {
2429 rust_type: "serde_json::Value".to_string(),
2430 serde_with: None,
2431 });
2432 }
2433 let object_type_name = if let Some(prop_name) = property_name {
2435 let prop_pascal = self.to_pascal_case(prop_name);
2437 format!(
2438 "{}{}",
2439 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2440 prop_pascal
2441 )
2442 } else {
2443 format!(
2445 "{}Object",
2446 self.current_schema_name.as_deref().unwrap_or("Unknown")
2447 )
2448 };
2449
2450 let object_type = self.analyze_object_schema(schema, dependencies)?;
2452
2453 let inline_schema = AnalyzedSchema {
2455 name: object_type_name.clone(),
2456 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2457 schema_type: object_type,
2458 dependencies: dependencies.clone(),
2459 nullable: false,
2460 description: schema.details().description.clone(),
2461 default: None,
2462 };
2463
2464 self.resolved_cache
2466 .insert(object_type_name.clone(), inline_schema);
2467 dependencies.insert(object_type_name.clone());
2468
2469 return Ok(SchemaType::Reference {
2471 target: object_type_name,
2472 });
2473 }
2474 _ => {
2475 return Ok(SchemaType::Primitive {
2476 rust_type: "serde_json::Value".to_string(),
2477 serde_with: None,
2478 });
2479 }
2480 }
2481 }
2482
2483 if schema.is_nullable_pattern() {
2485 if let Some(non_null) = schema.non_null_variant() {
2486 return self.analyze_property_schema_with_context(
2487 non_null,
2488 property_name,
2489 dependencies,
2490 );
2491 }
2492 }
2493
2494 if self.should_use_dynamic_json(schema) {
2496 return Ok(SchemaType::Primitive {
2497 rust_type: "serde_json::Value".to_string(),
2498 serde_with: None,
2499 });
2500 }
2501
2502 if let Schema::AllOf { all_of, .. } = schema {
2504 return self.analyze_allof_composition(all_of, dependencies);
2505 }
2506
2507 if let Some(variants) = schema.union_variants() {
2509 match variants.len().cmp(&1) {
2510 std::cmp::Ordering::Equal => {
2511 return self.analyze_property_schema_with_context(
2513 &variants[0],
2514 property_name,
2515 dependencies,
2516 );
2517 }
2518 std::cmp::Ordering::Greater => {
2519 let union_name = if let Some(prop_name) = property_name {
2522 let prop_pascal = self.to_pascal_case(prop_name);
2524 format!(
2525 "{}{}",
2526 self.current_schema_name.as_deref().unwrap_or(""),
2527 prop_pascal
2528 )
2529 } else {
2530 "UnionType".to_string()
2531 };
2532
2533 if let Schema::OneOf {
2535 one_of,
2536 discriminator,
2537 ..
2538 } = schema
2539 {
2540 let oneof_result = self.analyze_oneof_union(
2542 one_of,
2543 discriminator.as_ref(),
2544 &union_name,
2545 dependencies,
2546 )?;
2547
2548 if let SchemaType::Union {
2550 variants: _union_variants,
2551 } = &oneof_result
2552 {
2553 self.resolved_cache.insert(
2555 union_name.clone(),
2556 AnalyzedSchema {
2557 name: union_name.clone(),
2558 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2559 schema_type: oneof_result.clone(),
2560 dependencies: dependencies.clone(),
2561 nullable: false,
2562 description: schema.details().description.clone(),
2563 default: None,
2564 },
2565 );
2566
2567 dependencies.insert(union_name.clone());
2569 return Ok(SchemaType::Reference { target: union_name });
2570 }
2571
2572 return Ok(oneof_result);
2573 } else if let Schema::AnyOf {
2574 any_of,
2575 discriminator,
2576 ..
2577 } = schema
2578 {
2579 let union_analysis = self.analyze_anyof_union(
2581 any_of,
2582 discriminator.as_ref(),
2583 dependencies,
2584 &union_name,
2585 )?;
2586 return Ok(union_analysis);
2587 } else {
2588 let mut union_variants = Vec::new();
2591 for variant in variants {
2592 if let Some(ref_str) = variant.reference() {
2593 if let Some(target) = self.extract_schema_name(ref_str) {
2594 dependencies.insert(target.to_string());
2595 union_variants.push(SchemaRef {
2596 target: target.to_string(),
2597 nullable: false,
2598 });
2599 }
2600 }
2601 }
2602 return Ok(SchemaType::Union {
2603 variants: union_variants,
2604 });
2605 }
2606 }
2607 std::cmp::Ordering::Less => {}
2608 }
2609 }
2610
2611 if let Some(inferred_type) = schema.inferred_type() {
2613 match inferred_type {
2614 OpenApiSchemaType::Object => {
2615 if self.should_use_dynamic_json(schema) {
2617 return Ok(SchemaType::Primitive {
2618 rust_type: "serde_json::Value".to_string(),
2619 serde_with: None,
2620 });
2621 }
2622 return self.analyze_object_schema(schema, dependencies);
2623 }
2624 OpenApiSchemaType::Array => {
2625 let context_name = if let Some(prop_name) = property_name {
2626 let prop_pascal = self.to_pascal_case(prop_name);
2628 format!(
2629 "{}{}",
2630 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2631 prop_pascal
2632 )
2633 } else {
2634 "ArrayItem".to_string()
2636 };
2637 return self.analyze_array_schema(schema, &context_name, dependencies);
2638 }
2639 OpenApiSchemaType::String => {
2640 if let Some(enum_values) = schema.details().string_enum_values() {
2641 return Ok(SchemaType::StringEnum {
2642 values: enum_values,
2643 });
2644 } else {
2645 return Ok(SchemaType::Primitive {
2646 rust_type: "String".to_string(),
2647 serde_with: None,
2648 });
2649 }
2650 }
2651 _ => {
2652 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2654 return Ok(SchemaType::Primitive {
2655 rust_type,
2656 serde_with: None,
2657 });
2658 }
2659 }
2660 }
2661
2662 Ok(SchemaType::Primitive {
2663 rust_type: "serde_json::Value".to_string(),
2664 serde_with: None,
2665 })
2666 }
2667
2668 fn analyze_allof_composition(
2669 &mut self,
2670 all_of_schemas: &[Schema],
2671 dependencies: &mut HashSet<String>,
2672 ) -> Result<SchemaType> {
2673 let referenced_targets = all_of_schemas
2678 .iter()
2679 .filter_map(|schema| schema.reference())
2680 .filter_map(|reference| self.extract_schema_name(reference))
2681 .collect::<Vec<_>>();
2682 let only_reference_and_annotations = all_of_schemas.iter().all(|schema| {
2683 if schema.reference().is_some() {
2684 return true;
2685 }
2686 serde_json::to_value(schema)
2687 .ok()
2688 .and_then(|value| value.as_object().cloned())
2689 .is_some_and(|object| {
2690 object.keys().all(|key| {
2691 matches!(
2692 key.as_str(),
2693 "title"
2694 | "description"
2695 | "deprecated"
2696 | "readOnly"
2697 | "writeOnly"
2698 | "examples"
2699 | "example"
2700 | "externalDocs"
2701 | "xml"
2702 | "$comment"
2703 ) || key.starts_with("x-")
2704 })
2705 })
2706 });
2707 if referenced_targets.len() == 1 && only_reference_and_annotations {
2708 let target = referenced_targets[0];
2709 dependencies.insert(target.to_string());
2710 return Ok(SchemaType::Reference {
2711 target: target.to_string(),
2712 });
2713 }
2714
2715 let mut merged_properties = BTreeMap::new();
2717 let mut merged_required = HashSet::new();
2718 let mut descriptions = Vec::new();
2719
2720 let current_context = self.current_schema_name.clone();
2722
2723 for schema in all_of_schemas {
2724 match schema {
2725 Schema::Reference { reference, .. } => {
2726 if let Some(target) = self.extract_schema_name(reference) {
2728 dependencies.insert(target.to_string());
2729
2730 let analyzed_ref = self.analyze_schema(target)?;
2732
2733 match &analyzed_ref.schema_type {
2735 SchemaType::Object {
2736 properties,
2737 required,
2738 ..
2739 } => {
2740 for (prop_name, prop_info) in properties {
2742 merged_properties.insert(prop_name.clone(), prop_info.clone());
2743 }
2744 for req in required {
2746 merged_required.insert(req.clone());
2747 }
2748 }
2749 _ => {
2750 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2752 self.merge_schema_into_properties(
2753 &ref_schema,
2754 &mut merged_properties,
2755 &mut merged_required,
2756 dependencies,
2757 )?;
2758 }
2759 }
2760 }
2761 }
2762 }
2763 Schema::Typed {
2764 schema_type: OpenApiSchemaType::Object,
2765 ..
2766 }
2767 | Schema::Untyped { .. } => {
2768 let saved_context = self.current_schema_name.clone();
2770 self.current_schema_name = current_context.clone();
2771
2772 self.merge_schema_into_properties(
2774 schema,
2775 &mut merged_properties,
2776 &mut merged_required,
2777 dependencies,
2778 )?;
2779
2780 self.current_schema_name = saved_context;
2782 }
2783 _ => {
2784 self.merge_schema_into_properties(
2787 schema,
2788 &mut merged_properties,
2789 &mut merged_required,
2790 dependencies,
2791 )?;
2792 }
2793 }
2794
2795 if let Some(desc) = &schema.details().description {
2797 descriptions.push(desc.clone());
2798 }
2799 }
2800
2801 if !merged_properties.is_empty() {
2803 Ok(SchemaType::Object {
2804 properties: merged_properties,
2805 required: merged_required,
2806 additional_properties: ObjectAdditionalProperties::Forbidden,
2807 })
2808 } else {
2809 Ok(SchemaType::Composition {
2811 schemas: all_of_schemas
2812 .iter()
2813 .filter_map(|s| {
2814 if let Some(ref_str) = s.reference() {
2815 if let Some(target) = self.extract_schema_name(ref_str) {
2816 dependencies.insert(target.to_string());
2817 Some(SchemaRef {
2818 target: target.to_string(),
2819 nullable: false,
2820 })
2821 } else {
2822 None
2823 }
2824 } else {
2825 None
2826 }
2827 })
2828 .collect(),
2829 })
2830 }
2831 }
2832
2833 fn merge_schema_into_properties(
2834 &mut self,
2835 schema: &Schema,
2836 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2837 merged_required: &mut HashSet<String>,
2838 dependencies: &mut HashSet<String>,
2839 ) -> Result<()> {
2840 let details = schema.details();
2841
2842 if let Some(properties) = &details.properties {
2844 for (prop_name, prop_schema) in properties {
2845 let prop_type = self.analyze_property_schema_with_context(
2846 prop_schema,
2847 Some(prop_name),
2848 dependencies,
2849 )?;
2850 let prop_details = prop_schema.details();
2851
2852 let nullable = prop_schema.is_nullable_any();
2859 merged_properties.insert(
2860 prop_name.clone(),
2861 PropertyInfo {
2862 schema_type: prop_type,
2863 nullable,
2864 description: prop_details.description.clone(),
2865 default: prop_details.default.clone(),
2866 serde_attrs: Vec::new(),
2867 constraints: PropertyConstraints::from_schema_details(prop_details),
2868 },
2869 );
2870 }
2871 }
2872
2873 if let Some(required) = &details.required {
2875 for field in required {
2876 merged_required.insert(field.clone());
2877 }
2878 }
2879
2880 Ok(())
2881 }
2882
2883 fn analyze_oneof_union(
2884 &mut self,
2885 one_of_schemas: &[Schema],
2886 discriminator: Option<&crate::openapi::Discriminator>,
2887 parent_name: &str,
2888 dependencies: &mut HashSet<String>,
2889 ) -> Result<SchemaType> {
2890 if one_of_schemas.len() == 2 {
2893 let null_count = one_of_schemas
2894 .iter()
2895 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2896 .count();
2897 if null_count == 1 {
2898 if let Some(non_null) = one_of_schemas
2899 .iter()
2900 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2901 {
2902 return self
2903 .analyze_schema_value(non_null, parent_name)
2904 .map(|a| a.schema_type);
2905 }
2906 }
2907 }
2908
2909 if discriminator.is_none() {
2911 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2913 }
2914
2915 if one_of_schemas
2921 .iter()
2922 .any(|s| !self.branch_resolves_to_object(s))
2923 {
2924 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2925 }
2926
2927 let discriminator_field = discriminator
2929 .ok_or_else(|| {
2930 GeneratorError::InvalidDiscriminator(
2931 "expected discriminator after guard check".to_string(),
2932 )
2933 })?
2934 .property_name
2935 .clone();
2936
2937 let mut variants = Vec::new();
2938 let mut used_variant_names = std::collections::HashSet::new();
2939
2940 for variant_schema in one_of_schemas {
2941 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2943 Some((ref_str, false))
2944 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2945 Some((recursive_ref, true))
2946 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2947 if all_of.len() == 1 {
2949 if let Some(ref_str) = all_of[0].reference() {
2950 Some((ref_str, false))
2951 } else {
2952 all_of[0]
2953 .recursive_reference()
2954 .map(|recursive_ref| (recursive_ref, true))
2955 }
2956 } else {
2957 None
2958 }
2959 } else {
2960 None
2961 };
2962
2963 if let Some((ref_str, is_recursive)) = ref_info {
2964 let schema_name = if is_recursive && ref_str == "#" {
2965 self.find_recursive_anchor_schema()
2967 .or_else(|| self.current_schema_name.clone())
2968 .unwrap_or_else(|| "CompoundFilter".to_string())
2969 } else {
2970 self.extract_schema_name(ref_str)
2971 .map(|s| s.to_string())
2972 .unwrap_or_else(|| "UnknownRef".to_string())
2973 };
2974
2975 if !schema_name.is_empty() {
2976 dependencies.insert(schema_name.clone());
2977
2978 let discriminator_value = if let Some(disc) = discriminator {
2983 if let Some(mappings) = &disc.mapping {
2984 mappings
2987 .iter()
2988 .find(|(_, target_ref)| {
2989 target_ref.as_str() == ref_str
2991 || self
2992 .extract_schema_name(target_ref)
2993 .map(|s| s.to_string())
2994 == Some(schema_name.clone())
2995 })
2996 .map(|(key, _)| key.clone())
2997 .unwrap_or_else(|| {
2998 self.fallback_discriminator_value_for_field(
2999 &schema_name,
3000 &discriminator_field,
3001 )
3002 })
3003 } else {
3004 self.fallback_discriminator_value_for_field(
3005 &schema_name,
3006 &discriminator_field,
3007 )
3008 }
3009 } else {
3010 self.fallback_discriminator_value_for_field(
3011 &schema_name,
3012 &discriminator_field,
3013 )
3014 };
3015
3016 let base_name = self.to_rust_variant_name(&schema_name);
3018 let rust_name =
3019 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
3020
3021 let final_discriminator_value = discriminator_value;
3023
3024 variants.push(UnionVariant {
3025 rust_name,
3026 type_name: schema_name,
3027 discriminator_value: final_discriminator_value,
3028 schema_ref: ref_str.to_string(),
3029 });
3030 }
3031 } else {
3032 let variant_index = variants.len();
3034 let inline_type_name =
3035 self.generate_inline_type_name(variant_schema, variant_index);
3036
3037 let discriminator_value = if let Some(disc) = discriminator {
3039 if let Some(mappings) = &disc.mapping {
3040 mappings
3042 .iter()
3043 .find(|(_, target_ref)| {
3044 target_ref.contains(&format!("variant_{variant_index}"))
3045 })
3046 .map(|(key, _)| key.clone())
3047 .unwrap_or_else(|| {
3048 self.extract_inline_discriminator_value(
3049 variant_schema,
3050 &discriminator_field,
3051 variant_index,
3052 )
3053 })
3054 } else {
3055 self.extract_inline_discriminator_value(
3056 variant_schema,
3057 &discriminator_field,
3058 variant_index,
3059 )
3060 }
3061 } else {
3062 self.extract_inline_discriminator_value(
3063 variant_schema,
3064 &discriminator_field,
3065 variant_index,
3066 )
3067 };
3068
3069 let base_name = if discriminator_value.starts_with("variant_") {
3071 format!("Variant{variant_index}")
3072 } else {
3073 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
3075 self.to_rust_variant_name(&clean_name)
3076 };
3077 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
3078
3079 let final_discriminator_value = discriminator_value;
3081
3082 variants.push(UnionVariant {
3083 rust_name,
3084 type_name: inline_type_name.clone(),
3085 discriminator_value: final_discriminator_value,
3086 schema_ref: format!("inline_{variant_index}"),
3087 });
3088
3089 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3091 }
3092 }
3093
3094 if variants.is_empty() {
3095 let mut union_variants = Vec::new();
3098
3099 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
3100 if let Some(ref_str) = variant_schema.reference() {
3102 if let Some(schema_name) = self.extract_schema_name(ref_str) {
3103 dependencies.insert(schema_name.to_string());
3104 union_variants.push(SchemaRef {
3105 target: schema_name.to_string(),
3106 nullable: false,
3107 });
3108 }
3109 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3110 let schema_name = if recursive_ref == "#" {
3111 self.find_recursive_anchor_schema()
3113 .or_else(|| self.current_schema_name.clone())
3114 .unwrap_or_else(|| "CompoundFilter".to_string())
3115 } else {
3116 self.extract_schema_name(recursive_ref)
3117 .map(|s| s.to_string())
3118 .unwrap_or_else(|| "RecursiveType".to_string())
3119 };
3120 dependencies.insert(schema_name.clone());
3121 union_variants.push(SchemaRef {
3122 target: schema_name,
3123 nullable: false,
3124 });
3125 } else {
3126 let inline_name = self.generate_context_aware_name(
3128 parent_name,
3129 "InlineVariant",
3130 variant_index,
3131 Some(variant_schema),
3132 );
3133 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
3134 let variant_type = analyzed.schema_type;
3135
3136 for dep in &analyzed.dependencies {
3138 dependencies.insert(dep.clone());
3139 }
3140
3141 match &variant_type {
3142 SchemaType::Primitive { rust_type, .. } => {
3144 union_variants.push(SchemaRef {
3145 target: rust_type.clone(),
3146 nullable: false,
3147 });
3148 }
3149 SchemaType::Array { item_type } => {
3151 match item_type.as_ref() {
3152 SchemaType::Primitive { rust_type, .. } => {
3153 let type_name = format!("Vec<{rust_type}>");
3154 union_variants.push(SchemaRef {
3155 target: type_name,
3156 nullable: false,
3157 });
3158 }
3159 SchemaType::Reference { target } => {
3160 let type_name = format!("Vec<{target}>");
3161 union_variants.push(SchemaRef {
3162 target: type_name,
3163 nullable: false,
3164 });
3165 }
3166 _ => {
3167 let inline_type_name = self.generate_context_aware_name(
3169 parent_name,
3170 "Variant",
3171 variant_index,
3172 None,
3173 );
3174 self.add_inline_schema(
3175 &inline_type_name,
3176 variant_schema,
3177 dependencies,
3178 )?;
3179 union_variants.push(SchemaRef {
3180 target: inline_type_name,
3181 nullable: false,
3182 });
3183 }
3184 }
3185 }
3186 SchemaType::Reference { target } => {
3188 union_variants.push(SchemaRef {
3189 target: target.clone(),
3190 nullable: false,
3191 });
3192 }
3193 _ => {
3195 let inline_type_name =
3196 format!("{}Variant{}", parent_name, variant_index + 1);
3197 self.add_inline_schema(
3198 &inline_type_name,
3199 variant_schema,
3200 dependencies,
3201 )?;
3202 union_variants.push(SchemaRef {
3203 target: inline_type_name,
3204 nullable: false,
3205 });
3206 }
3207 }
3208 }
3209 }
3210
3211 if !union_variants.is_empty() {
3212 return Ok(SchemaType::Union {
3213 variants: union_variants,
3214 });
3215 }
3216
3217 return Ok(SchemaType::Primitive {
3219 rust_type: "serde_json::Value".to_string(),
3220 serde_with: None,
3221 });
3222 }
3223
3224 Ok(SchemaType::DiscriminatedUnion {
3225 discriminator_field,
3226 variants,
3227 })
3228 }
3229
3230 fn analyze_untagged_oneof_union(
3231 &mut self,
3232 one_of_schemas: &[Schema],
3233 parent_name: &str,
3234 dependencies: &mut HashSet<String>,
3235 ) -> Result<SchemaType> {
3236 let filtered: Vec<&Schema> = one_of_schemas
3240 .iter()
3241 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3242 .collect();
3243
3244 if filtered.len() == 1 {
3246 return self
3247 .analyze_schema_value(filtered[0], parent_name)
3248 .map(|a| a.schema_type);
3249 }
3250
3251 let mut union_variants = Vec::new();
3252
3253 for (variant_index, variant_schema) in filtered.iter().copied().enumerate() {
3254 if let Some(ref_str) = variant_schema.reference() {
3256 if let Some(schema_name) = self.extract_schema_name(ref_str) {
3257 dependencies.insert(schema_name.to_string());
3258 union_variants.push(SchemaRef {
3259 target: schema_name.to_string(),
3260 nullable: false,
3261 });
3262 }
3263 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
3264 let schema_name = if recursive_ref == "#" {
3265 self.find_recursive_anchor_schema()
3267 .or_else(|| self.current_schema_name.clone())
3268 .unwrap_or_else(|| "CompoundFilter".to_string())
3269 } else {
3270 self.extract_schema_name(recursive_ref)
3271 .map(|s| s.to_string())
3272 .unwrap_or_else(|| "RecursiveType".to_string())
3273 };
3274 dependencies.insert(schema_name.clone());
3275 union_variants.push(SchemaRef {
3276 target: schema_name,
3277 nullable: false,
3278 });
3279 } else {
3280 let inline_name = self.generate_context_aware_name(
3282 parent_name,
3283 "InlineVariant",
3284 variant_index,
3285 Some(variant_schema),
3286 );
3287 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
3288 let variant_type = analyzed.schema_type;
3289
3290 for dep in &analyzed.dependencies {
3292 dependencies.insert(dep.clone());
3293 }
3294
3295 match &variant_type {
3296 SchemaType::Primitive { rust_type, .. } => {
3298 union_variants.push(SchemaRef {
3299 target: rust_type.clone(),
3300 nullable: false,
3301 });
3302 }
3303 SchemaType::Array { item_type } => {
3305 match item_type.as_ref() {
3306 SchemaType::Primitive { rust_type, .. } => {
3307 let type_name = format!("Vec<{rust_type}>");
3308 union_variants.push(SchemaRef {
3309 target: type_name,
3310 nullable: false,
3311 });
3312 }
3313 SchemaType::Reference { target } => {
3314 let type_name = format!("Vec<{target}>");
3315 union_variants.push(SchemaRef {
3316 target: type_name,
3317 nullable: false,
3318 });
3319 }
3320 SchemaType::Array {
3322 item_type: inner_item_type,
3323 } => {
3324 match inner_item_type.as_ref() {
3325 SchemaType::Primitive { rust_type, .. } => {
3326 let type_name = format!("Vec<Vec<{rust_type}>>");
3327 union_variants.push(SchemaRef {
3328 target: type_name,
3329 nullable: false,
3330 });
3331 }
3332 SchemaType::Reference { target } => {
3333 let type_name = format!("Vec<Vec<{target}>>");
3334 union_variants.push(SchemaRef {
3335 target: type_name,
3336 nullable: false,
3337 });
3338 }
3339 _ => {
3340 let inline_type_name = self.generate_context_aware_name(
3342 parent_name,
3343 "Variant",
3344 variant_index,
3345 None,
3346 );
3347 self.add_inline_schema(
3348 &inline_type_name,
3349 variant_schema,
3350 dependencies,
3351 )?;
3352 union_variants.push(SchemaRef {
3353 target: inline_type_name,
3354 nullable: false,
3355 });
3356 }
3357 }
3358 }
3359 _ => {
3360 let inline_type_name = self.generate_context_aware_name(
3362 parent_name,
3363 "Variant",
3364 variant_index,
3365 None,
3366 );
3367 self.add_inline_schema(
3368 &inline_type_name,
3369 variant_schema,
3370 dependencies,
3371 )?;
3372 union_variants.push(SchemaRef {
3373 target: inline_type_name,
3374 nullable: false,
3375 });
3376 }
3377 }
3378 }
3379 SchemaType::Reference { target } => {
3381 union_variants.push(SchemaRef {
3382 target: target.clone(),
3383 nullable: false,
3384 });
3385 }
3386 _ => {
3388 let inline_type_name = self.generate_context_aware_name(
3389 parent_name,
3390 "Variant",
3391 variant_index,
3392 None,
3393 );
3394 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3395 union_variants.push(SchemaRef {
3396 target: inline_type_name,
3397 nullable: false,
3398 });
3399 }
3400 }
3401 }
3402 }
3403
3404 if !union_variants.is_empty() {
3405 return Ok(SchemaType::Union {
3406 variants: union_variants,
3407 });
3408 }
3409
3410 Ok(SchemaType::Primitive {
3412 rust_type: "serde_json::Value".to_string(),
3413 serde_with: None,
3414 })
3415 }
3416
3417 fn add_inline_schema(
3418 &mut self,
3419 type_name: &str,
3420 schema: &Schema,
3421 dependencies: &mut HashSet<String>,
3422 ) -> Result<()> {
3423 if let Some(schema_type) = schema.schema_type() {
3425 match schema_type {
3426 OpenApiSchemaType::String
3427 | OpenApiSchemaType::Integer
3428 | OpenApiSchemaType::Number
3429 | OpenApiSchemaType::Boolean => {
3430 let rust_type =
3431 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3432
3433 self.resolved_cache.insert(
3435 type_name.to_string(),
3436 AnalyzedSchema {
3437 name: type_name.to_string(),
3438 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3439 schema_type: SchemaType::Primitive {
3440 rust_type,
3441 serde_with: None,
3442 },
3443 dependencies: HashSet::new(),
3444 nullable: false,
3445 description: schema.details().description.clone(),
3446 default: None,
3447 },
3448 );
3449 return Ok(());
3450 }
3451 _ => {}
3452 }
3453 }
3454
3455 let previous_schema_name = self.current_schema_name.take();
3459 self.current_schema_name = Some(type_name.to_string());
3460 let analyzed = self.analyze_schema_value(schema, type_name)?;
3461 self.current_schema_name = previous_schema_name;
3462
3463 self.resolved_cache.insert(type_name.to_string(), analyzed);
3465
3466 if let Some(cached) = self.resolved_cache.get(type_name) {
3468 for dep in &cached.dependencies {
3469 dependencies.insert(dep.clone());
3470 }
3471 }
3472
3473 Ok(())
3474 }
3475
3476 fn extract_inline_discriminator_value(
3477 &self,
3478 schema: &Schema,
3479 discriminator_field: &str,
3480 variant_index: usize,
3481 ) -> String {
3482 if let Some(properties) = &schema.details().properties {
3484 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3485 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3487 if enum_values.len() == 1 {
3488 if let Some(value) = enum_values[0].as_str() {
3489 return value.to_string();
3490 }
3491 }
3492 }
3493 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3495 if let Some(value) = const_value.as_str() {
3496 return value.to_string();
3497 }
3498 }
3499 if let Some(const_value) = &discriminator_prop.details().const_value {
3501 if let Some(value) = const_value.as_str() {
3502 return value.to_string();
3503 }
3504 }
3505 }
3506 }
3507
3508 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3510 return inferred_name;
3511 }
3512
3513 format!("variant_{variant_index}")
3515 }
3516
3517 fn infer_variant_name_from_structure(
3518 &self,
3519 schema: &Schema,
3520 _variant_index: usize,
3521 ) -> Option<String> {
3522 let details = schema.details();
3523
3524 if let Some(properties) = &details.properties {
3526 if properties.contains_key("text") && properties.len() <= 3 {
3528 return Some("text".to_string());
3529 }
3530 if properties.contains_key("image") || properties.contains_key("source") {
3531 return Some("image".to_string());
3532 }
3533 if properties.contains_key("document") {
3534 return Some("document".to_string());
3535 }
3536 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3537 return Some("tool_result".to_string());
3538 }
3539 if properties.contains_key("content") && properties.contains_key("is_error") {
3540 return Some("tool_result".to_string());
3541 }
3542 if properties.contains_key("partial_json") {
3543 return Some("partial_json".to_string());
3544 }
3545
3546 let property_names: Vec<&String> = properties.keys().collect();
3548
3549 for prop_name in &property_names {
3551 if prop_name.contains("result") {
3552 return Some("result".to_string());
3553 }
3554 if prop_name.contains("error") {
3555 return Some("error".to_string());
3556 }
3557 if prop_name.contains("content") && property_names.len() <= 2 {
3558 return Some("content".to_string());
3559 }
3560 }
3561
3562 let significant_props = property_names
3564 .iter()
3565 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3566 .collect::<Vec<_>>();
3567
3568 if significant_props.len() == 1 {
3569 return Some((*significant_props[0]).clone());
3570 }
3571 }
3572
3573 if let Some(description) = &details.description {
3575 let desc_lower = description.to_lowercase();
3576 if desc_lower.contains("text") && desc_lower.len() < 100 {
3577 return Some("text".to_string());
3578 }
3579 if desc_lower.contains("image") {
3580 return Some("image".to_string());
3581 }
3582 if desc_lower.contains("document") {
3583 return Some("document".to_string());
3584 }
3585 if desc_lower.contains("tool") && desc_lower.contains("result") {
3586 return Some("tool_result".to_string());
3587 }
3588 }
3589
3590 None
3591 }
3592
3593 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3594 if discriminator.is_empty() {
3596 return "Variant".to_string();
3597 }
3598
3599 let mut result = String::new();
3600 let mut next_upper = true;
3601
3602 for c in discriminator.chars() {
3603 match c {
3604 'a'..='z' => {
3605 if next_upper {
3606 result.push(c.to_ascii_uppercase());
3607 next_upper = false;
3608 } else {
3609 result.push(c);
3610 }
3611 }
3612 'A'..='Z' => {
3613 result.push(c);
3614 next_upper = false;
3615 }
3616 '0'..='9' => {
3617 result.push(c);
3618 next_upper = false;
3619 }
3620 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3621 next_upper = true;
3623 }
3624 _ => {
3625 next_upper = true;
3627 }
3628 }
3629 }
3630
3631 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3633 result = format!("Variant{result}");
3634 }
3635
3636 result
3637 }
3638
3639 fn ensure_unique_variant_name(
3640 &self,
3641 base_name: String,
3642 used_names: &mut std::collections::HashSet<String>,
3643 ) -> String {
3644 let mut candidate = base_name.clone();
3645 let mut counter = 1;
3646
3647 while used_names.contains(&candidate) {
3648 counter += 1;
3649 candidate = format!("{base_name}{counter}");
3650 }
3651
3652 used_names.insert(candidate.clone());
3653 candidate
3654 }
3655
3656 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3657 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3659 return meaningful_name;
3660 }
3661
3662 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3664 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3665 }
3666
3667 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3668 let details = schema.details();
3669
3670 if let Some(description) = &details.description {
3672 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3673 return Some(name_from_desc);
3674 }
3675 }
3676
3677 if let Some(properties) = &details.properties {
3679 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3680 return Some(format!("{name_from_props}Block"));
3681 }
3682 }
3683
3684 None
3685 }
3686
3687 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3688 if description.len() > 100 || description.contains('\n') {
3690 return None;
3691 }
3692
3693 let words: Vec<&str> = description
3695 .split_whitespace()
3696 .take(2) .filter(|word| {
3698 let w = word.to_lowercase();
3699 word.len() > 2
3700 && ![
3701 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3702 ]
3703 .contains(&w.as_str())
3704 })
3705 .collect();
3706
3707 if words.is_empty() {
3708 return None;
3709 }
3710
3711 let combined = words.join("_");
3713 let pascal_name = self.discriminator_to_variant_name(&combined);
3714
3715 if !pascal_name.ends_with("Content")
3717 && !pascal_name.ends_with("Block")
3718 && !pascal_name.ends_with("Type")
3719 {
3720 Some(format!("{pascal_name}Content"))
3721 } else {
3722 Some(pascal_name)
3723 }
3724 }
3725
3726 fn extract_type_name_from_properties(
3727 &self,
3728 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3729 ) -> Option<String> {
3730 let significant_props: Vec<&String> = properties
3732 .keys()
3733 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3734 .collect();
3735
3736 if significant_props.is_empty() {
3737 return None;
3738 }
3739
3740 if significant_props.len() == 1 {
3742 let prop_name = significant_props[0];
3743 return Some(self.discriminator_to_variant_name(prop_name));
3744 }
3745
3746 let mut sorted_props = significant_props.clone();
3749 sorted_props.sort();
3750 if let Some(first_prop) = sorted_props.first() {
3751 return Some(self.discriminator_to_variant_name(first_prop));
3752 }
3753
3754 None
3755 }
3756
3757 fn openapi_type_to_rust_type(
3758 &self,
3759 openapi_type: OpenApiSchemaType,
3760 details: &crate::openapi::SchemaDetails,
3761 ) -> String {
3762 self.type_mapper.map(openapi_type, details).rust_type
3767 }
3768
3769 #[allow(dead_code)]
3770 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3771 self.fallback_discriminator_value_for_field(schema_name, "type")
3772 }
3773
3774 fn fallback_discriminator_value_for_field(
3775 &self,
3776 schema_name: &str,
3777 field_name: &str,
3778 ) -> String {
3779 if let Some(ref_schema) = self.schemas.get(schema_name) {
3781 if let Some(extracted) =
3782 self.extract_discriminator_value_for_field(ref_schema, field_name)
3783 {
3784 return extracted;
3785 }
3786 }
3787
3788 self.generate_discriminator_value_from_name(schema_name)
3790 }
3791
3792 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3793 let mut result = String::new();
3795 let mut chars = schema_name.chars().peekable();
3796 let mut first = true;
3797
3798 while let Some(c) = chars.next() {
3799 if c.is_uppercase()
3800 && !first
3801 && chars
3802 .peek()
3803 .map(|&next| next.is_lowercase())
3804 .unwrap_or(false)
3805 {
3806 result.push('.');
3807 }
3808 result.push(c.to_ascii_lowercase());
3809 first = false;
3810 }
3811
3812 if result.ends_with("event") {
3814 result = result[..result.len() - 5].to_string();
3815 }
3816
3817 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3819 result = format!("response.{}", result.trim_start_matches("response"));
3820 }
3821
3822 result
3823 }
3824
3825 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3826 let mut name = schema_name;
3828
3829 if name.starts_with("Response") && name.len() > 8 {
3831 name = &name[8..]; }
3833
3834 if name.ends_with("Event") && name.len() > 5 {
3836 name = &name[..name.len() - 5]; }
3838
3839 name = name.trim_matches('_');
3841
3842 if name.is_empty() {
3844 schema_name.to_string()
3845 } else {
3846 self.discriminator_to_variant_name(name)
3848 }
3849 }
3850
3851 fn hoist_inline_string_enum(
3875 &mut self,
3876 schema: &Schema,
3877 enum_values: Vec<String>,
3878 primary_name: String,
3879 dependencies: &mut HashSet<String>,
3880 ) -> SchemaType {
3881 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3882 matches!(
3883 &existing.schema_type,
3884 SchemaType::StringEnum { values: existing_values }
3885 if existing_values == values
3886 )
3887 }
3888
3889 let mut enum_type_name = primary_name.clone();
3890 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3891 None => true,
3892 Some(existing) if matches_values(existing, &enum_values) => false,
3893 Some(_) => {
3894 let suffix = enum_values
3897 .first()
3898 .map(|v| self.to_pascal_case(v))
3899 .unwrap_or_else(|| "Variant".to_string());
3900 let candidate = format!("{primary_name}{suffix}");
3901
3902 let resolved = match self.resolved_cache.get(&candidate) {
3903 None => Some((candidate.clone(), true)),
3904 Some(existing) if matches_values(existing, &enum_values) => {
3905 Some((candidate.clone(), false))
3906 }
3907 Some(_) => {
3908 let mut found = None;
3911 for n in 2..1000 {
3912 let numbered = format!("{candidate}_{n}");
3913 match self.resolved_cache.get(&numbered) {
3914 None => {
3915 found = Some((numbered, true));
3916 break;
3917 }
3918 Some(existing) if matches_values(existing, &enum_values) => {
3919 found = Some((numbered, false));
3920 break;
3921 }
3922 Some(_) => continue,
3923 }
3924 }
3925 found
3926 }
3927 };
3928
3929 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3930 enum_type_name = resolved_name;
3931 insert
3932 }
3933 };
3934
3935 if should_insert {
3938 self.resolved_cache.insert(
3939 enum_type_name.clone(),
3940 AnalyzedSchema {
3941 name: enum_type_name.clone(),
3942 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3943 schema_type: SchemaType::StringEnum {
3944 values: enum_values,
3945 },
3946 dependencies: HashSet::new(),
3947 nullable: false,
3948 description: schema.details().description.clone(),
3949 default: schema.details().default.clone(),
3950 },
3951 );
3952 }
3953
3954 dependencies.insert(enum_type_name.clone());
3956 SchemaType::Reference {
3957 target: enum_type_name,
3958 }
3959 }
3960
3961 fn analyze_array_schema(
3962 &mut self,
3963 schema: &Schema,
3964 parent_schema_name: &str,
3965 dependencies: &mut HashSet<String>,
3966 ) -> Result<SchemaType> {
3967 let details = schema.details();
3968
3969 if let Some(items_schema) = &details.items {
3971 let item_type = match items_schema.as_ref() {
3973 Schema::Reference { reference, .. } => {
3974 let target = self
3976 .extract_schema_name(reference)
3977 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3978 .to_string();
3979 dependencies.insert(target.clone());
3980 SchemaType::Reference { target }
3981 }
3982 Schema::RecursiveRef { recursive_ref, .. } => {
3983 if recursive_ref == "#" {
3985 let target = self
3987 .find_recursive_anchor_schema()
3988 .unwrap_or_else(|| parent_schema_name.to_string());
3989 dependencies.insert(target.clone());
3990 SchemaType::Reference { target }
3991 } else {
3992 let target = self
3993 .extract_schema_name(recursive_ref)
3994 .unwrap_or("RecursiveType")
3995 .to_string();
3996 dependencies.insert(target.clone());
3997 SchemaType::Reference { target }
3998 }
3999 }
4000 Schema::Typed { schema_type, .. } => {
4001 match schema_type {
4003 OpenApiSchemaType::String => {
4004 match items_schema
4008 .details()
4009 .string_enum_values()
4010 .filter(|values| !values.is_empty())
4011 {
4012 Some(values) => self.hoist_inline_string_enum(
4013 items_schema,
4014 values,
4015 format!("{parent_schema_name}Item"),
4016 dependencies,
4017 ),
4018 None => SchemaType::Primitive {
4019 rust_type: "String".to_string(),
4020 serde_with: None,
4021 },
4022 }
4023 }
4024 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
4025 let details = items_schema.details();
4026 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
4027 SchemaType::Primitive {
4028 rust_type,
4029 serde_with: None,
4030 }
4031 }
4032 OpenApiSchemaType::Boolean => SchemaType::Primitive {
4033 rust_type: "bool".to_string(),
4034 serde_with: None,
4035 },
4036 OpenApiSchemaType::Object => {
4037 let object_type_name = format!("{parent_schema_name}Item");
4039
4040 let object_type =
4042 self.analyze_object_schema(items_schema, dependencies)?;
4043
4044 let inline_schema = AnalyzedSchema {
4046 name: object_type_name.clone(),
4047 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
4048 schema_type: object_type,
4049 dependencies: dependencies.clone(),
4050 nullable: false,
4051 description: items_schema.details().description.clone(),
4052 default: None,
4053 };
4054
4055 self.resolved_cache
4057 .insert(object_type_name.clone(), inline_schema);
4058 dependencies.insert(object_type_name.clone());
4059
4060 SchemaType::Reference {
4062 target: object_type_name,
4063 }
4064 }
4065 OpenApiSchemaType::Array => {
4066 self.analyze_array_schema(
4068 items_schema,
4069 parent_schema_name,
4070 dependencies,
4071 )?
4072 }
4073 _ => SchemaType::Primitive {
4074 rust_type: "serde_json::Value".to_string(),
4075 serde_with: None,
4076 },
4077 }
4078 }
4079 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
4080 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
4082
4083 match &analyzed.schema_type {
4085 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
4086 let union_name = format!("{parent_schema_name}ItemUnion");
4089
4090 let mut union_schema = analyzed;
4092 union_schema.name = union_name.clone();
4093
4094 self.resolved_cache.insert(union_name.clone(), union_schema);
4096
4097 dependencies.insert(union_name.clone());
4099
4100 SchemaType::Reference { target: union_name }
4102 }
4103 _ => analyzed.schema_type,
4104 }
4105 }
4106 Schema::Untyped { .. } => {
4107 if let Some(inferred) = items_schema.inferred_type() {
4109 match inferred {
4110 OpenApiSchemaType::Object => {
4111 let object_type_name = format!("{parent_schema_name}Item");
4113
4114 let object_type =
4116 self.analyze_object_schema(items_schema, dependencies)?;
4117
4118 let inline_schema = AnalyzedSchema {
4120 name: object_type_name.clone(),
4121 original: serde_json::to_value(items_schema)
4122 .unwrap_or(Value::Null),
4123 schema_type: object_type,
4124 dependencies: dependencies.clone(),
4125 nullable: false,
4126 description: items_schema.details().description.clone(),
4127 default: None,
4128 };
4129
4130 self.resolved_cache
4132 .insert(object_type_name.clone(), inline_schema);
4133 dependencies.insert(object_type_name.clone());
4134
4135 SchemaType::Reference {
4137 target: object_type_name,
4138 }
4139 }
4140 OpenApiSchemaType::String => {
4141 match items_schema
4144 .details()
4145 .string_enum_values()
4146 .filter(|values| !values.is_empty())
4147 {
4148 Some(values) => self.hoist_inline_string_enum(
4149 items_schema,
4150 values,
4151 format!("{parent_schema_name}Item"),
4152 dependencies,
4153 ),
4154 None => SchemaType::Primitive {
4155 rust_type: "String".to_string(),
4156 serde_with: None,
4157 },
4158 }
4159 }
4160 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
4161 let details = items_schema.details();
4162 let rust_type = self.get_number_rust_type(inferred, details);
4163 SchemaType::Primitive {
4164 rust_type,
4165 serde_with: None,
4166 }
4167 }
4168 OpenApiSchemaType::Boolean => SchemaType::Primitive {
4169 rust_type: "bool".to_string(),
4170 serde_with: None,
4171 },
4172 _ => SchemaType::Primitive {
4173 rust_type: "serde_json::Value".to_string(),
4174 serde_with: None,
4175 },
4176 }
4177 } else {
4178 SchemaType::Primitive {
4179 rust_type: "serde_json::Value".to_string(),
4180 serde_with: None,
4181 }
4182 }
4183 }
4184 _ => SchemaType::Primitive {
4185 rust_type: "serde_json::Value".to_string(),
4186 serde_with: None,
4187 },
4188 };
4189
4190 Ok(SchemaType::Array {
4191 item_type: Box::new(item_type),
4192 })
4193 } else {
4194 Ok(SchemaType::Primitive {
4196 rust_type: "Vec<serde_json::Value>".to_string(),
4197 serde_with: None,
4198 })
4199 }
4200 }
4201
4202 fn get_number_rust_type(
4203 &self,
4204 schema_type: OpenApiSchemaType,
4205 details: &crate::openapi::SchemaDetails,
4206 ) -> String {
4207 let format = details.format.as_deref();
4211 match schema_type {
4212 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
4213 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
4214 _ => self.type_mapper.dynamic_json().rust_type,
4215 }
4216 }
4217
4218 fn analyze_anyof_union(
4219 &mut self,
4220 any_of_schemas: &[Schema],
4221 discriminator: Option<&Discriminator>,
4222 dependencies: &mut HashSet<String>,
4223 context_name: &str,
4224 ) -> Result<SchemaType> {
4225 let filtered_owned: Vec<Schema>;
4230 let any_of_schemas: &[Schema] = if any_of_schemas
4231 .iter()
4232 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4233 {
4234 filtered_owned = any_of_schemas
4235 .iter()
4236 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4237 .cloned()
4238 .collect();
4239 if filtered_owned.is_empty() {
4240 return Ok(SchemaType::Primitive {
4241 rust_type: "serde_json::Value".to_string(),
4242 serde_with: None,
4243 });
4244 }
4245 if filtered_owned.len() == 1 {
4246 return self
4247 .analyze_schema_value(&filtered_owned[0], context_name)
4248 .map(|a| a.schema_type);
4249 }
4250 &filtered_owned
4251 } else {
4252 any_of_schemas
4253 };
4254
4255 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
4257 let has_objects = any_of_schemas.iter().any(|s| {
4258 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
4259 || s.inferred_type() == Some(OpenApiSchemaType::Object)
4260 });
4261 let has_arrays = any_of_schemas
4262 .iter()
4263 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
4264
4265 let all_string_like = any_of_schemas.iter().all(|s| {
4268 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
4269 || s.details().const_value.is_some()
4270 });
4271
4272 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
4273 if let Some(disc) = discriminator {
4275 return self.analyze_oneof_union(
4277 any_of_schemas,
4278 Some(disc),
4279 context_name,
4280 dependencies,
4281 );
4282 }
4283
4284 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
4286 return self.analyze_oneof_union(
4287 any_of_schemas,
4288 Some(&Discriminator {
4289 property_name: disc_field,
4290 mapping: None,
4291 default_mapping: None,
4292 extensions: crate::extensions::Extensions::default(),
4293 }),
4294 context_name,
4295 dependencies,
4296 );
4297 }
4298
4299 let mut variants = Vec::new();
4301
4302 for schema in any_of_schemas {
4303 if let Some(ref_str) = schema.reference() {
4304 if let Some(target) = self.extract_schema_name(ref_str) {
4305 dependencies.insert(target.to_string());
4306 variants.push(SchemaRef {
4307 target: target.to_string(),
4308 nullable: false,
4309 });
4310 }
4311 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
4312 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
4313 {
4314 let inline_index = variants.len();
4316 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
4317
4318 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
4320
4321 variants.push(SchemaRef {
4322 target: inline_type_name,
4323 nullable: false,
4324 });
4325 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
4326 let array_type =
4328 self.analyze_array_schema(schema, context_name, dependencies)?;
4329
4330 let array_type_name = if let Some(items_schema) = &schema.details().items {
4332 if let Some(ref_str) = items_schema.reference() {
4333 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
4334 dependencies.insert(item_type_name.to_string());
4335 format!("{item_type_name}Array")
4336 } else {
4337 self.generate_context_aware_name(
4338 context_name,
4339 "Array",
4340 variants.len(),
4341 Some(schema),
4342 )
4343 }
4344 } else {
4345 self.generate_context_aware_name(
4346 context_name,
4347 "Array",
4348 variants.len(),
4349 Some(schema),
4350 )
4351 }
4352 } else {
4353 self.generate_context_aware_name(
4354 context_name,
4355 "Array",
4356 variants.len(),
4357 Some(schema),
4358 )
4359 };
4360
4361 self.resolved_cache.insert(
4363 array_type_name.clone(),
4364 AnalyzedSchema {
4365 name: array_type_name.clone(),
4366 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4367 schema_type: array_type,
4368 dependencies: HashSet::new(),
4369 nullable: false,
4370 description: Some("Array variant in union".to_string()),
4371 default: None,
4372 },
4373 );
4374
4375 dependencies.insert(array_type_name.clone());
4377
4378 variants.push(SchemaRef {
4379 target: array_type_name,
4380 nullable: false,
4381 });
4382 } else if let Some(schema_type) = schema.schema_type() {
4383 let primitive_unions = self
4393 .type_mapper
4394 .config_shape_primitive_unions()
4395 .unwrap_or(true);
4396
4397 if primitive_unions {
4398 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
4399 variants.push(SchemaRef {
4400 target: mapped.rust_type,
4401 nullable: false,
4402 });
4403 } else {
4404 let inline_index = variants.len();
4405 let inline_type_name = match schema_type {
4406 OpenApiSchemaType::String => {
4407 if inline_index == 0 {
4408 format!("{context_name}String")
4409 } else {
4410 format!("{context_name}StringVariant{inline_index}")
4411 }
4412 }
4413 OpenApiSchemaType::Number => {
4414 if inline_index == 0 {
4415 format!("{context_name}Number")
4416 } else {
4417 format!("{context_name}NumberVariant{inline_index}")
4418 }
4419 }
4420 OpenApiSchemaType::Integer => {
4421 if inline_index == 0 {
4422 format!("{context_name}Integer")
4423 } else {
4424 format!("{context_name}IntegerVariant{inline_index}")
4425 }
4426 }
4427 OpenApiSchemaType::Boolean => {
4428 if inline_index == 0 {
4429 format!("{context_name}Boolean")
4430 } else {
4431 format!("{context_name}BooleanVariant{inline_index}")
4432 }
4433 }
4434 _ => format!("{context_name}Variant{inline_index}"),
4435 };
4436
4437 let rust_type =
4438 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4439
4440 self.resolved_cache.insert(
4441 inline_type_name.clone(),
4442 AnalyzedSchema {
4443 name: inline_type_name.clone(),
4444 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4445 schema_type: SchemaType::Primitive {
4446 rust_type,
4447 serde_with: None,
4448 },
4449 dependencies: HashSet::new(),
4450 nullable: false,
4451 description: schema.details().description.clone(),
4452 default: None,
4453 },
4454 );
4455
4456 dependencies.insert(inline_type_name.clone());
4457
4458 variants.push(SchemaRef {
4459 target: inline_type_name,
4460 nullable: false,
4461 });
4462 }
4463 }
4464 }
4465
4466 if !variants.is_empty() {
4467 return Ok(SchemaType::Union { variants });
4468 }
4469 }
4470
4471 let all_strings = any_of_schemas.iter().all(|schema| {
4473 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4474 || schema.details().const_value.is_some()
4475 });
4476
4477 if all_strings {
4478 let mut enum_values = Vec::new();
4480 let mut has_open_string = false;
4481
4482 for schema in any_of_schemas {
4483 if let Some(const_val) = &schema.details().const_value {
4484 if let Some(const_str) = const_val.as_str() {
4485 enum_values.push(const_str.to_string());
4486 }
4487 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4488 has_open_string = true;
4489 }
4490 }
4491
4492 if !enum_values.is_empty() {
4493 if has_open_string {
4494 return Ok(SchemaType::ExtensibleEnum {
4497 known_values: enum_values,
4498 });
4499 } else {
4500 return Ok(SchemaType::StringEnum {
4502 values: enum_values,
4503 });
4504 }
4505 }
4506 }
4507
4508 Ok(SchemaType::Primitive {
4510 rust_type: "serde_json::Value".to_string(),
4511 serde_with: None,
4512 })
4513 }
4514
4515 fn find_recursive_anchor_schema(&self) -> Option<String> {
4517 for (schema_name, schema) in &self.schemas {
4519 let details = schema.details();
4520 if details.recursive_anchor == Some(true) {
4521 return Some(schema_name.clone());
4522 }
4523 }
4524
4525 None
4529 }
4530
4531 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4534 if let Schema::AnyOf { any_of, .. } = schema {
4536 if any_of.len() == 2 {
4537 let has_null = any_of
4538 .iter()
4539 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4540 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4541
4542 if has_null && has_empty_object {
4543 return true;
4544 }
4545 }
4546 }
4547
4548 self.is_dynamic_object_pattern(schema)
4550 }
4551
4552 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4554 let is_object = match schema.schema_type() {
4556 Some(OpenApiSchemaType::Object) => true,
4557 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4558 _ => false,
4559 };
4560
4561 if !is_object {
4562 return false;
4563 }
4564
4565 let details = schema.details();
4566
4567 if self.has_explicit_additional_properties(schema) {
4570 return false;
4571 }
4572
4573 let no_properties = details
4575 .properties
4576 .as_ref()
4577 .map(|props| props.is_empty())
4578 .unwrap_or(true);
4579
4580 if no_properties {
4581 let has_structural_constraints = details
4584 .required
4585 .as_ref()
4586 .map(|req| req.iter().any(|r| r != "type"))
4587 .unwrap_or(false)
4588 || details.pattern_properties.is_some()
4589 || details.property_names.is_some()
4590 || details.min_properties.is_some()
4591 || details.max_properties.is_some()
4592 || details.dependent_required.is_some()
4593 || details.dependent_schemas.is_some()
4594 || details.if_schema.is_some()
4595 || details.then_schema.is_some()
4596 || details.else_schema.is_some();
4597
4598 return !has_structural_constraints;
4599 }
4600
4601 false
4602 }
4603
4604 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4606 let details = schema.details();
4607
4608 matches!(
4610 &details.additional_properties,
4611 Some(crate::openapi::AdditionalProperties::Boolean(true))
4612 | Some(crate::openapi::AdditionalProperties::Schema(_))
4613 )
4614 }
4615
4616 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4618 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4619 .map_err(GeneratorError::ParseError)?;
4620 let mut canonical_operation_ids = HashSet::new();
4625
4626 if let Some(paths) = &spec.paths {
4627 for (path, path_item) in paths {
4628 let resolved = self.resolve_path_item(path_item, &spec)?;
4630 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4631 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4632 }
4633 }
4634 if let Some(webhooks) = &spec.webhooks {
4641 for (name, path_item) in webhooks {
4642 let synthetic_path = format!("/__webhook__/{name}");
4643 self.ingest_path_item_operations(
4644 &synthetic_path,
4645 path_item,
4646 analysis,
4647 &mut canonical_operation_ids,
4648 )?;
4649 }
4650 }
4651 Ok(())
4652 }
4653
4654 fn resolve_path_item(
4658 &self,
4659 path_item: &crate::openapi::PathItem,
4660 spec: &crate::openapi::OpenApiSpec,
4661 ) -> Result<Option<crate::openapi::PathItem>> {
4662 let Some(reference) = &path_item.reference else {
4663 return Ok(None);
4664 };
4665 let target_name = reference
4666 .strip_prefix("#/components/pathItems/")
4667 .ok_or_else(|| {
4668 GeneratorError::UnresolvedReference(format!(
4669 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4670 ))
4671 })?;
4672 let pi = spec
4673 .components
4674 .as_ref()
4675 .and_then(|c| c.path_items.as_ref())
4676 .and_then(|map| map.get(target_name))
4677 .ok_or_else(|| {
4678 GeneratorError::UnresolvedReference(format!(
4679 "Path Item ref {reference} not found in components/pathItems"
4680 ))
4681 })?;
4682 Ok(Some(pi.clone()))
4683 }
4684
4685 fn ingest_path_item_operations(
4686 &mut self,
4687 path: &str,
4688 path_item: &crate::openapi::PathItem,
4689 analysis: &mut SchemaAnalysis,
4690 canonical_operation_ids: &mut HashSet<String>,
4691 ) -> Result<()> {
4692 for (method, operation) in path_item.operations() {
4693 let raw_operation_id = operation
4695 .operation_id
4696 .clone()
4697 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4698
4699 let operation_id = if canonical_operation_ids
4710 .contains(&Self::canonical_operation_id(&raw_operation_id))
4711 {
4712 let method_lower = method.to_lowercase();
4713 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4714 let mut suffix = 2;
4715 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4716 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4717 suffix += 1;
4718 }
4719 eprintln!(
4720 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4721 raw_operation_id, method, path, candidate
4722 );
4723 candidate
4724 } else {
4725 raw_operation_id.clone()
4726 };
4727
4728 let (op_info, responses) = self.analyze_single_operation(
4729 &operation_id,
4730 method,
4731 path,
4732 operation,
4733 path_item.parameters.as_ref(),
4734 analysis,
4735 )?;
4736 analysis
4737 .operation_id_aliases
4738 .entry(raw_operation_id)
4739 .or_default()
4740 .push(operation_id.clone());
4741 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4742 analysis
4743 .operation_responses
4744 .insert(operation_id.clone(), responses);
4745 analysis.operations.insert(operation_id, op_info);
4746 }
4747 Ok(())
4748 }
4749
4750 fn canonical_operation_id(operation_id: &str) -> String {
4751 use heck::ToPascalCase;
4752 operation_id.replace('.', "_").to_pascal_case()
4753 }
4754
4755 fn generate_operation_id(method: &str, path: &str) -> String {
4758 let mut operation_id = method.to_lowercase();
4760
4761 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4763
4764 for part in path_parts {
4765 if part.is_empty() {
4766 continue;
4767 }
4768
4769 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4771 &part[1..part.len() - 1]
4772 } else {
4773 part
4774 };
4775
4776 let pascal_case_part = cleaned_part
4778 .split(&['-', '_'][..])
4779 .map(|s| {
4780 let mut chars = s.chars();
4781 match chars.next() {
4782 None => String::new(),
4783 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4784 }
4785 })
4786 .collect::<String>();
4787
4788 operation_id.push_str(&pascal_case_part);
4789 }
4790
4791 operation_id
4792 }
4793
4794 fn analyze_single_operation(
4796 &mut self,
4797 operation_id: &str,
4798 method: &str,
4799 path: &str,
4800 operation: &crate::openapi::Operation,
4801 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4802 _analysis: &mut SchemaAnalysis,
4803 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
4804 let raw_path_item = self
4805 .openapi_spec
4806 .get("paths")
4807 .and_then(|paths| paths.get(path))
4808 .cloned();
4809 let raw_operation = raw_path_item
4810 .as_ref()
4811 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
4812 .cloned();
4813 let request_body = operation
4814 .request_body
4815 .as_ref()
4816 .map(|request_body| self.resolve_request_body(request_body))
4817 .transpose()?;
4818 let mut op_info = OperationInfo {
4819 operation_id: operation_id.to_string(),
4820 method: method.to_uppercase(),
4821 path: normalize_operation_path(path),
4822 summary: operation.summary.clone(),
4823 description: operation.description.clone(),
4824 request_body: None,
4825 request_body_required: request_body
4827 .as_ref()
4828 .and_then(|rb| rb.required)
4829 .unwrap_or(false),
4830 response_schemas: BTreeMap::new(),
4831 parameters: Vec::new(),
4832 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4835 };
4836 let mut operation_responses = BTreeMap::new();
4837
4838 if let Some(request_body) = &request_body {
4840 use crate::openapi::{
4841 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
4842 media_type_essence,
4843 };
4844 if let Some((content_type, maybe_schema)) = request_body.best_content() {
4845 op_info.request_body = if is_json_media_type(content_type) {
4846 match maybe_schema {
4847 Some(s) => {
4848 let validation_schema = self
4849 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4850 .unwrap_or(
4851 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4852 );
4853 Some(
4854 self.resolve_or_inline_schema(s, operation_id, "Request")
4855 .map(|name| RequestBodyContent::Json {
4856 schema_name: name,
4857 media_type: content_type.to_string(),
4858 validation_schema,
4859 })?,
4860 )
4861 }
4862 None => Some(RequestBodyContent::SchemaLess {
4863 media_type: content_type.to_string(),
4864 }),
4865 }
4866 } else if is_form_urlencoded_media_type(content_type) {
4867 match maybe_schema {
4868 Some(s) => {
4869 let validation_schema = self
4870 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4871 .unwrap_or(
4872 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4873 );
4874 Some(
4875 self.resolve_or_inline_schema(s, operation_id, "Request")
4876 .map(|name| RequestBodyContent::FormUrlEncoded {
4877 schema_name: name,
4878 media_type: content_type.to_string(),
4879 validation_schema,
4880 })?,
4881 )
4882 }
4883 None => Some(RequestBodyContent::SchemaLess {
4884 media_type: content_type.to_string(),
4885 }),
4886 }
4887 } else if media_type_essence(content_type)
4888 .eq_ignore_ascii_case("multipart/form-data")
4889 {
4890 match maybe_schema {
4891 Some(schema) => {
4892 let validation_schema = self
4893 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4894 .unwrap_or(
4895 serde_json::to_value(schema)
4896 .map_err(GeneratorError::ParseError)?,
4897 );
4898 Some(
4899 self.resolve_or_inline_schema(schema, operation_id, "Request")
4900 .map(|schema_name| RequestBodyContent::Multipart {
4901 schema_name,
4902 media_type: content_type.to_string(),
4903 validation_schema,
4904 })?,
4905 )
4906 }
4907 None => Some(RequestBodyContent::SchemaLess {
4908 media_type: content_type.to_string(),
4909 }),
4910 }
4911 } else if is_binary_media_type(content_type, maybe_schema) {
4912 if media_type_essence(content_type)
4913 .eq_ignore_ascii_case("application/octet-stream")
4914 {
4915 Some(RequestBodyContent::OctetStream {
4916 media_type: content_type.to_string(),
4917 })
4918 } else {
4919 Some(RequestBodyContent::Binary {
4920 media_type: content_type.to_string(),
4921 })
4922 }
4923 } else if crate::openapi::is_text_media_type(content_type) {
4924 Some(RequestBodyContent::TextPlain {
4929 media_type: content_type.to_string(),
4930 })
4931 } else {
4932 None
4933 };
4934 }
4935 if op_info.request_body.is_none() {
4936 let mut media_types = request_body
4937 .content
4938 .as_ref()
4939 .map(|content| content.keys().cloned().collect::<Vec<_>>())
4940 .unwrap_or_default();
4941 media_types.sort();
4942 if !media_types.is_empty() {
4943 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
4944 }
4945 }
4946 }
4947
4948 if let Some(responses) = &operation.responses {
4950 for (status_code, response) in responses {
4951 let response = self.resolve_response(response)?;
4952 let supports_streaming = response.content.as_ref().is_some_and(|content| {
4958 content
4959 .keys()
4960 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
4961 });
4962 if supports_streaming {
4963 op_info.supports_streaming = true;
4964 }
4965
4966 let mut response_info = OperationResponse {
4967 supports_streaming,
4968 has_content: response
4969 .content
4970 .as_ref()
4971 .is_some_and(|content| !content.is_empty()),
4972 ..Default::default()
4973 };
4974 if let Some((media_type, schema)) = response.json_content() {
4975 if let Some(schema_ref) = schema.reference() {
4976 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4978 op_info
4979 .response_schemas
4980 .insert(status_code.clone(), schema_name.to_string());
4981 response_info.schema_name = Some(schema_name.to_string());
4982 response_info.media_type = Some(media_type.to_string());
4983 response_info.body = Some(OperationResponseBody::Json {
4984 schema_name: schema_name.to_string(),
4985 media_type: media_type.to_string(),
4986 });
4987 }
4988 } else {
4989 let synthetic_name =
4991 self.generate_inline_response_type_name(operation_id, status_code);
4992
4993 let mut deps = HashSet::new();
4995 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4996
4997 op_info
4998 .response_schemas
4999 .insert(status_code.clone(), synthetic_name.clone());
5000 response_info.body = Some(OperationResponseBody::Json {
5001 schema_name: synthetic_name.clone(),
5002 media_type: media_type.to_string(),
5003 });
5004 response_info.schema_name = Some(synthetic_name);
5005 response_info.media_type = Some(media_type.to_string());
5006 }
5007 }
5008 if response_info.body.is_none()
5009 && let Some(content) = response.content.as_ref()
5010 {
5011 let selected = content
5012 .iter()
5013 .find(|(media_type, media)| {
5014 matches!(
5015 crate::openapi::classify_response_media_type(
5016 media_type,
5017 media.schema.as_ref()
5018 ),
5019 crate::openapi::ResponseMediaKind::Text
5020 )
5021 })
5022 .or_else(|| {
5023 content.iter().find(|(media_type, media)| {
5024 matches!(
5025 crate::openapi::classify_response_media_type(
5026 media_type,
5027 media.schema.as_ref()
5028 ),
5029 crate::openapi::ResponseMediaKind::Binary
5030 ) && !crate::openapi::is_wildcard_media_type(media_type)
5031 })
5032 })
5033 .or_else(|| {
5034 content.iter().find(|(media_type, media)| {
5035 matches!(
5036 crate::openapi::classify_response_media_type(
5037 media_type,
5038 media.schema.as_ref()
5039 ),
5040 crate::openapi::ResponseMediaKind::Binary
5041 )
5042 })
5043 });
5044 if let Some((media_type, media)) = selected {
5045 response_info.body = match crate::openapi::classify_response_media_type(
5046 media_type,
5047 media.schema.as_ref(),
5048 ) {
5049 crate::openapi::ResponseMediaKind::Text => {
5050 Some(OperationResponseBody::Text {
5051 media_type: media_type.clone(),
5052 })
5053 }
5054 crate::openapi::ResponseMediaKind::Binary => {
5055 Some(OperationResponseBody::Binary {
5056 media_type: media_type.clone(),
5057 wildcard: crate::openapi::is_wildcard_media_type(media_type),
5058 })
5059 }
5060 _ => None,
5061 };
5062 }
5063 }
5064 response_info.unsupported_media_types = response
5065 .content
5066 .as_ref()
5067 .into_iter()
5068 .flat_map(|content| content.iter())
5069 .filter(|(media_type, content)| {
5070 match crate::openapi::classify_response_media_type(
5071 media_type,
5072 content.schema.as_ref(),
5073 ) {
5074 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
5075 crate::openapi::ResponseMediaKind::Unsupported => true,
5076 crate::openapi::ResponseMediaKind::EventStream
5077 | crate::openapi::ResponseMediaKind::Text
5078 | crate::openapi::ResponseMediaKind::Binary => false,
5079 }
5080 })
5081 .map(|(media_type, _)| media_type.clone())
5082 .collect();
5083 operation_responses.insert(status_code.clone(), response_info);
5084 }
5085 }
5086
5087 if op_info.supports_streaming
5090 && let Some(parameters) = &operation.parameters
5091 {
5092 for param in parameters {
5093 if let Some(name) = param.name.as_deref() {
5094 if name.eq_ignore_ascii_case("stream") {
5095 op_info.stream_parameter = Some(name.to_string());
5096 break;
5097 }
5098 }
5099 }
5100 }
5101
5102 if let Some(parameters) = &operation.parameters {
5104 for (index, param) in parameters.iter().enumerate() {
5105 let resolved = self.resolve_parameter(param).into_owned();
5109 let validation_schema = raw_operation
5110 .as_ref()
5111 .and_then(|operation| operation.get("parameters"))
5112 .and_then(Value::as_array)
5113 .and_then(|parameters| parameters.get(index))
5114 .and_then(|parameter| self.raw_parameter_schema(parameter));
5115 if let Some(param_info) =
5116 self.analyze_parameter(&resolved, operation_id, validation_schema)?
5117 {
5118 op_info.parameters.push(param_info);
5119 }
5120 }
5121 }
5122
5123 if let Some(path_params) = path_item_parameters {
5125 let existing_keys: std::collections::HashSet<(String, String)> = op_info
5126 .parameters
5127 .iter()
5128 .map(|p| (p.name.clone(), p.location.clone()))
5129 .collect();
5130 for (index, param) in path_params.iter().enumerate() {
5131 let resolved = self.resolve_parameter(param).into_owned();
5132 let validation_schema = raw_path_item
5133 .as_ref()
5134 .and_then(|path_item| path_item.get("parameters"))
5135 .and_then(Value::as_array)
5136 .and_then(|parameters| parameters.get(index))
5137 .and_then(|parameter| self.raw_parameter_schema(parameter));
5138 if let Some(param_info) =
5139 self.analyze_parameter(&resolved, operation_id, validation_schema)?
5140 {
5141 if !existing_keys
5142 .contains(&(param_info.name.clone(), param_info.location.clone()))
5143 {
5144 op_info.parameters.push(param_info);
5145 }
5146 }
5147 }
5148 }
5149
5150 let mut declared_path_names: std::collections::HashSet<String> = op_info
5158 .parameters
5159 .iter()
5160 .filter(|p| p.location == "path")
5161 .map(|p| p.name.clone())
5162 .collect();
5163 let bytes = path.as_bytes().iter();
5164 let mut current = String::new();
5165 let mut in_brace = false;
5166 let mut synthesized: Vec<String> = Vec::new();
5167 for b in bytes {
5168 match *b {
5169 b'{' => {
5170 in_brace = true;
5171 current.clear();
5172 }
5173 b'}' if in_brace => {
5174 in_brace = false;
5175 if !current.is_empty() && !declared_path_names.contains(¤t) {
5176 synthesized.push(current.clone());
5177 declared_path_names.insert(current.clone());
5178 }
5179 }
5180 _ if in_brace => current.push(*b as char),
5181 _ => {}
5182 }
5183 }
5184 for name in synthesized {
5185 eprintln!(
5186 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
5187 path, name
5188 );
5189 op_info.parameters.push(ParameterInfo {
5190 name,
5191 location: "path".to_string(),
5192 required: true,
5193 schema_ref: None,
5194 rust_type: "String".to_string(),
5195 description: None,
5196 enum_values: None,
5197 enum_varnames: None,
5198 rust_ident: None,
5199 query_serialization: None,
5200 validation_schema: None,
5201 });
5202 }
5203
5204 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
5212 for p in op_info.parameters.iter_mut() {
5213 let raw = base_param_ident(&p.name);
5214 let mut chosen = raw.clone();
5215 let mut suffix = 2;
5216 while !used.insert(chosen.clone()) {
5217 chosen = format!("{raw}_{suffix}");
5218 suffix += 1;
5219 }
5220 p.rust_ident = Some(chosen);
5221 }
5222
5223 Ok((op_info, operation_responses))
5224 }
5225
5226 fn resolve_request_body(
5228 &self,
5229 request_body: &crate::openapi::RequestBody,
5230 ) -> Result<crate::openapi::RequestBody> {
5231 let mut current = request_body.clone();
5232 let mut visited = HashSet::new();
5233 while let Some(reference) = current.reference.clone() {
5234 if !visited.insert(reference.clone()) {
5235 return Err(GeneratorError::CircularDependency(format!(
5236 "request body reference {reference}"
5237 )));
5238 }
5239
5240 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5241 GeneratorError::UnresolvedReference(format!(
5242 "external request body reference `{reference}` is not supported"
5243 ))
5244 })?;
5245 if !pointer.is_empty() && !pointer.starts_with('/') {
5246 return Err(GeneratorError::UnresolvedReference(format!(
5247 "request body reference `{reference}` is not a local JSON Pointer"
5248 )));
5249 }
5250 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5251 GeneratorError::UnresolvedReference(format!(
5252 "request body reference `{reference}` does not exist"
5253 ))
5254 })?;
5255 let object = value.as_object().ok_or_else(|| {
5256 GeneratorError::InvalidSchema(format!(
5257 "request body reference `{reference}` must target an object"
5258 ))
5259 })?;
5260 if !["$ref", "description", "required", "content"]
5261 .iter()
5262 .any(|field| object.contains_key(*field))
5263 {
5264 return Err(GeneratorError::InvalidSchema(format!(
5265 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
5266 )));
5267 }
5268 current = serde_json::from_value(value.clone()).map_err(|error| {
5269 GeneratorError::InvalidSchema(format!(
5270 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
5271 ))
5272 })?;
5273 }
5274 Ok(current)
5275 }
5276
5277 fn resolve_response(
5284 &self,
5285 response: &crate::openapi::Response,
5286 ) -> Result<crate::openapi::Response> {
5287 let mut current = response.clone();
5288 let mut visited = HashSet::new();
5289 while let Some(reference) = current.reference.clone() {
5290 if !visited.insert(reference.clone()) {
5291 return Err(GeneratorError::CircularDependency(format!(
5292 "response reference {reference}"
5293 )));
5294 }
5295
5296 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5297 GeneratorError::UnresolvedReference(format!(
5298 "external response reference `{reference}` is not supported"
5299 ))
5300 })?;
5301 if !pointer.is_empty() && !pointer.starts_with('/') {
5302 return Err(GeneratorError::UnresolvedReference(format!(
5303 "response reference `{reference}` is not a local JSON Pointer"
5304 )));
5305 }
5306 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5307 GeneratorError::UnresolvedReference(format!(
5308 "response reference `{reference}` does not exist"
5309 ))
5310 })?;
5311 let object = value.as_object().ok_or_else(|| {
5312 GeneratorError::InvalidSchema(format!(
5313 "response reference `{reference}` must target an object"
5314 ))
5315 })?;
5316 if !["$ref", "description", "headers", "content", "links"]
5317 .iter()
5318 .any(|field| object.contains_key(*field))
5319 {
5320 return Err(GeneratorError::InvalidSchema(format!(
5321 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
5322 )));
5323 }
5324 current = serde_json::from_value(value.clone()).map_err(|error| {
5325 GeneratorError::InvalidSchema(format!(
5326 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
5327 ))
5328 })?;
5329 }
5330 Ok(current)
5331 }
5332
5333 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
5340 use heck::ToPascalCase;
5341 let base_name = operation_id.replace('.', "_").to_pascal_case();
5342 let suffix = Self::status_code_suffix(status_code);
5343 format!("{}Response{}", base_name, suffix)
5344 }
5345
5346 fn status_code_suffix(status_code: &str) -> String {
5353 match status_code {
5354 "" | "200" => String::new(),
5355 "default" | "Default" => "Default".to_string(),
5356 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
5357 other => other.to_ascii_lowercase(),
5358 }
5359 }
5360
5361 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
5363 use heck::ToPascalCase;
5364 let base_name = operation_id.replace('.', "_").to_pascal_case();
5368 format!("{}Request", base_name)
5369 }
5370
5371 fn resolve_or_inline_schema(
5374 &mut self,
5375 schema: &crate::openapi::Schema,
5376 operation_id: &str,
5377 suffix: &str,
5378 ) -> Result<String> {
5379 if let Some(schema_ref) = schema.reference()
5380 && let Some(schema_name) = self.extract_schema_name(schema_ref)
5381 {
5382 return Ok(schema_name.to_string());
5383 }
5384 let synthetic_name = if suffix == "Request" {
5386 self.generate_inline_request_type_name(operation_id)
5387 } else {
5388 self.generate_inline_response_type_name(operation_id, "")
5389 };
5390 let mut deps = HashSet::new();
5391 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5392 Ok(synthetic_name)
5393 }
5394
5395 fn resolve_parameter<'a>(
5398 &'a self,
5399 param: &'a crate::openapi::Parameter,
5400 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
5401 if let Some(ref_str) = param.reference.as_deref() {
5402 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
5403 if let Some(resolved) = self.component_parameters.get(param_name) {
5404 return std::borrow::Cow::Borrowed(resolved);
5405 }
5406 }
5407 }
5408 std::borrow::Cow::Borrowed(param)
5409 }
5410
5411 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
5424 if self.resolve_cached_schema(name).is_some_and(|schema| {
5425 matches!(
5426 schema.schema_type,
5427 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
5428 )
5429 }) {
5430 return true;
5431 }
5432 let Some(schema_value) = self
5433 .openapi_spec
5434 .get("components")
5435 .and_then(|c| c.get("schemas"))
5436 .and_then(|s| s.get(name))
5437 else {
5438 return false;
5439 };
5440 let is_string_type = schema_value
5441 .get("type")
5442 .and_then(|v| v.as_str())
5443 .map(|s| s == "string")
5444 .unwrap_or(false);
5445 let has_enum_or_const =
5446 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
5447 is_string_type && has_enum_or_const
5448 }
5449
5450 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
5451 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
5452 return Some(value.clone());
5453 };
5454 let pointer = reference.strip_prefix('#')?;
5455 self.openapi_spec.pointer(pointer).cloned()
5456 }
5457
5458 fn raw_request_body_schema(
5459 &self,
5460 operation: Option<&Value>,
5461 content_type: &str,
5462 ) -> Option<Value> {
5463 let request_body = operation?.get("requestBody")?;
5464 self.resolve_raw_local_reference(request_body)?
5465 .get("content")?
5466 .get(content_type)?
5467 .get("schema")
5468 .cloned()
5469 }
5470
5471 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
5472 self.resolve_raw_local_reference(parameter)?
5473 .get("schema")
5474 .cloned()
5475 }
5476
5477 fn analyze_parameter(
5478 &mut self,
5479 param: &crate::openapi::Parameter,
5480 operation_id: &str,
5481 raw_validation_schema: Option<Value>,
5482 ) -> Result<Option<ParameterInfo>> {
5483 use heck::ToPascalCase;
5484
5485 let name = param.name.as_deref().unwrap_or("");
5486 let location = param.location.as_deref().unwrap_or("");
5487 let required = param.required.unwrap_or(false);
5488 let validation_schema = match raw_validation_schema {
5489 Some(schema) => Some(schema),
5490 None => param
5491 .schema
5492 .as_ref()
5493 .map(serde_json::to_value)
5494 .transpose()
5495 .map_err(GeneratorError::ParseError)?,
5496 };
5497
5498 let mut rust_type = "String".to_string();
5499 let mut schema_ref = None;
5500 let mut enum_values: Option<Vec<String>> = None;
5501 let mut enum_varnames: Option<Vec<String>> = None;
5502 let mut query_serialization: Option<QuerySerialization> = None;
5503
5504 let is_query = location == "query";
5510 let is_simple_header = location == "header"
5511 && matches!(param.style.as_deref(), None | Some("simple"))
5512 && param.explode != Some(true);
5513 let form_style = matches!(param.style.as_deref(), None | Some("form"));
5514 let form_exploded = form_style && param.explode.unwrap_or(true);
5515 let deep_object =
5516 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
5517
5518 let object_serialization = if !is_query {
5519 None
5520 } else if deep_object {
5521 Some(QuerySerialization::DeepObject)
5522 } else if form_exploded {
5523 Some(QuerySerialization::FormExplodedObject)
5524 } else if form_style {
5525 Some(QuerySerialization::FormObject)
5526 } else {
5527 None
5528 };
5529
5530 if let Some(schema) = ¶m.schema {
5531 if let Some(ref_str) = schema.reference() {
5532 if let Some(name) = self.extract_schema_name(ref_str) {
5538 if self.referenced_schema_is_string_enum(name) {
5539 schema_ref = Some(name.to_string());
5540 } else if object_serialization.is_some()
5541 && self.referenced_schema_is_object(name)
5542 {
5543 schema_ref = Some(name.to_string());
5544 query_serialization = if form_exploded && self.uses_aws_query_conventions()
5545 {
5546 match self.referenced_array_struct_item_type(name, 1) {
5547 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5548 Some(QuerySerialization::FormExplodedNestedObject {
5549 properties,
5550 })
5551 }
5552 _ => object_serialization.clone(),
5553 }
5554 } else {
5555 object_serialization.clone()
5556 };
5557 } else if (is_query && form_style || is_simple_header)
5558 && let Some(item_type) = self.referenced_array_param_item_type(name)
5559 {
5560 schema_ref = Some(name.to_string());
5566 query_serialization = Some(if is_simple_header {
5567 QuerySerialization::SimpleHeaderArray { item_type }
5568 } else if form_exploded {
5569 QuerySerialization::FormExplodedArray { item_type }
5570 } else {
5571 QuerySerialization::FormArray { item_type }
5572 });
5573 }
5574 }
5575 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
5576 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5581 let param_pascal = name.to_pascal_case();
5582 let synthetic_name = format!("{op_pascal}{param_pascal}");
5583 let mut deps = HashSet::new();
5584 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5585 schema_ref = Some(synthetic_name.clone());
5586 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
5587 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
5588 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5589 Some(QuerySerialization::FormExplodedNestedObject { properties })
5590 }
5591 _ => object_serialization.clone(),
5592 }
5593 } else {
5594 object_serialization.clone()
5595 };
5596 } else if (is_query && form_style || is_simple_header)
5597 && matches!(
5598 schema.schema_type(),
5599 Some(crate::openapi::SchemaType::Array)
5600 )
5601 && let Some(item_type) = self.array_param_item_type(schema)
5602 {
5603 query_serialization = Some(if is_simple_header {
5611 QuerySerialization::SimpleHeaderArray { item_type }
5612 } else if form_exploded {
5613 QuerySerialization::FormExplodedArray { item_type }
5614 } else {
5615 QuerySerialization::FormArray { item_type }
5616 });
5617 } else if let Some(schema_type) = schema.schema_type() {
5618 let format = schema.details().format.clone();
5624 rust_type = match schema_type {
5625 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5626 crate::openapi::SchemaType::Integer => {
5627 self.type_mapper.integer_format(format.as_deref()).rust_type
5628 }
5629 crate::openapi::SchemaType::Number => {
5630 self.type_mapper.number_format(format.as_deref()).rust_type
5631 }
5632 crate::openapi::SchemaType::String => "String".to_string(),
5633 _ => "String".to_string(),
5634 };
5635
5636 if matches!(schema_type, crate::openapi::SchemaType::String) {
5637 let details = schema.details();
5638 if details.is_string_enum() {
5639 if let Some(values) = details.string_enum_values() {
5640 if !values.is_empty() {
5641 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5642 let param_pascal = name.to_pascal_case();
5643 rust_type = format!("{op_pascal}{param_pascal}");
5644 enum_varnames = details
5649 .extra
5650 .get("x-enum-varnames")
5651 .and_then(Value::as_array)
5652 .map(|raw| {
5653 raw.iter()
5654 .filter_map(Value::as_str)
5655 .map(str::to_owned)
5656 .collect::<Vec<_>>()
5657 })
5658 .filter(|names| names.len() == values.len());
5659 enum_values = Some(values);
5660 }
5661 }
5662 }
5663 }
5664 }
5665
5666 if is_query && query_serialization.is_none() {
5667 let referenced_name = schema
5668 .reference()
5669 .and_then(|reference| self.extract_schema_name(reference));
5670 let is_object = referenced_name
5671 .is_some_and(|name| self.referenced_schema_is_object(name))
5672 || Self::schema_is_inline_object(schema);
5673 let is_array = referenced_name
5674 .is_some_and(|name| self.referenced_schema_is_array(name))
5675 || matches!(
5676 schema.schema_type(),
5677 Some(crate::openapi::SchemaType::Array)
5678 );
5679 let is_composed = referenced_name
5680 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
5681 let reason = if param.style.as_deref() == Some("deepObject")
5682 && param.explode == Some(false)
5683 {
5684 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
5685 } else if param.style.as_deref() == Some("deepObject") && !is_object {
5686 Some("style=deepObject is defined only for object query parameters".to_string())
5687 } else if is_object {
5688 Some(format!(
5689 "object query parameters do not support style={}",
5690 param.style.as_deref().unwrap_or("form")
5691 ))
5692 } else if is_array && form_style {
5693 Some(
5694 "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"
5695 .to_string(),
5696 )
5697 } else if is_array {
5698 Some(format!(
5699 "array query parameters do not yet support style={}",
5700 param.style.as_deref().unwrap_or("form")
5701 ))
5702 } else if is_composed {
5703 Some(
5704 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
5705 .to_string(),
5706 )
5707 } else {
5708 None
5709 };
5710 if let Some(reason) = reason {
5711 query_serialization = Some(QuerySerialization::Unsupported { reason });
5712 }
5713 }
5714 }
5715
5716 Ok(Some(ParameterInfo {
5717 name: name.to_string(),
5718 location: location.to_string(),
5719 required,
5720 schema_ref,
5721 rust_type,
5722 description: param.description.clone(),
5723 enum_values,
5724 enum_varnames,
5725 rust_ident: None,
5726 query_serialization,
5727 validation_schema,
5728 }))
5729 }
5730
5731 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
5740 let items = schema.details().items.as_deref()?;
5741 let unwrapped = unwrap_annotation_allof(items);
5745 if let Some(ref_str) = unwrapped.reference() {
5746 let name = self.extract_schema_name(ref_str)?;
5747 return self
5748 .referenced_array_scalar_item_type(name)
5749 .or_else(|| self.referenced_array_struct_item_type(name, 1));
5750 }
5751 let format = unwrapped.details().format.clone();
5752 let scalar = match unwrapped.schema_type()? {
5753 crate::openapi::SchemaType::String => "String".to_string(),
5754 crate::openapi::SchemaType::Integer => {
5755 self.type_mapper.integer_format(format.as_deref()).rust_type
5756 }
5757 crate::openapi::SchemaType::Number => {
5758 self.type_mapper.number_format(format.as_deref()).rust_type
5759 }
5760 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5761 _ => return None,
5762 };
5763 Some(ArrayItemType::Scalar(scalar))
5764 }
5765
5766 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
5769 let schema = self.resolve_cached_schema(name)?;
5770 let SchemaType::Array { item_type } = &schema.schema_type else {
5771 return None;
5772 };
5773 self.analyzed_array_item_type(item_type)
5774 }
5775
5776 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
5777 self.analyzed_array_item_type_at_depth(item_type, 1)
5778 }
5779
5780 fn referenced_array_struct_item_type(
5785 &self,
5786 name: &str,
5787 nested_array_depth: usize,
5788 ) -> Option<ArrayItemType> {
5789 let resolved = self.resolve_cached_schema(name)?;
5790 let SchemaType::Object {
5791 properties,
5792 required,
5793 additional_properties,
5794 } = &resolved.schema_type
5795 else {
5796 return None;
5797 };
5798 if properties.is_empty()
5799 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5800 {
5801 return None;
5802 }
5803 let mut projected = Vec::with_capacity(properties.len());
5804 let mut has_array = false;
5805 for (wire_name, property) in properties {
5806 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
5807 QueryStructPropertyType::Scalar(scalar)
5808 } else {
5809 if nested_array_depth == 0 {
5810 return None;
5811 }
5812 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
5813 let item_type =
5814 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
5815 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
5816 return None;
5817 }
5818 has_array = true;
5819 QueryStructPropertyType::Array { item_type }
5820 } else {
5821 has_array = true;
5822 QueryStructPropertyType::Object {
5823 properties: self.query_flat_object_properties(&property.schema_type)?,
5824 }
5825 }
5826 };
5827 projected.push(QueryStructProperty {
5828 wire_name: wire_name.clone(),
5829 required: required.contains(wire_name),
5830 value_type,
5831 });
5832 }
5833 if has_array {
5834 Some(ArrayItemType::NestedStructRef {
5835 schema_name: name.to_string(),
5836 properties: projected,
5837 })
5838 } else {
5839 Some(ArrayItemType::FlatStructRef {
5840 schema_name: name.to_string(),
5841 properties: projected,
5842 })
5843 }
5844 }
5845
5846 fn analyzed_array_item_type_at_depth(
5847 &self,
5848 item_type: &SchemaType,
5849 nested_array_depth: usize,
5850 ) -> Option<ArrayItemType> {
5851 match item_type {
5852 SchemaType::Primitive { rust_type, .. } => {
5853 Some(ArrayItemType::Scalar(rust_type.clone()))
5854 }
5855 SchemaType::Reference { target } => self
5856 .referenced_array_scalar_item_type(target)
5857 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
5858 _ => None,
5859 }
5860 }
5861
5862 fn resolve_query_array_type<'a>(
5863 &'a self,
5864 schema_type: &'a SchemaType,
5865 ) -> Option<&'a SchemaType> {
5866 match schema_type {
5867 SchemaType::Array { item_type } => Some(item_type),
5868 SchemaType::Reference { target } => {
5869 let resolved = self.resolve_cached_schema(target)?;
5870 let SchemaType::Array { item_type } = &resolved.schema_type else {
5871 return None;
5872 };
5873 Some(item_type)
5874 }
5875 _ => None,
5876 }
5877 }
5878
5879 fn query_flat_object_properties(
5880 &self,
5881 schema_type: &SchemaType,
5882 ) -> Option<Vec<QueryStructProperty>> {
5883 let schema_type = match schema_type {
5884 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
5885 other => other,
5886 };
5887 let SchemaType::Object {
5888 properties,
5889 required,
5890 additional_properties,
5891 } = schema_type
5892 else {
5893 return None;
5894 };
5895 if properties.is_empty()
5896 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5897 {
5898 return None;
5899 }
5900 properties
5901 .iter()
5902 .map(|(wire_name, property)| {
5903 Some(QueryStructProperty {
5904 wire_name: wire_name.clone(),
5905 required: required.contains(wire_name),
5906 value_type: QueryStructPropertyType::Scalar(
5907 self.query_scalar_type(&property.schema_type)?,
5908 ),
5909 })
5910 })
5911 .collect()
5912 }
5913
5914 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
5915 match schema_type {
5916 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
5917 "String" => Some(QueryScalarType::String),
5918 "bool" => Some(QueryScalarType::Boolean),
5919 value if value.starts_with('i') || value.starts_with('u') => {
5920 Some(QueryScalarType::Integer)
5921 }
5922 value if value.starts_with('f') => Some(QueryScalarType::Number),
5923 "serde_json::Value" => None,
5924 _ => Some(QueryScalarType::String),
5925 },
5926 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
5927 Some(QueryScalarType::String)
5928 }
5929 SchemaType::Reference { target } => {
5930 let resolved = self.resolve_cached_schema(target)?;
5931 self.query_scalar_type(&resolved.schema_type)
5932 }
5933 _ => None,
5934 }
5935 }
5936
5937 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
5945 let resolved = self.resolve_cached_schema(name)?;
5946 let supported = match &resolved.schema_type {
5947 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
5948 SchemaType::Primitive { .. } => resolved
5949 .original
5950 .get("type")
5951 .is_some_and(Self::query_scalar_type_value),
5952 _ => false,
5953 };
5954 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
5955 }
5956
5957 fn query_scalar_type_value(value: &Value) -> bool {
5958 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
5959 if let Some(value) = value.as_str() {
5960 return SCALARS.contains(&value);
5961 }
5962 let Some(values) = value.as_array() else {
5963 return false;
5964 };
5965 if !values.iter().all(Value::is_string) {
5966 return false;
5967 }
5968 let mut non_null = values
5969 .iter()
5970 .filter_map(Value::as_str)
5971 .filter(|value| *value != "null");
5972 let Some(scalar) = non_null.next() else {
5973 return false;
5974 };
5975 non_null.next().is_none() && SCALARS.contains(&scalar)
5976 }
5977
5978 fn referenced_schema_is_object(&self, name: &str) -> bool {
5982 self.resolve_cached_schema(name)
5983 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
5984 }
5985
5986 fn referenced_schema_is_array(&self, name: &str) -> bool {
5987 self.resolve_cached_schema(name)
5988 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
5989 }
5990
5991 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
5992 self.resolve_cached_schema(name).is_some_and(|schema| {
5993 matches!(
5994 schema.schema_type,
5995 SchemaType::Composition { .. }
5996 | SchemaType::Union { .. }
5997 | SchemaType::DiscriminatedUnion { .. }
5998 )
5999 })
6000 }
6001
6002 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
6003 let mut current = name;
6004 let mut visited = HashSet::new();
6005 loop {
6006 if !visited.insert(current) {
6007 return None;
6008 }
6009 let schema = self.resolved_cache.get(current)?;
6010 if let SchemaType::Reference { target } = &schema.schema_type {
6011 current = target;
6012 } else {
6013 return Some(schema);
6014 }
6015 }
6016 }
6017
6018 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
6020 match schema.schema_type() {
6021 Some(crate::openapi::SchemaType::Object) => true,
6022 None => schema.details().properties.is_some(),
6023 _ => false,
6024 }
6025 }
6026}
6027
6028fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
6029 let Some(schemas) = openapi_spec
6030 .pointer_mut("/components/schemas")
6031 .and_then(Value::as_object_mut)
6032 else {
6033 return;
6034 };
6035
6036 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
6037 for name in schemas.keys() {
6038 names_by_rust_name
6039 .entry(crate::generator::rust_type_name(name))
6040 .or_default()
6041 .push(name.clone());
6042 }
6043
6044 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
6047 let mut aliases = BTreeMap::<String, String>::new();
6048
6049 for (rust_name, mut names) in names_by_rust_name {
6050 if names.len() < 2 {
6051 continue;
6052 }
6053
6054 names.sort_by_key(|name| (name != &rust_name, name.clone()));
6057 for source_name in names.into_iter().skip(1) {
6058 let mut suffix = 2;
6059 let replacement = loop {
6060 let candidate = format!("{rust_name}{suffix}");
6061 if claimed_rust_names.insert(candidate.clone()) {
6062 break candidate;
6063 }
6064 suffix += 1;
6065 };
6066
6067 eprintln!(
6068 "⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
6069 );
6070 aliases.insert(source_name, replacement);
6071 }
6072 }
6073
6074 if aliases.is_empty() {
6075 return;
6076 }
6077
6078 let original_schemas = std::mem::take(schemas);
6079 for (name, schema) in original_schemas {
6080 schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
6081 }
6082
6083 rewrite_component_schema_references(openapi_spec, &aliases);
6084}
6085
6086fn disambiguate_analyzed_schema_names(
6087 analysis: &mut SchemaAnalysis,
6088 component_schemas: &BTreeMap<String, Schema>,
6089) {
6090 let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
6091 for name in analysis.schemas.keys() {
6092 names_by_rust_name
6093 .entry(crate::generator::rust_type_name(name))
6094 .or_default()
6095 .push(name.clone());
6096 }
6097
6098 let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
6099 let mut aliases = BTreeMap::<String, String>::new();
6100
6101 for (rust_name, mut names) in names_by_rust_name {
6102 if names.len() < 2 {
6103 continue;
6104 }
6105 names.sort_by_key(|name| {
6106 (
6107 !component_schemas.contains_key(name),
6108 name != &rust_name,
6109 name.clone(),
6110 )
6111 });
6112
6113 for source_name in names.into_iter().skip(1) {
6114 let mut suffix = 2;
6115 let replacement = loop {
6116 let candidate = format!("{rust_name}{suffix}");
6117 if claimed_rust_names.insert(candidate.clone()) {
6118 break candidate;
6119 }
6120 suffix += 1;
6121 };
6122 eprintln!(
6123 "⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
6124 );
6125 aliases.insert(source_name, replacement);
6126 }
6127 }
6128
6129 if aliases.is_empty() {
6130 return;
6131 }
6132
6133 let original_schemas = std::mem::take(&mut analysis.schemas);
6134 for (name, mut schema) in original_schemas {
6135 schema.name = renamed_schema_name(&schema.name, &aliases);
6136 schema.dependencies = schema
6137 .dependencies
6138 .into_iter()
6139 .map(|name| renamed_schema_name(&name, &aliases))
6140 .collect();
6141 rewrite_schema_type_names(&mut schema.schema_type, &aliases);
6142 analysis
6143 .schemas
6144 .insert(renamed_schema_name(&name, &aliases), schema);
6145 }
6146
6147 let original_edges = std::mem::take(&mut analysis.dependencies.edges);
6148 for (name, dependencies) in original_edges {
6149 analysis.dependencies.edges.insert(
6150 renamed_schema_name(&name, &aliases),
6151 dependencies
6152 .into_iter()
6153 .map(|name| renamed_schema_name(&name, &aliases))
6154 .collect(),
6155 );
6156 }
6157 analysis.dependencies.recursive_schemas = analysis
6158 .dependencies
6159 .recursive_schemas
6160 .iter()
6161 .map(|name| renamed_schema_name(name, &aliases))
6162 .collect();
6163
6164 analysis.patterns.tagged_enum_schemas = analysis
6165 .patterns
6166 .tagged_enum_schemas
6167 .iter()
6168 .map(|name| renamed_schema_name(name, &aliases))
6169 .collect();
6170 analysis.patterns.untagged_enum_schemas = analysis
6171 .patterns
6172 .untagged_enum_schemas
6173 .iter()
6174 .map(|name| renamed_schema_name(name, &aliases))
6175 .collect();
6176 analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
6177 .into_iter()
6178 .map(|(name, mappings)| {
6179 (
6180 renamed_schema_name(&name, &aliases),
6181 mappings
6182 .into_iter()
6183 .map(|(value, schema_name)| {
6184 (value, renamed_schema_name(&schema_name, &aliases))
6185 })
6186 .collect(),
6187 )
6188 })
6189 .collect();
6190
6191 for operation in analysis.operations.values_mut() {
6192 if let Some(request_body) = &mut operation.request_body {
6193 rewrite_request_body_schema_name(request_body, &aliases);
6194 }
6195 for schema_name in operation.response_schemas.values_mut() {
6196 *schema_name = renamed_schema_name(schema_name, &aliases);
6197 }
6198 for parameter in &mut operation.parameters {
6199 if let Some(schema_name) = &mut parameter.schema_ref {
6200 *schema_name = renamed_schema_name(schema_name, &aliases);
6201 }
6202 if let Some(serialization) = &mut parameter.query_serialization {
6203 rewrite_query_serialization_schema_names(serialization, &aliases);
6204 }
6205 }
6206 }
6207
6208 for responses in analysis.operation_responses.values_mut() {
6209 for response in responses.values_mut() {
6210 if let Some(schema_name) = &mut response.schema_name {
6211 *schema_name = renamed_schema_name(schema_name, &aliases);
6212 }
6213 if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
6214 *schema_name = renamed_schema_name(schema_name, &aliases);
6215 }
6216 }
6217 }
6218}
6219
6220fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
6221 aliases
6222 .get(name)
6223 .cloned()
6224 .unwrap_or_else(|| name.to_string())
6225}
6226
6227fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
6228 match schema_type {
6229 SchemaType::Object {
6230 properties,
6231 additional_properties,
6232 ..
6233 } => {
6234 for property in properties.values_mut() {
6235 rewrite_schema_type_names(&mut property.schema_type, aliases);
6236 }
6237 if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
6238 rewrite_schema_type_names(value_type, aliases);
6239 }
6240 }
6241 SchemaType::DiscriminatedUnion { variants, .. } => {
6242 for variant in variants {
6243 variant.type_name = renamed_schema_name(&variant.type_name, aliases);
6244 variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
6245 }
6246 }
6247 SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
6248 for variant in variants {
6249 variant.target = renamed_schema_name(&variant.target, aliases);
6250 }
6251 }
6252 SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
6253 SchemaType::Reference { target } => {
6254 *target = renamed_schema_name(target, aliases);
6255 }
6256 SchemaType::Primitive { .. }
6257 | SchemaType::StringEnum { .. }
6258 | SchemaType::ExtensibleEnum { .. } => {}
6259 }
6260}
6261
6262fn rewrite_request_body_schema_name(
6263 request_body: &mut RequestBodyContent,
6264 aliases: &BTreeMap<String, String>,
6265) {
6266 match request_body {
6267 RequestBodyContent::Json { schema_name, .. }
6268 | RequestBodyContent::FormUrlEncoded { schema_name, .. }
6269 | RequestBodyContent::Multipart { schema_name, .. } => {
6270 *schema_name = renamed_schema_name(schema_name, aliases);
6271 }
6272 _ => {}
6273 }
6274}
6275
6276fn rewrite_query_serialization_schema_names(
6277 serialization: &mut QuerySerialization,
6278 aliases: &BTreeMap<String, String>,
6279) {
6280 match serialization {
6281 QuerySerialization::FormExplodedArray { item_type }
6282 | QuerySerialization::FormArray { item_type }
6283 | QuerySerialization::SimpleHeaderArray { item_type } => {
6284 rewrite_array_item_type_schema_names(item_type, aliases);
6285 }
6286 QuerySerialization::FormExplodedNestedObject { properties } => {
6287 for property in properties {
6288 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6289 }
6290 }
6291 _ => {}
6292 }
6293}
6294
6295fn rewrite_array_item_type_schema_names(
6296 item_type: &mut ArrayItemType,
6297 aliases: &BTreeMap<String, String>,
6298) {
6299 match item_type {
6300 ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
6301 ArrayItemType::FlatStructRef {
6302 schema_name,
6303 properties,
6304 }
6305 | ArrayItemType::NestedStructRef {
6306 schema_name,
6307 properties,
6308 } => {
6309 *schema_name = renamed_schema_name(schema_name, aliases);
6310 for property in properties {
6311 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6312 }
6313 }
6314 ArrayItemType::Scalar(_) => {}
6315 }
6316}
6317
6318fn rewrite_query_property_type_schema_names(
6319 property_type: &mut QueryStructPropertyType,
6320 aliases: &BTreeMap<String, String>,
6321) {
6322 match property_type {
6323 QueryStructPropertyType::Array { item_type } => {
6324 rewrite_array_item_type_schema_names(item_type, aliases)
6325 }
6326 QueryStructPropertyType::Object { properties } => {
6327 for property in properties {
6328 rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6329 }
6330 }
6331 QueryStructPropertyType::Scalar(_) => {}
6332 }
6333}
6334
6335fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
6336 match value {
6337 Value::Array(values) => {
6338 for value in values {
6339 rewrite_component_schema_references(value, aliases);
6340 }
6341 }
6342 Value::Object(object) => {
6343 if let Some(Value::String(reference)) = object.get_mut("$ref") {
6344 rewrite_component_schema_reference(reference, aliases);
6345 }
6346
6347 if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
6348 for target_value in mapping.values_mut() {
6349 let Some(target) = target_value.as_str() else {
6350 continue;
6351 };
6352 let replacement = aliases.get(target).cloned().or_else(|| {
6353 let mut target = target.to_string();
6354 rewrite_component_schema_reference(&mut target, aliases).then_some(target)
6355 });
6356 if let Some(replacement) = replacement {
6357 *target_value = Value::String(replacement);
6358 }
6359 }
6360 }
6361
6362 for value in object.values_mut() {
6363 rewrite_component_schema_references(value, aliases);
6364 }
6365 }
6366 _ => {}
6367 }
6368}
6369
6370fn rewrite_component_schema_reference(
6371 reference: &mut String,
6372 aliases: &BTreeMap<String, String>,
6373) -> bool {
6374 const PREFIX: &str = "#/components/schemas/";
6375 let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
6376 return false;
6377 };
6378 let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
6379
6380 for (source, replacement) in aliases {
6381 let encoded_source = source.replace('~', "~0").replace('/', "~1");
6382 if encoded_name == encoded_source {
6383 reference.replace_range(
6384 PREFIX.len()..PREFIX.len() + encoded_source.len(),
6385 replacement,
6386 );
6387 return true;
6388 }
6389 }
6390
6391 false
6392}