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(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1094 let spec: OpenApiSpec =
1095 serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
1096 let schemas = Self::extract_schemas(&spec)?;
1097
1098 let component_parameters = spec
1099 .components
1100 .as_ref()
1101 .and_then(|c| c.parameters.as_ref())
1102 .cloned()
1103 .unwrap_or_default();
1104 Ok(Self {
1105 schemas,
1106 resolved_cache: BTreeMap::new(),
1107 openapi_spec,
1108 current_schema_name: None,
1109 component_parameters,
1110 type_mapper,
1111 })
1112 }
1113
1114 pub fn new_with_extensions(
1117 openapi_spec: Value,
1118 extension_paths: &[std::path::PathBuf],
1119 ) -> Result<Self> {
1120 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1121 Self::new(merged_spec)
1122 }
1123
1124 pub fn new_with_extensions_and_type_mapper(
1127 openapi_spec: Value,
1128 extension_paths: &[std::path::PathBuf],
1129 type_mapper: TypeMapper,
1130 ) -> Result<Self> {
1131 let merged_spec = merge_schema_extensions(openapi_spec, extension_paths)?;
1132 Self::with_type_mapper(merged_spec, type_mapper)
1133 }
1134
1135 pub fn type_mapper(&self) -> &TypeMapper {
1139 &self.type_mapper
1140 }
1141
1142 fn generate_context_aware_name(
1145 &self,
1146 base_context: &str,
1147 type_hint: &str,
1148 index: usize,
1149 schema: Option<&Schema>,
1150 ) -> String {
1151 if let Some(schema) = schema {
1153 if type_hint == "Array"
1155 && matches!(schema.schema_type(), Some(OpenApiSchemaType::Array))
1156 {
1157 if let Some(items_schema) = &schema.details().items {
1158 if let Some(item_type) = items_schema.schema_type() {
1160 match item_type {
1161 OpenApiSchemaType::Object => {
1162 return format!("{base_context}ItemArray");
1163 }
1164 OpenApiSchemaType::String => {
1165 return format!("{base_context}StringArray");
1166 }
1167 _ => {}
1168 }
1169 }
1170 }
1171 }
1172 }
1173
1174 match type_hint {
1176 "Array" => {
1177 format!("{base_context}Array")
1179 }
1180 "Variant" | "InlineVariant" => {
1181 if index == 0 {
1183 format!("{base_context}{type_hint}")
1184 } else {
1185 format!("{}{}{}", base_context, type_hint, index + 1)
1186 }
1187 }
1188 _ => {
1189 format!("{base_context}{type_hint}{index}")
1191 }
1192 }
1193 }
1194
1195 fn to_pascal_case(&self, s: &str) -> String {
1197 s.split(['_', '-'])
1198 .filter(|part| !part.is_empty())
1199 .map(|part| {
1200 let mut chars = part.chars();
1201 match chars.next() {
1202 None => String::new(),
1203 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1204 }
1205 })
1206 .collect()
1207 }
1208
1209 fn extract_schemas(spec: &OpenApiSpec) -> Result<BTreeMap<String, Schema>> {
1210 let schemas = spec.components.as_ref().and_then(|c| c.schemas.as_ref());
1215 Ok(schemas
1216 .map(|m| {
1217 m.iter()
1218 .map(|(k, v)| (k.clone(), v.clone()))
1219 .collect::<BTreeMap<_, _>>()
1220 })
1221 .unwrap_or_default())
1222 }
1223
1224 pub fn analyze(&mut self) -> Result<SchemaAnalysis> {
1225 let validation_context = ValidationContext {
1226 openapi_version: self
1227 .openapi_spec
1228 .get("openapi")
1229 .and_then(Value::as_str)
1230 .unwrap_or_default()
1231 .to_string(),
1232 json_schema_dialect: self
1233 .openapi_spec
1234 .get("jsonSchemaDialect")
1235 .and_then(Value::as_str)
1236 .map(str::to_string),
1237 component_schemas: self
1238 .openapi_spec
1239 .pointer("/components/schemas")
1240 .and_then(Value::as_object)
1241 .map(|schemas| {
1242 schemas
1243 .iter()
1244 .map(|(name, schema)| (name.clone(), schema.clone()))
1245 .collect()
1246 })
1247 .unwrap_or_default(),
1248 };
1249 let mut analysis = SchemaAnalysis {
1250 schemas: BTreeMap::new(),
1251 dependencies: DependencyGraph::new(),
1252 patterns: DetectedPatterns {
1253 tagged_enum_schemas: HashSet::new(),
1254 untagged_enum_schemas: HashSet::new(),
1255 type_mappings: BTreeMap::new(),
1256 },
1257 operations: BTreeMap::new(),
1258 operation_responses: BTreeMap::new(),
1259 operation_id_aliases: BTreeMap::new(),
1260 used_type_features: crate::type_mapping::UsedFeatures::default(),
1261 enum_extensions: BTreeMap::new(),
1262 validation_context,
1263 };
1264
1265 self.detect_patterns(&mut analysis.patterns)?;
1267
1268 let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
1270 for schema_name in schema_names {
1271 let analyzed = self.analyze_schema(&schema_name)?;
1272
1273 for dep in &analyzed.dependencies {
1275 analysis
1276 .dependencies
1277 .add_dependency(schema_name.clone(), dep.clone());
1278 }
1279
1280 analysis.schemas.insert(schema_name, analyzed);
1281 }
1282
1283 for (inline_name, inline_schema) in &self.resolved_cache {
1286 if !analysis.schemas.contains_key(inline_name) {
1287 analysis
1289 .schemas
1290 .insert(inline_name.clone(), inline_schema.clone());
1291
1292 for dep in &inline_schema.dependencies {
1294 analysis
1295 .dependencies
1296 .add_dependency(inline_name.clone(), dep.clone());
1297 }
1298
1299 let mut schemas_to_update = Vec::new();
1304 for (schema_name, schema) in &analysis.schemas {
1305 if schema_name == inline_name {
1307 continue;
1308 }
1309
1310 if schema.dependencies.contains(inline_name) {
1311 schemas_to_update.push(schema_name.clone());
1313 }
1314 }
1315
1316 for schema_name in schemas_to_update {
1318 analysis
1319 .dependencies
1320 .add_dependency(schema_name, inline_name.clone());
1321 }
1322 }
1323 }
1324
1325 self.analyze_operations(&mut analysis)?;
1327
1328 for (inline_name, inline_schema) in &self.resolved_cache {
1331 if !analysis.schemas.contains_key(inline_name) {
1332 analysis
1333 .schemas
1334 .insert(inline_name.clone(), inline_schema.clone());
1335
1336 for dep in &inline_schema.dependencies {
1338 analysis
1339 .dependencies
1340 .add_dependency(inline_name.clone(), dep.clone());
1341 }
1342 }
1343 }
1344
1345 analysis.used_type_features = self.type_mapper.used_features();
1349
1350 for (name, analyzed) in &analysis.schemas {
1355 let enum_value_count = match &analyzed.schema_type {
1356 SchemaType::StringEnum { values } => values.len(),
1357 SchemaType::ExtensibleEnum { known_values } => known_values.len(),
1358 _ => continue,
1359 };
1360 if let Some(ext) = extract_enum_extensions(&analyzed.original, enum_value_count, name) {
1361 analysis.enum_extensions.insert(name.clone(), ext);
1362 }
1363 }
1364
1365 Ok(analysis)
1366 }
1367
1368 fn detect_patterns(&self, patterns: &mut DetectedPatterns) -> Result<()> {
1369 for (schema_name, schema) in &self.schemas {
1370 if self.is_discriminated_union(schema) {
1372 patterns.tagged_enum_schemas.insert(schema_name.clone());
1373
1374 if let Some(mappings) = self.extract_type_mappings(schema)? {
1376 patterns.type_mappings.insert(schema_name.clone(), mappings);
1377 }
1378 }
1379 else if self.is_simple_union(schema) {
1381 patterns.untagged_enum_schemas.insert(schema_name.clone());
1382 }
1383 }
1384
1385 Ok(())
1386 }
1387
1388 fn is_discriminated_union(&self, schema: &Schema) -> bool {
1389 if schema.is_discriminated_union() {
1391 return true;
1392 }
1393
1394 if let Some(variants) = schema.union_variants() {
1396 return variants.len() > 2 && self.detect_discriminator_field(variants).is_some();
1397 }
1398
1399 false
1400 }
1401
1402 fn all_variants_have_const_field(&self, variants: &[Schema], field_name: &str) -> bool {
1403 variants.iter().all(|variant| {
1404 if let Some(ref_str) = variant.reference() {
1405 if let Some(schema_name) = self.extract_schema_name(ref_str) {
1407 if let Some(schema) = self.schemas.get(schema_name) {
1408 return self.has_const_discriminator_field(schema, field_name);
1409 }
1410 }
1411 } else {
1412 return self.has_const_discriminator_field(variant, field_name);
1414 }
1415 false
1416 })
1417 }
1418
1419 fn branch_resolves_to_object(&self, schema: &Schema) -> bool {
1428 if let Some(ref_str) = schema.reference() {
1430 return match self
1431 .extract_schema_name(ref_str)
1432 .and_then(|n| self.schemas.get(n))
1433 {
1434 Some(target) => self.branch_resolves_to_object(target),
1435 None => false,
1436 };
1437 }
1438 if matches!(
1441 schema,
1442 Schema::AllOf { .. } | Schema::AnyOf { .. } | Schema::OneOf { .. }
1443 ) {
1444 return true;
1445 }
1446 if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object)) {
1447 return true;
1448 }
1449 if schema.inferred_type() == Some(OpenApiSchemaType::Object) {
1450 return true;
1451 }
1452 false
1455 }
1456
1457 fn detect_discriminator_field(&self, variants: &[Schema]) -> Option<String> {
1461 if variants.is_empty() {
1462 return None;
1463 }
1464
1465 let first_variant = &variants[0];
1467 let first_schema = if let Some(ref_str) = first_variant.reference() {
1468 let schema_name = self.extract_schema_name(ref_str)?;
1469 self.schemas.get(schema_name)?
1470 } else {
1471 first_variant
1472 };
1473
1474 let properties = first_schema.details().properties.as_ref()?;
1475 let mut candidates: Vec<String> = Vec::new();
1476
1477 for (field_name, field_schema) in properties {
1478 let details = field_schema.details();
1479 let is_const = details.const_value.is_some()
1480 || details.enum_values.as_ref().is_some_and(|v| v.len() == 1)
1481 || details.extra.contains_key("const");
1482 if is_const {
1483 candidates.push(field_name.clone());
1484 }
1485 }
1486
1487 if candidates.is_empty() {
1488 return None;
1489 }
1490
1491 candidates.sort_by(|a, b| {
1493 if a == "type" {
1494 std::cmp::Ordering::Less
1495 } else if b == "type" {
1496 std::cmp::Ordering::Greater
1497 } else {
1498 a.cmp(b)
1499 }
1500 });
1501
1502 for candidate in &candidates {
1504 if self.all_variants_have_const_field(variants, candidate) {
1505 return Some(candidate.clone());
1506 }
1507 }
1508
1509 None
1510 }
1511
1512 fn has_const_discriminator_field(&self, schema: &Schema, field_name: &str) -> bool {
1513 if let Some(properties) = &schema.details().properties {
1514 if let Some(field) = properties.get(field_name) {
1515 if field.details().const_value.is_some() {
1517 return true;
1518 }
1519 if let Some(enum_vals) = &field.details().enum_values {
1521 return enum_vals.len() == 1;
1522 }
1523 return field.details().extra.contains_key("const");
1525 }
1526 }
1527 false
1528 }
1529
1530 fn is_simple_union(&self, schema: &Schema) -> bool {
1531 if let Some(variants) = schema.union_variants() {
1532 if variants.len() > 1 && !schema.is_nullable_pattern() {
1534 let has_refs = variants.iter().any(|v| v.is_reference());
1535 return has_refs;
1536 }
1537 }
1538 false
1539 }
1540
1541 fn extract_type_mappings(&self, schema: &Schema) -> Result<Option<BTreeMap<String, String>>> {
1542 let variants = schema.union_variants().ok_or_else(|| {
1543 GeneratorError::InvalidSchema("No variants found for discriminated union".to_string())
1544 })?;
1545
1546 let discriminator_field = if let Some(discriminator) = schema.discriminator() {
1548 discriminator.property_name.clone()
1549 } else if let Some(detected) = self.detect_discriminator_field(variants) {
1550 detected
1551 } else {
1552 "type".to_string() };
1554
1555 let mut mappings = BTreeMap::new();
1556
1557 for variant in variants {
1558 if let Some(ref_str) = variant.reference() {
1559 if let Some(type_name) = self.extract_schema_name(ref_str) {
1560 if let Some(variant_schema) = self.schemas.get(type_name) {
1561 if let Some(discriminator_value) = self
1562 .extract_discriminator_value_for_field(
1563 variant_schema,
1564 &discriminator_field,
1565 )
1566 {
1567 mappings.insert(type_name.to_string(), discriminator_value);
1568 }
1569 }
1570 }
1571 }
1572 }
1573
1574 if mappings.is_empty() {
1575 Ok(None)
1576 } else {
1577 Ok(Some(mappings))
1578 }
1579 }
1580
1581 #[allow(dead_code)]
1582 fn extract_discriminator_value(&self, schema: &Schema) -> Option<String> {
1583 self.extract_discriminator_value_for_field(schema, "type")
1584 }
1585
1586 fn extract_discriminator_value_for_field(
1587 &self,
1588 schema: &Schema,
1589 field_name: &str,
1590 ) -> Option<String> {
1591 if let Some(properties) = &schema.details().properties {
1592 if let Some(type_field) = properties.get(field_name) {
1593 if let Some(const_value) = &type_field.details().const_value {
1595 if let Some(value) = const_value.as_str() {
1596 return Some(value.to_string());
1597 }
1598 }
1599 if let Some(enum_values) = &type_field.details().enum_values {
1601 if enum_values.len() == 1 {
1602 return enum_values[0].as_str().map(|s| s.to_string());
1603 }
1604 }
1605 if let Some(const_value) = type_field.details().extra.get("const") {
1607 return const_value.as_str().map(|s| s.to_string());
1608 }
1609 if let Some(stainless_const) = type_field.details().extra.get("x-stainless-const") {
1611 if stainless_const.as_bool() == Some(true) {
1612 if let Some(default_value) = &type_field.details().default {
1613 if let Some(value) = default_value.as_str() {
1614 return Some(value.to_string());
1615 }
1616 }
1617 }
1618 }
1619 }
1620 }
1621 None
1622 }
1623
1624 fn get_any_reference<'a>(&self, schema: &'a Schema) -> Option<&'a str> {
1625 schema.reference().or_else(|| schema.recursive_reference())
1626 }
1627
1628 fn extract_schema_name<'a>(&self, ref_str: &'a str) -> Option<&'a str> {
1629 if ref_str == "#" {
1630 return None; }
1632
1633 let parts: Vec<&str> = ref_str.split('/').collect();
1634
1635 if parts.len() >= 4 && parts[0] == "#" && parts[2] == "schemas" {
1637 return Some(parts[3]);
1638 }
1639
1640 if parts.len() >= 3 && parts[0] == "#" && parts[1] == "definitions" {
1643 return Some(parts[2]);
1644 }
1645
1646 let last = parts.last()?;
1652 if last.is_empty()
1653 || last.chars().all(|c| c.is_ascii_digit())
1654 || matches!(
1655 *last,
1656 "schema" | "properties" | "items" | "additionalProperties"
1657 )
1658 {
1659 return None;
1660 }
1661 let first = last.chars().next().unwrap_or(' ');
1662 if !first.is_ascii_alphabetic() || !first.is_ascii_uppercase() {
1663 return None;
1664 }
1665 Some(last)
1666 }
1667
1668 fn analyze_schema(&mut self, schema_name: &str) -> Result<AnalyzedSchema> {
1669 if let Some(cached) = self.resolved_cache.get(schema_name) {
1671 return Ok(cached.clone());
1672 }
1673
1674 self.current_schema_name = Some(schema_name.to_string());
1676
1677 let schema = self
1678 .schemas
1679 .get(schema_name)
1680 .ok_or_else(|| GeneratorError::UnresolvedReference(schema_name.to_string()))?
1681 .clone();
1682
1683 self.resolved_cache.insert(
1685 schema_name.to_string(),
1686 AnalyzedSchema {
1687 name: schema_name.to_string(),
1688 original: serde_json::to_value(&schema).unwrap_or(Value::Null),
1689 schema_type: SchemaType::Reference {
1690 target: "placeholder".to_string(),
1691 },
1692 dependencies: HashSet::new(),
1693 nullable: false,
1694 description: None,
1695 default: None,
1696 },
1697 );
1698
1699 let analyzed = self.analyze_schema_value(&schema, schema_name)?;
1700
1701 self.resolved_cache
1703 .insert(schema_name.to_string(), analyzed.clone());
1704
1705 Ok(analyzed)
1706 }
1707
1708 fn analyze_schema_value(
1709 &mut self,
1710 schema: &Schema,
1711 schema_name: &str,
1712 ) -> Result<AnalyzedSchema> {
1713 let details = schema.details();
1714 let description = details.description.clone();
1715 let nullable = details.is_nullable() || schema.type_array_contains_null();
1717 let mut dependencies = HashSet::new();
1718
1719 let schema_type = match schema {
1720 Schema::Reference { reference, .. } => {
1721 match self.extract_schema_name(reference) {
1726 Some(name) => {
1727 let target = name.to_string();
1728 dependencies.insert(target.clone());
1729 SchemaType::Reference { target }
1730 }
1731 None => {
1732 eprintln!(
1733 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
1734 reference
1735 );
1736 SchemaType::Primitive {
1737 rust_type: "serde_json::Value".to_string(),
1738 serde_with: None,
1739 }
1740 }
1741 }
1742 }
1743 Schema::RecursiveRef { recursive_ref, .. }
1744 | Schema::DynamicRef {
1745 dynamic_ref: recursive_ref,
1746 ..
1747 } => {
1748 if recursive_ref == "#" {
1754 dependencies.insert(schema_name.to_string());
1755 SchemaType::Reference {
1756 target: schema_name.to_string(),
1757 }
1758 } else {
1759 let target = self
1760 .extract_schema_name(recursive_ref)
1761 .unwrap_or(schema_name)
1762 .to_string();
1763 dependencies.insert(target.clone());
1764 SchemaType::Reference { target }
1765 }
1766 }
1767 Schema::Typed { .. } | Schema::TypedMulti { .. } => {
1768 let primary = schema
1769 .schema_type()
1770 .cloned()
1771 .unwrap_or(OpenApiSchemaType::Object);
1772 let format = details.format.as_deref();
1773 match primary {
1774 OpenApiSchemaType::String => {
1775 if let Some(values) = details.string_enum_values() {
1776 SchemaType::StringEnum { values }
1777 } else {
1778 SchemaType::Primitive {
1779 rust_type: self.type_mapper.string_format(format).rust_type,
1780 serde_with: None,
1781 }
1782 }
1783 }
1784 OpenApiSchemaType::Integer => SchemaType::Primitive {
1785 rust_type: self.type_mapper.integer_format(format).rust_type,
1786 serde_with: None,
1787 },
1788 OpenApiSchemaType::Number => SchemaType::Primitive {
1789 rust_type: self.type_mapper.number_format(format).rust_type,
1790 serde_with: None,
1791 },
1792 OpenApiSchemaType::Boolean => SchemaType::Primitive {
1793 rust_type: self.type_mapper.boolean().rust_type,
1794 serde_with: None,
1795 },
1796 OpenApiSchemaType::Array => {
1797 self.analyze_array_schema(schema, schema_name, &mut dependencies)?
1799 }
1800 OpenApiSchemaType::Object => {
1801 if self.should_use_dynamic_json(schema) {
1803 SchemaType::Primitive {
1804 rust_type: self.type_mapper.dynamic_json().rust_type,
1805 serde_with: None,
1806 }
1807 } else {
1808 self.analyze_object_schema(schema, &mut dependencies)?
1810 }
1811 }
1812 _ => SchemaType::Primitive {
1813 rust_type: self.type_mapper.dynamic_json().rust_type,
1814 serde_with: None,
1815 },
1816 }
1817 }
1818 Schema::AnyOf {
1819 any_of,
1820 discriminator,
1821 ..
1822 } => {
1823 self.analyze_anyof_union(
1825 any_of,
1826 discriminator.as_ref(),
1827 &mut dependencies,
1828 schema_name,
1829 )?
1830 }
1831 Schema::OneOf {
1832 one_of,
1833 discriminator,
1834 ..
1835 } => {
1836 self.analyze_oneof_union(
1838 one_of,
1839 discriminator.as_ref(),
1840 schema_name,
1841 &mut dependencies,
1842 )?
1843 }
1844 Schema::AllOf { all_of, .. } => {
1845 self.analyze_allof_composition(all_of, &mut dependencies)?
1847 }
1848 Schema::Untyped { .. } => {
1849 if let Some(inferred) = schema.inferred_type() {
1851 match inferred {
1852 OpenApiSchemaType::Object => {
1853 if self.should_use_dynamic_json(schema) {
1854 SchemaType::Primitive {
1855 rust_type: "serde_json::Value".to_string(),
1856 serde_with: None,
1857 }
1858 } else {
1859 self.analyze_object_schema(schema, &mut dependencies)?
1860 }
1861 }
1862 OpenApiSchemaType::String if details.is_string_enum() => {
1863 SchemaType::StringEnum {
1864 values: details.string_enum_values().unwrap_or_default(),
1865 }
1866 }
1867 _ => SchemaType::Primitive {
1868 rust_type: "serde_json::Value".to_string(),
1869 serde_with: None,
1870 },
1871 }
1872 } else {
1873 SchemaType::Primitive {
1874 rust_type: "serde_json::Value".to_string(),
1875 serde_with: None,
1876 }
1877 }
1878 }
1879 };
1880
1881 Ok(AnalyzedSchema {
1882 name: schema_name.to_string(),
1883 original: serde_json::to_value(schema).unwrap_or(Value::Null), schema_type,
1885 dependencies,
1886 nullable,
1887 description,
1888 default: details.default.clone(),
1889 })
1890 }
1891
1892 fn analyze_object_schema(
1893 &mut self,
1894 schema: &Schema,
1895 dependencies: &mut HashSet<String>,
1896 ) -> Result<SchemaType> {
1897 let details = schema.details();
1898 let properties = &details.properties;
1899 let required = details
1900 .required
1901 .as_ref()
1902 .map(|req| req.iter().cloned().collect::<HashSet<String>>())
1903 .unwrap_or_default();
1904
1905 let mut property_info = BTreeMap::new();
1906
1907 if let Some(props) = properties {
1908 for (prop_name, prop_schema) in props {
1909 let prop_type = if let Schema::AnyOf { any_of, .. } = prop_schema {
1911 if self.should_use_dynamic_json(prop_schema) {
1913 SchemaType::Primitive {
1915 rust_type: "serde_json::Value".to_string(),
1916 serde_with: None,
1917 }
1918 } else if prop_schema.is_nullable_pattern()
1919 && let Some(non_null) = prop_schema.non_null_variant()
1920 {
1921 self.analyze_property_schema_with_context(
1929 non_null,
1930 Some(prop_name),
1931 dependencies,
1932 )?
1933 } else {
1934 let context_name = self
1937 .current_schema_name
1938 .clone()
1939 .unwrap_or_else(|| "Unknown".to_string());
1940
1941 let prop_pascal = self.to_pascal_case(prop_name);
1943 let mut union_type_name = format!("{context_name}{prop_pascal}");
1944
1945 if self.schemas.contains_key(&union_type_name)
1948 || self.resolved_cache.contains_key(&union_type_name)
1949 {
1950 let mut suffix = 2;
1951 loop {
1952 let candidate = format!("{union_type_name}Union{suffix}");
1953 if !self.schemas.contains_key(&candidate)
1954 && !self.resolved_cache.contains_key(&candidate)
1955 {
1956 union_type_name = candidate;
1957 break;
1958 }
1959 suffix += 1;
1960 if suffix > 1000 {
1961 break;
1962 }
1963 }
1964 }
1965
1966 let union_schema_type = self.analyze_anyof_union(
1968 any_of,
1969 prop_schema.discriminator(),
1970 dependencies,
1971 &union_type_name,
1972 )?;
1973
1974 self.resolved_cache.insert(
1976 union_type_name.clone(),
1977 AnalyzedSchema {
1978 name: union_type_name.clone(),
1979 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
1980 schema_type: union_schema_type,
1981 dependencies: HashSet::new(),
1982 nullable: false,
1983 description: prop_schema.details().description.clone(),
1984 default: None,
1985 },
1986 );
1987
1988 dependencies.insert(union_type_name.clone());
1990 SchemaType::Reference {
1991 target: union_type_name,
1992 }
1993 }
1994 } else if let Schema::OneOf {
1995 one_of,
1996 discriminator,
1997 ..
1998 } = prop_schema
1999 {
2000 if prop_schema.is_nullable_pattern()
2007 && let Some(non_null) = prop_schema.non_null_variant()
2008 {
2009 let unwrapped = self.analyze_property_schema_with_context(
2010 non_null,
2011 Some(prop_name),
2012 dependencies,
2013 )?;
2014 let prop_details = prop_schema.details();
2015 let prop_nullable = true;
2016 let prop_description = prop_details.description.clone();
2017 let prop_default = prop_details.default.clone();
2018 property_info.insert(
2019 prop_name.clone(),
2020 PropertyInfo {
2021 schema_type: unwrapped,
2022 nullable: prop_nullable,
2023 description: prop_description,
2024 default: prop_default,
2025 serde_attrs: Vec::new(),
2026 constraints: PropertyConstraints::from_schema_details(prop_details),
2027 },
2028 );
2029 continue;
2030 }
2031
2032 let context_name = self
2034 .current_schema_name
2035 .clone()
2036 .unwrap_or_else(|| "Unknown".to_string());
2037 let prop_pascal = self.to_pascal_case(prop_name);
2038 let mut union_type_name = format!("{context_name}{prop_pascal}");
2039 if self.schemas.contains_key(&union_type_name)
2041 || self.resolved_cache.contains_key(&union_type_name)
2042 {
2043 let mut suffix = 2;
2044 loop {
2045 let candidate = format!("{union_type_name}Union{suffix}");
2046 if !self.schemas.contains_key(&candidate)
2047 && !self.resolved_cache.contains_key(&candidate)
2048 {
2049 union_type_name = candidate;
2050 break;
2051 }
2052 suffix += 1;
2053 if suffix > 1000 {
2054 break;
2055 }
2056 }
2057 }
2058
2059 let union_schema_type = self.analyze_oneof_union(
2061 one_of,
2062 discriminator.as_ref(),
2063 &union_type_name,
2064 dependencies,
2065 )?;
2066
2067 self.resolved_cache.insert(
2069 union_type_name.clone(),
2070 AnalyzedSchema {
2071 name: union_type_name.clone(),
2072 original: serde_json::to_value(prop_schema).unwrap_or(Value::Null),
2073 schema_type: union_schema_type,
2074 dependencies: HashSet::new(),
2075 nullable: false,
2076 description: prop_schema.details().description.clone(),
2077 default: None,
2078 },
2079 );
2080
2081 dependencies.insert(union_type_name.clone());
2083 SchemaType::Reference {
2084 target: union_type_name,
2085 }
2086 } else {
2087 self.analyze_property_schema_with_context(
2089 prop_schema,
2090 Some(prop_name),
2091 dependencies,
2092 )?
2093 };
2094
2095 let prop_details = prop_schema.details();
2096 let prop_nullable = prop_schema.is_nullable_any();
2098 let prop_description = prop_details.description.clone();
2099 let prop_default = prop_details.default.clone();
2100
2101 property_info.insert(
2102 prop_name.clone(),
2103 PropertyInfo {
2104 schema_type: prop_type,
2105 nullable: prop_nullable,
2106 description: prop_description,
2107 default: prop_default,
2108 serde_attrs: Vec::new(),
2109 constraints: PropertyConstraints::from_schema_details(prop_details),
2110 },
2111 );
2112 }
2113 }
2114
2115 let typed_enabled = self
2123 .type_mapper
2124 .config()
2125 .shape
2126 .as_ref()
2127 .and_then(|s| s.additional_properties_typed)
2128 .unwrap_or(true);
2129
2130 let additional_properties = match &details.additional_properties {
2131 Some(crate::openapi::AdditionalProperties::Boolean(true)) => {
2132 ObjectAdditionalProperties::Untyped
2133 }
2134 Some(crate::openapi::AdditionalProperties::Boolean(false)) => {
2135 ObjectAdditionalProperties::Forbidden
2136 }
2137 Some(crate::openapi::AdditionalProperties::Schema(value_schema)) if typed_enabled => {
2138 let analyzed =
2139 self.analyze_property_schema_with_context(value_schema, None, dependencies)?;
2140 ObjectAdditionalProperties::Typed {
2141 value_type: Box::new(analyzed),
2142 }
2143 }
2144 Some(crate::openapi::AdditionalProperties::Schema(_)) => {
2145 ObjectAdditionalProperties::Untyped
2147 }
2148 None => ObjectAdditionalProperties::Forbidden,
2149 };
2150
2151 Ok(SchemaType::Object {
2152 properties: property_info,
2153 required,
2154 additional_properties,
2155 })
2156 }
2157
2158 fn analyze_property_schema_with_context(
2159 &mut self,
2160 schema: &Schema,
2161 property_name: Option<&str>,
2162 dependencies: &mut HashSet<String>,
2163 ) -> Result<SchemaType> {
2164 if let Some(ref_str) = self.get_any_reference(schema) {
2165 let target_opt = if ref_str == "#" {
2166 Some(
2167 self.find_recursive_anchor_schema()
2168 .unwrap_or_else(|| "UnknownRecursive".to_string()),
2169 )
2170 } else {
2171 self.extract_schema_name(ref_str).map(|s| s.to_string())
2172 };
2173 match target_opt {
2174 Some(target) => {
2175 dependencies.insert(target.clone());
2176 return Ok(SchemaType::Reference { target });
2177 }
2178 None => {
2179 eprintln!(
2180 "⚠️ unresolvable $ref `{}` — typing as serde_json::Value",
2181 ref_str
2182 );
2183 return Ok(SchemaType::Primitive {
2184 rust_type: "serde_json::Value".to_string(),
2185 serde_with: None,
2186 });
2187 }
2188 }
2189 }
2190
2191 if let Some(schema_type) = schema.schema_type() {
2192 match schema_type {
2193 OpenApiSchemaType::String => {
2194 if let Some(enum_values) = schema.details().string_enum_values() {
2196 let context_name = self
2199 .current_schema_name
2200 .clone()
2201 .unwrap_or_else(|| "Unknown".to_string());
2202
2203 let primary_name = if let Some(prop_name) = property_name {
2205 let prop_pascal = self.to_pascal_case(prop_name);
2207 format!("{context_name}{prop_pascal}")
2208 } else {
2209 let suffix = if !enum_values.is_empty() {
2212 let first_value = self.to_pascal_case(&enum_values[0]);
2213 format!("{first_value}Enum")
2214 } else {
2215 "StringEnum".to_string()
2216 };
2217 format!("{context_name}{suffix}")
2218 };
2219
2220 return Ok(self.hoist_inline_string_enum(
2221 schema,
2222 enum_values,
2223 primary_name,
2224 dependencies,
2225 ));
2226 } else {
2227 let mapped = self
2233 .type_mapper
2234 .string_format(schema.details().format.as_deref());
2235 return Ok(SchemaType::Primitive {
2236 rust_type: mapped.rust_type,
2237 serde_with: mapped.serde_with,
2238 });
2239 }
2240 }
2241 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
2242 let details = schema.details();
2243 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
2244 return Ok(SchemaType::Primitive {
2245 rust_type,
2246 serde_with: None,
2247 });
2248 }
2249 OpenApiSchemaType::Boolean => {
2250 return Ok(SchemaType::Primitive {
2251 rust_type: "bool".to_string(),
2252 serde_with: None,
2253 });
2254 }
2255 OpenApiSchemaType::Array => {
2256 let context_name = if let Some(prop_name) = property_name {
2258 let prop_pascal = self.to_pascal_case(prop_name);
2260 format!(
2261 "{}{}",
2262 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2263 prop_pascal
2264 )
2265 } else {
2266 "ArrayItem".to_string()
2268 };
2269 return self.analyze_array_schema(schema, &context_name, dependencies);
2270 }
2271 OpenApiSchemaType::Object => {
2272 if self.should_use_dynamic_json(schema) {
2274 return Ok(SchemaType::Primitive {
2275 rust_type: "serde_json::Value".to_string(),
2276 serde_with: None,
2277 });
2278 }
2279 let object_type_name = if let Some(prop_name) = property_name {
2281 let prop_pascal = self.to_pascal_case(prop_name);
2283 format!(
2284 "{}{}",
2285 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2286 prop_pascal
2287 )
2288 } else {
2289 format!(
2291 "{}Object",
2292 self.current_schema_name.as_deref().unwrap_or("Unknown")
2293 )
2294 };
2295
2296 let object_type = self.analyze_object_schema(schema, dependencies)?;
2298
2299 let inline_schema = AnalyzedSchema {
2301 name: object_type_name.clone(),
2302 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2303 schema_type: object_type,
2304 dependencies: dependencies.clone(),
2305 nullable: false,
2306 description: schema.details().description.clone(),
2307 default: None,
2308 };
2309
2310 self.resolved_cache
2312 .insert(object_type_name.clone(), inline_schema);
2313 dependencies.insert(object_type_name.clone());
2314
2315 return Ok(SchemaType::Reference {
2317 target: object_type_name,
2318 });
2319 }
2320 _ => {
2321 return Ok(SchemaType::Primitive {
2322 rust_type: "serde_json::Value".to_string(),
2323 serde_with: None,
2324 });
2325 }
2326 }
2327 }
2328
2329 if schema.is_nullable_pattern() {
2331 if let Some(non_null) = schema.non_null_variant() {
2332 return self.analyze_property_schema_with_context(
2333 non_null,
2334 property_name,
2335 dependencies,
2336 );
2337 }
2338 }
2339
2340 if self.should_use_dynamic_json(schema) {
2342 return Ok(SchemaType::Primitive {
2343 rust_type: "serde_json::Value".to_string(),
2344 serde_with: None,
2345 });
2346 }
2347
2348 if let Schema::AllOf { all_of, .. } = schema {
2350 return self.analyze_allof_composition(all_of, dependencies);
2351 }
2352
2353 if let Some(variants) = schema.union_variants() {
2355 match variants.len().cmp(&1) {
2356 std::cmp::Ordering::Equal => {
2357 return self.analyze_property_schema_with_context(
2359 &variants[0],
2360 property_name,
2361 dependencies,
2362 );
2363 }
2364 std::cmp::Ordering::Greater => {
2365 let union_name = if let Some(prop_name) = property_name {
2368 let prop_pascal = self.to_pascal_case(prop_name);
2370 format!(
2371 "{}{}",
2372 self.current_schema_name.as_deref().unwrap_or(""),
2373 prop_pascal
2374 )
2375 } else {
2376 "UnionType".to_string()
2377 };
2378
2379 if let Schema::OneOf {
2381 one_of,
2382 discriminator,
2383 ..
2384 } = schema
2385 {
2386 let oneof_result = self.analyze_oneof_union(
2388 one_of,
2389 discriminator.as_ref(),
2390 &union_name,
2391 dependencies,
2392 )?;
2393
2394 if let SchemaType::Union {
2396 variants: _union_variants,
2397 } = &oneof_result
2398 {
2399 self.resolved_cache.insert(
2401 union_name.clone(),
2402 AnalyzedSchema {
2403 name: union_name.clone(),
2404 original: serde_json::to_value(schema).unwrap_or(Value::Null),
2405 schema_type: oneof_result.clone(),
2406 dependencies: dependencies.clone(),
2407 nullable: false,
2408 description: schema.details().description.clone(),
2409 default: None,
2410 },
2411 );
2412
2413 dependencies.insert(union_name.clone());
2415 return Ok(SchemaType::Reference { target: union_name });
2416 }
2417
2418 return Ok(oneof_result);
2419 } else if let Schema::AnyOf {
2420 any_of,
2421 discriminator,
2422 ..
2423 } = schema
2424 {
2425 let union_analysis = self.analyze_anyof_union(
2427 any_of,
2428 discriminator.as_ref(),
2429 dependencies,
2430 &union_name,
2431 )?;
2432 return Ok(union_analysis);
2433 } else {
2434 let mut union_variants = Vec::new();
2437 for variant in variants {
2438 if let Some(ref_str) = variant.reference() {
2439 if let Some(target) = self.extract_schema_name(ref_str) {
2440 dependencies.insert(target.to_string());
2441 union_variants.push(SchemaRef {
2442 target: target.to_string(),
2443 nullable: false,
2444 });
2445 }
2446 }
2447 }
2448 return Ok(SchemaType::Union {
2449 variants: union_variants,
2450 });
2451 }
2452 }
2453 std::cmp::Ordering::Less => {}
2454 }
2455 }
2456
2457 if let Some(inferred_type) = schema.inferred_type() {
2459 match inferred_type {
2460 OpenApiSchemaType::Object => {
2461 if self.should_use_dynamic_json(schema) {
2463 return Ok(SchemaType::Primitive {
2464 rust_type: "serde_json::Value".to_string(),
2465 serde_with: None,
2466 });
2467 }
2468 return self.analyze_object_schema(schema, dependencies);
2469 }
2470 OpenApiSchemaType::Array => {
2471 let context_name = if let Some(prop_name) = property_name {
2472 let prop_pascal = self.to_pascal_case(prop_name);
2474 format!(
2475 "{}{}",
2476 self.current_schema_name.as_deref().unwrap_or("Unknown"),
2477 prop_pascal
2478 )
2479 } else {
2480 "ArrayItem".to_string()
2482 };
2483 return self.analyze_array_schema(schema, &context_name, dependencies);
2484 }
2485 OpenApiSchemaType::String => {
2486 if let Some(enum_values) = schema.details().string_enum_values() {
2487 return Ok(SchemaType::StringEnum {
2488 values: enum_values,
2489 });
2490 } else {
2491 return Ok(SchemaType::Primitive {
2492 rust_type: "String".to_string(),
2493 serde_with: None,
2494 });
2495 }
2496 }
2497 _ => {
2498 let rust_type = self.openapi_type_to_rust_type(inferred_type, schema.details());
2500 return Ok(SchemaType::Primitive {
2501 rust_type,
2502 serde_with: None,
2503 });
2504 }
2505 }
2506 }
2507
2508 Ok(SchemaType::Primitive {
2509 rust_type: "serde_json::Value".to_string(),
2510 serde_with: None,
2511 })
2512 }
2513
2514 fn analyze_allof_composition(
2515 &mut self,
2516 all_of_schemas: &[Schema],
2517 dependencies: &mut HashSet<String>,
2518 ) -> Result<SchemaType> {
2519 let referenced_targets = all_of_schemas
2524 .iter()
2525 .filter_map(|schema| schema.reference())
2526 .filter_map(|reference| self.extract_schema_name(reference))
2527 .collect::<Vec<_>>();
2528 let only_reference_and_annotations = all_of_schemas.iter().all(|schema| {
2529 if schema.reference().is_some() {
2530 return true;
2531 }
2532 serde_json::to_value(schema)
2533 .ok()
2534 .and_then(|value| value.as_object().cloned())
2535 .is_some_and(|object| {
2536 object.keys().all(|key| {
2537 matches!(
2538 key.as_str(),
2539 "title"
2540 | "description"
2541 | "deprecated"
2542 | "readOnly"
2543 | "writeOnly"
2544 | "examples"
2545 | "example"
2546 | "externalDocs"
2547 | "xml"
2548 | "$comment"
2549 ) || key.starts_with("x-")
2550 })
2551 })
2552 });
2553 if referenced_targets.len() == 1 && only_reference_and_annotations {
2554 let target = referenced_targets[0];
2555 dependencies.insert(target.to_string());
2556 return Ok(SchemaType::Reference {
2557 target: target.to_string(),
2558 });
2559 }
2560
2561 let mut merged_properties = BTreeMap::new();
2563 let mut merged_required = HashSet::new();
2564 let mut descriptions = Vec::new();
2565
2566 let current_context = self.current_schema_name.clone();
2568
2569 for schema in all_of_schemas {
2570 match schema {
2571 Schema::Reference { reference, .. } => {
2572 if let Some(target) = self.extract_schema_name(reference) {
2574 dependencies.insert(target.to_string());
2575
2576 let analyzed_ref = self.analyze_schema(target)?;
2578
2579 match &analyzed_ref.schema_type {
2581 SchemaType::Object {
2582 properties,
2583 required,
2584 ..
2585 } => {
2586 for (prop_name, prop_info) in properties {
2588 merged_properties.insert(prop_name.clone(), prop_info.clone());
2589 }
2590 for req in required {
2592 merged_required.insert(req.clone());
2593 }
2594 }
2595 _ => {
2596 if let Some(ref_schema) = self.schemas.get(target).cloned() {
2598 self.merge_schema_into_properties(
2599 &ref_schema,
2600 &mut merged_properties,
2601 &mut merged_required,
2602 dependencies,
2603 )?;
2604 }
2605 }
2606 }
2607 }
2608 }
2609 Schema::Typed {
2610 schema_type: OpenApiSchemaType::Object,
2611 ..
2612 }
2613 | Schema::Untyped { .. } => {
2614 let saved_context = self.current_schema_name.clone();
2616 self.current_schema_name = current_context.clone();
2617
2618 self.merge_schema_into_properties(
2620 schema,
2621 &mut merged_properties,
2622 &mut merged_required,
2623 dependencies,
2624 )?;
2625
2626 self.current_schema_name = saved_context;
2628 }
2629 _ => {
2630 self.merge_schema_into_properties(
2633 schema,
2634 &mut merged_properties,
2635 &mut merged_required,
2636 dependencies,
2637 )?;
2638 }
2639 }
2640
2641 if let Some(desc) = &schema.details().description {
2643 descriptions.push(desc.clone());
2644 }
2645 }
2646
2647 if !merged_properties.is_empty() {
2649 Ok(SchemaType::Object {
2650 properties: merged_properties,
2651 required: merged_required,
2652 additional_properties: ObjectAdditionalProperties::Forbidden,
2653 })
2654 } else {
2655 Ok(SchemaType::Composition {
2657 schemas: all_of_schemas
2658 .iter()
2659 .filter_map(|s| {
2660 if let Some(ref_str) = s.reference() {
2661 if let Some(target) = self.extract_schema_name(ref_str) {
2662 dependencies.insert(target.to_string());
2663 Some(SchemaRef {
2664 target: target.to_string(),
2665 nullable: false,
2666 })
2667 } else {
2668 None
2669 }
2670 } else {
2671 None
2672 }
2673 })
2674 .collect(),
2675 })
2676 }
2677 }
2678
2679 fn merge_schema_into_properties(
2680 &mut self,
2681 schema: &Schema,
2682 merged_properties: &mut BTreeMap<String, PropertyInfo>,
2683 merged_required: &mut HashSet<String>,
2684 dependencies: &mut HashSet<String>,
2685 ) -> Result<()> {
2686 let details = schema.details();
2687
2688 if let Some(properties) = &details.properties {
2690 for (prop_name, prop_schema) in properties {
2691 let prop_type = self.analyze_property_schema_with_context(
2692 prop_schema,
2693 Some(prop_name),
2694 dependencies,
2695 )?;
2696 let prop_details = prop_schema.details();
2697
2698 let nullable = prop_schema.is_nullable_any();
2705 merged_properties.insert(
2706 prop_name.clone(),
2707 PropertyInfo {
2708 schema_type: prop_type,
2709 nullable,
2710 description: prop_details.description.clone(),
2711 default: prop_details.default.clone(),
2712 serde_attrs: Vec::new(),
2713 constraints: PropertyConstraints::from_schema_details(prop_details),
2714 },
2715 );
2716 }
2717 }
2718
2719 if let Some(required) = &details.required {
2721 for field in required {
2722 merged_required.insert(field.clone());
2723 }
2724 }
2725
2726 Ok(())
2727 }
2728
2729 fn analyze_oneof_union(
2730 &mut self,
2731 one_of_schemas: &[Schema],
2732 discriminator: Option<&crate::openapi::Discriminator>,
2733 parent_name: &str,
2734 dependencies: &mut HashSet<String>,
2735 ) -> Result<SchemaType> {
2736 if one_of_schemas.len() == 2 {
2739 let null_count = one_of_schemas
2740 .iter()
2741 .filter(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2742 .count();
2743 if null_count == 1 {
2744 if let Some(non_null) = one_of_schemas
2745 .iter()
2746 .find(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
2747 {
2748 return self
2749 .analyze_schema_value(non_null, parent_name)
2750 .map(|a| a.schema_type);
2751 }
2752 }
2753 }
2754
2755 if discriminator.is_none() {
2757 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2759 }
2760
2761 if one_of_schemas
2767 .iter()
2768 .any(|s| !self.branch_resolves_to_object(s))
2769 {
2770 return self.analyze_untagged_oneof_union(one_of_schemas, parent_name, dependencies);
2771 }
2772
2773 let discriminator_field = discriminator
2775 .ok_or_else(|| {
2776 GeneratorError::InvalidDiscriminator(
2777 "expected discriminator after guard check".to_string(),
2778 )
2779 })?
2780 .property_name
2781 .clone();
2782
2783 let mut variants = Vec::new();
2784 let mut used_variant_names = std::collections::HashSet::new();
2785
2786 for variant_schema in one_of_schemas {
2787 let ref_info = if let Some(ref_str) = variant_schema.reference() {
2789 Some((ref_str, false))
2790 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2791 Some((recursive_ref, true))
2792 } else if let Schema::AllOf { all_of, .. } = variant_schema {
2793 if all_of.len() == 1 {
2795 if let Some(ref_str) = all_of[0].reference() {
2796 Some((ref_str, false))
2797 } else {
2798 all_of[0]
2799 .recursive_reference()
2800 .map(|recursive_ref| (recursive_ref, true))
2801 }
2802 } else {
2803 None
2804 }
2805 } else {
2806 None
2807 };
2808
2809 if let Some((ref_str, is_recursive)) = ref_info {
2810 let schema_name = if is_recursive && ref_str == "#" {
2811 self.find_recursive_anchor_schema()
2813 .or_else(|| self.current_schema_name.clone())
2814 .unwrap_or_else(|| "CompoundFilter".to_string())
2815 } else {
2816 self.extract_schema_name(ref_str)
2817 .map(|s| s.to_string())
2818 .unwrap_or_else(|| "UnknownRef".to_string())
2819 };
2820
2821 if !schema_name.is_empty() {
2822 dependencies.insert(schema_name.clone());
2823
2824 let discriminator_value = if let Some(disc) = discriminator {
2829 if let Some(mappings) = &disc.mapping {
2830 mappings
2833 .iter()
2834 .find(|(_, target_ref)| {
2835 target_ref.as_str() == ref_str
2837 || self
2838 .extract_schema_name(target_ref)
2839 .map(|s| s.to_string())
2840 == Some(schema_name.clone())
2841 })
2842 .map(|(key, _)| key.clone())
2843 .unwrap_or_else(|| {
2844 self.fallback_discriminator_value_for_field(
2845 &schema_name,
2846 &discriminator_field,
2847 )
2848 })
2849 } else {
2850 self.fallback_discriminator_value_for_field(
2851 &schema_name,
2852 &discriminator_field,
2853 )
2854 }
2855 } else {
2856 self.fallback_discriminator_value_for_field(
2857 &schema_name,
2858 &discriminator_field,
2859 )
2860 };
2861
2862 let base_name = self.to_rust_variant_name(&schema_name);
2864 let rust_name =
2865 self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2866
2867 let final_discriminator_value = discriminator_value;
2869
2870 variants.push(UnionVariant {
2871 rust_name,
2872 type_name: schema_name,
2873 discriminator_value: final_discriminator_value,
2874 schema_ref: ref_str.to_string(),
2875 });
2876 }
2877 } else {
2878 let variant_index = variants.len();
2880 let inline_type_name =
2881 self.generate_inline_type_name(variant_schema, variant_index);
2882
2883 let discriminator_value = if let Some(disc) = discriminator {
2885 if let Some(mappings) = &disc.mapping {
2886 mappings
2888 .iter()
2889 .find(|(_, target_ref)| {
2890 target_ref.contains(&format!("variant_{variant_index}"))
2891 })
2892 .map(|(key, _)| key.clone())
2893 .unwrap_or_else(|| {
2894 self.extract_inline_discriminator_value(
2895 variant_schema,
2896 &discriminator_field,
2897 variant_index,
2898 )
2899 })
2900 } else {
2901 self.extract_inline_discriminator_value(
2902 variant_schema,
2903 &discriminator_field,
2904 variant_index,
2905 )
2906 }
2907 } else {
2908 self.extract_inline_discriminator_value(
2909 variant_schema,
2910 &discriminator_field,
2911 variant_index,
2912 )
2913 };
2914
2915 let base_name = if discriminator_value.starts_with("variant_") {
2917 format!("Variant{variant_index}")
2918 } else {
2919 let clean_name = self.discriminator_to_variant_name(&discriminator_value);
2921 self.to_rust_variant_name(&clean_name)
2922 };
2923 let rust_name = self.ensure_unique_variant_name(base_name, &mut used_variant_names);
2924
2925 let final_discriminator_value = discriminator_value;
2927
2928 variants.push(UnionVariant {
2929 rust_name,
2930 type_name: inline_type_name.clone(),
2931 discriminator_value: final_discriminator_value,
2932 schema_ref: format!("inline_{variant_index}"),
2933 });
2934
2935 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
2937 }
2938 }
2939
2940 if variants.is_empty() {
2941 let mut union_variants = Vec::new();
2944
2945 for (variant_index, variant_schema) in one_of_schemas.iter().enumerate() {
2946 if let Some(ref_str) = variant_schema.reference() {
2948 if let Some(schema_name) = self.extract_schema_name(ref_str) {
2949 dependencies.insert(schema_name.to_string());
2950 union_variants.push(SchemaRef {
2951 target: schema_name.to_string(),
2952 nullable: false,
2953 });
2954 }
2955 } else if let Some(recursive_ref) = variant_schema.recursive_reference() {
2956 let schema_name = if recursive_ref == "#" {
2957 self.find_recursive_anchor_schema()
2959 .or_else(|| self.current_schema_name.clone())
2960 .unwrap_or_else(|| "CompoundFilter".to_string())
2961 } else {
2962 self.extract_schema_name(recursive_ref)
2963 .map(|s| s.to_string())
2964 .unwrap_or_else(|| "RecursiveType".to_string())
2965 };
2966 dependencies.insert(schema_name.clone());
2967 union_variants.push(SchemaRef {
2968 target: schema_name,
2969 nullable: false,
2970 });
2971 } else {
2972 let inline_name = self.generate_context_aware_name(
2974 parent_name,
2975 "InlineVariant",
2976 variant_index,
2977 Some(variant_schema),
2978 );
2979 let analyzed = self.analyze_schema_value(variant_schema, &inline_name)?;
2980 let variant_type = analyzed.schema_type;
2981
2982 for dep in &analyzed.dependencies {
2984 dependencies.insert(dep.clone());
2985 }
2986
2987 match &variant_type {
2988 SchemaType::Primitive { rust_type, .. } => {
2990 union_variants.push(SchemaRef {
2991 target: rust_type.clone(),
2992 nullable: false,
2993 });
2994 }
2995 SchemaType::Array { item_type } => {
2997 match item_type.as_ref() {
2998 SchemaType::Primitive { rust_type, .. } => {
2999 let type_name = format!("Vec<{rust_type}>");
3000 union_variants.push(SchemaRef {
3001 target: type_name,
3002 nullable: false,
3003 });
3004 }
3005 SchemaType::Reference { target } => {
3006 let type_name = format!("Vec<{target}>");
3007 union_variants.push(SchemaRef {
3008 target: type_name,
3009 nullable: false,
3010 });
3011 }
3012 _ => {
3013 let inline_type_name = self.generate_context_aware_name(
3015 parent_name,
3016 "Variant",
3017 variant_index,
3018 None,
3019 );
3020 self.add_inline_schema(
3021 &inline_type_name,
3022 variant_schema,
3023 dependencies,
3024 )?;
3025 union_variants.push(SchemaRef {
3026 target: inline_type_name,
3027 nullable: false,
3028 });
3029 }
3030 }
3031 }
3032 SchemaType::Reference { target } => {
3034 union_variants.push(SchemaRef {
3035 target: target.clone(),
3036 nullable: false,
3037 });
3038 }
3039 _ => {
3041 let inline_type_name =
3042 format!("{}Variant{}", parent_name, variant_index + 1);
3043 self.add_inline_schema(
3044 &inline_type_name,
3045 variant_schema,
3046 dependencies,
3047 )?;
3048 union_variants.push(SchemaRef {
3049 target: inline_type_name,
3050 nullable: false,
3051 });
3052 }
3053 }
3054 }
3055 }
3056
3057 if !union_variants.is_empty() {
3058 return Ok(SchemaType::Union {
3059 variants: union_variants,
3060 });
3061 }
3062
3063 return Ok(SchemaType::Primitive {
3065 rust_type: "serde_json::Value".to_string(),
3066 serde_with: None,
3067 });
3068 }
3069
3070 Ok(SchemaType::DiscriminatedUnion {
3071 discriminator_field,
3072 variants,
3073 })
3074 }
3075
3076 fn analyze_untagged_oneof_union(
3077 &mut self,
3078 one_of_schemas: &[Schema],
3079 parent_name: &str,
3080 dependencies: &mut HashSet<String>,
3081 ) -> Result<SchemaType> {
3082 let filtered: Vec<&Schema> = one_of_schemas
3086 .iter()
3087 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
3088 .collect();
3089
3090 if filtered.len() == 1 {
3092 return self
3093 .analyze_schema_value(filtered[0], parent_name)
3094 .map(|a| a.schema_type);
3095 }
3096
3097 let mut union_variants = Vec::new();
3098
3099 for (variant_index, variant_schema) in filtered.iter().copied().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 SchemaType::Array {
3168 item_type: inner_item_type,
3169 } => {
3170 match inner_item_type.as_ref() {
3171 SchemaType::Primitive { rust_type, .. } => {
3172 let type_name = format!("Vec<Vec<{rust_type}>>");
3173 union_variants.push(SchemaRef {
3174 target: type_name,
3175 nullable: false,
3176 });
3177 }
3178 SchemaType::Reference { target } => {
3179 let type_name = format!("Vec<Vec<{target}>>");
3180 union_variants.push(SchemaRef {
3181 target: type_name,
3182 nullable: false,
3183 });
3184 }
3185 _ => {
3186 let inline_type_name = self.generate_context_aware_name(
3188 parent_name,
3189 "Variant",
3190 variant_index,
3191 None,
3192 );
3193 self.add_inline_schema(
3194 &inline_type_name,
3195 variant_schema,
3196 dependencies,
3197 )?;
3198 union_variants.push(SchemaRef {
3199 target: inline_type_name,
3200 nullable: false,
3201 });
3202 }
3203 }
3204 }
3205 _ => {
3206 let inline_type_name = self.generate_context_aware_name(
3208 parent_name,
3209 "Variant",
3210 variant_index,
3211 None,
3212 );
3213 self.add_inline_schema(
3214 &inline_type_name,
3215 variant_schema,
3216 dependencies,
3217 )?;
3218 union_variants.push(SchemaRef {
3219 target: inline_type_name,
3220 nullable: false,
3221 });
3222 }
3223 }
3224 }
3225 SchemaType::Reference { target } => {
3227 union_variants.push(SchemaRef {
3228 target: target.clone(),
3229 nullable: false,
3230 });
3231 }
3232 _ => {
3234 let inline_type_name = self.generate_context_aware_name(
3235 parent_name,
3236 "Variant",
3237 variant_index,
3238 None,
3239 );
3240 self.add_inline_schema(&inline_type_name, variant_schema, dependencies)?;
3241 union_variants.push(SchemaRef {
3242 target: inline_type_name,
3243 nullable: false,
3244 });
3245 }
3246 }
3247 }
3248 }
3249
3250 if !union_variants.is_empty() {
3251 return Ok(SchemaType::Union {
3252 variants: union_variants,
3253 });
3254 }
3255
3256 Ok(SchemaType::Primitive {
3258 rust_type: "serde_json::Value".to_string(),
3259 serde_with: None,
3260 })
3261 }
3262
3263 fn add_inline_schema(
3264 &mut self,
3265 type_name: &str,
3266 schema: &Schema,
3267 dependencies: &mut HashSet<String>,
3268 ) -> Result<()> {
3269 if let Some(schema_type) = schema.schema_type() {
3271 match schema_type {
3272 OpenApiSchemaType::String
3273 | OpenApiSchemaType::Integer
3274 | OpenApiSchemaType::Number
3275 | OpenApiSchemaType::Boolean => {
3276 let rust_type =
3277 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
3278
3279 self.resolved_cache.insert(
3281 type_name.to_string(),
3282 AnalyzedSchema {
3283 name: type_name.to_string(),
3284 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3285 schema_type: SchemaType::Primitive {
3286 rust_type,
3287 serde_with: None,
3288 },
3289 dependencies: HashSet::new(),
3290 nullable: false,
3291 description: schema.details().description.clone(),
3292 default: None,
3293 },
3294 );
3295 return Ok(());
3296 }
3297 _ => {}
3298 }
3299 }
3300
3301 let previous_schema_name = self.current_schema_name.take();
3305 self.current_schema_name = Some(type_name.to_string());
3306 let analyzed = self.analyze_schema_value(schema, type_name)?;
3307 self.current_schema_name = previous_schema_name;
3308
3309 self.resolved_cache.insert(type_name.to_string(), analyzed);
3311
3312 if let Some(cached) = self.resolved_cache.get(type_name) {
3314 for dep in &cached.dependencies {
3315 dependencies.insert(dep.clone());
3316 }
3317 }
3318
3319 Ok(())
3320 }
3321
3322 fn extract_inline_discriminator_value(
3323 &self,
3324 schema: &Schema,
3325 discriminator_field: &str,
3326 variant_index: usize,
3327 ) -> String {
3328 if let Some(properties) = &schema.details().properties {
3330 if let Some(discriminator_prop) = properties.get(discriminator_field) {
3331 if let Some(enum_values) = &discriminator_prop.details().enum_values {
3333 if enum_values.len() == 1 {
3334 if let Some(value) = enum_values[0].as_str() {
3335 return value.to_string();
3336 }
3337 }
3338 }
3339 if let Some(const_value) = discriminator_prop.details().extra.get("const") {
3341 if let Some(value) = const_value.as_str() {
3342 return value.to_string();
3343 }
3344 }
3345 if let Some(const_value) = &discriminator_prop.details().const_value {
3347 if let Some(value) = const_value.as_str() {
3348 return value.to_string();
3349 }
3350 }
3351 }
3352 }
3353
3354 if let Some(inferred_name) = self.infer_variant_name_from_structure(schema, variant_index) {
3356 return inferred_name;
3357 }
3358
3359 format!("variant_{variant_index}")
3361 }
3362
3363 fn infer_variant_name_from_structure(
3364 &self,
3365 schema: &Schema,
3366 _variant_index: usize,
3367 ) -> Option<String> {
3368 let details = schema.details();
3369
3370 if let Some(properties) = &details.properties {
3372 if properties.contains_key("text") && properties.len() <= 3 {
3374 return Some("text".to_string());
3375 }
3376 if properties.contains_key("image") || properties.contains_key("source") {
3377 return Some("image".to_string());
3378 }
3379 if properties.contains_key("document") {
3380 return Some("document".to_string());
3381 }
3382 if properties.contains_key("tool_use_id") || properties.contains_key("tool_result") {
3383 return Some("tool_result".to_string());
3384 }
3385 if properties.contains_key("content") && properties.contains_key("is_error") {
3386 return Some("tool_result".to_string());
3387 }
3388 if properties.contains_key("partial_json") {
3389 return Some("partial_json".to_string());
3390 }
3391
3392 let property_names: Vec<&String> = properties.keys().collect();
3394
3395 for prop_name in &property_names {
3397 if prop_name.contains("result") {
3398 return Some("result".to_string());
3399 }
3400 if prop_name.contains("error") {
3401 return Some("error".to_string());
3402 }
3403 if prop_name.contains("content") && property_names.len() <= 2 {
3404 return Some("content".to_string());
3405 }
3406 }
3407
3408 let significant_props = property_names
3410 .iter()
3411 .filter(|&name| !["type", "id", "cache_control"].contains(&name.as_str()))
3412 .collect::<Vec<_>>();
3413
3414 if significant_props.len() == 1 {
3415 return Some((*significant_props[0]).clone());
3416 }
3417 }
3418
3419 if let Some(description) = &details.description {
3421 let desc_lower = description.to_lowercase();
3422 if desc_lower.contains("text") && desc_lower.len() < 100 {
3423 return Some("text".to_string());
3424 }
3425 if desc_lower.contains("image") {
3426 return Some("image".to_string());
3427 }
3428 if desc_lower.contains("document") {
3429 return Some("document".to_string());
3430 }
3431 if desc_lower.contains("tool") && desc_lower.contains("result") {
3432 return Some("tool_result".to_string());
3433 }
3434 }
3435
3436 None
3437 }
3438
3439 fn discriminator_to_variant_name(&self, discriminator: &str) -> String {
3440 if discriminator.is_empty() {
3442 return "Variant".to_string();
3443 }
3444
3445 let mut result = String::new();
3446 let mut next_upper = true;
3447
3448 for c in discriminator.chars() {
3449 match c {
3450 'a'..='z' => {
3451 if next_upper {
3452 result.push(c.to_ascii_uppercase());
3453 next_upper = false;
3454 } else {
3455 result.push(c);
3456 }
3457 }
3458 'A'..='Z' => {
3459 result.push(c);
3460 next_upper = false;
3461 }
3462 '0'..='9' => {
3463 result.push(c);
3464 next_upper = false;
3465 }
3466 '_' | '-' | '.' | ' ' | '/' | '\\' => {
3467 next_upper = true;
3469 }
3470 _ => {
3471 next_upper = true;
3473 }
3474 }
3475 }
3476
3477 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3479 result = format!("Variant{result}");
3480 }
3481
3482 result
3483 }
3484
3485 fn ensure_unique_variant_name(
3486 &self,
3487 base_name: String,
3488 used_names: &mut std::collections::HashSet<String>,
3489 ) -> String {
3490 let mut candidate = base_name.clone();
3491 let mut counter = 1;
3492
3493 while used_names.contains(&candidate) {
3494 counter += 1;
3495 candidate = format!("{base_name}{counter}");
3496 }
3497
3498 used_names.insert(candidate.clone());
3499 candidate
3500 }
3501
3502 fn generate_inline_type_name(&self, schema: &Schema, variant_index: usize) -> String {
3503 if let Some(meaningful_name) = self.infer_type_name_from_structure(schema) {
3505 return meaningful_name;
3506 }
3507
3508 let context = self.current_schema_name.as_deref().unwrap_or("Inline");
3510 self.generate_context_aware_name(context, "Variant", variant_index, Some(schema))
3511 }
3512
3513 fn infer_type_name_from_structure(&self, schema: &Schema) -> Option<String> {
3514 let details = schema.details();
3515
3516 if let Some(description) = &details.description {
3518 if let Some(name_from_desc) = self.extract_type_name_from_description(description) {
3519 return Some(name_from_desc);
3520 }
3521 }
3522
3523 if let Some(properties) = &details.properties {
3525 if let Some(name_from_props) = self.extract_type_name_from_properties(properties) {
3526 return Some(format!("{name_from_props}Block"));
3527 }
3528 }
3529
3530 None
3531 }
3532
3533 fn extract_type_name_from_description(&self, description: &str) -> Option<String> {
3534 if description.len() > 100 || description.contains('\n') {
3536 return None;
3537 }
3538
3539 let words: Vec<&str> = description
3541 .split_whitespace()
3542 .take(2) .filter(|word| {
3544 let w = word.to_lowercase();
3545 word.len() > 2
3546 && ![
3547 "the", "and", "for", "with", "that", "this", "are", "can", "will", "was",
3548 ]
3549 .contains(&w.as_str())
3550 })
3551 .collect();
3552
3553 if words.is_empty() {
3554 return None;
3555 }
3556
3557 let combined = words.join("_");
3559 let pascal_name = self.discriminator_to_variant_name(&combined);
3560
3561 if !pascal_name.ends_with("Content")
3563 && !pascal_name.ends_with("Block")
3564 && !pascal_name.ends_with("Type")
3565 {
3566 Some(format!("{pascal_name}Content"))
3567 } else {
3568 Some(pascal_name)
3569 }
3570 }
3571
3572 fn extract_type_name_from_properties(
3573 &self,
3574 properties: &std::collections::BTreeMap<String, crate::openapi::Schema>,
3575 ) -> Option<String> {
3576 let significant_props: Vec<&String> = properties
3578 .keys()
3579 .filter(|name| !["type", "id", "cache_control"].contains(&name.as_str()))
3580 .collect();
3581
3582 if significant_props.is_empty() {
3583 return None;
3584 }
3585
3586 if significant_props.len() == 1 {
3588 let prop_name = significant_props[0];
3589 return Some(self.discriminator_to_variant_name(prop_name));
3590 }
3591
3592 let mut sorted_props = significant_props.clone();
3595 sorted_props.sort();
3596 if let Some(first_prop) = sorted_props.first() {
3597 return Some(self.discriminator_to_variant_name(first_prop));
3598 }
3599
3600 None
3601 }
3602
3603 fn openapi_type_to_rust_type(
3604 &self,
3605 openapi_type: OpenApiSchemaType,
3606 details: &crate::openapi::SchemaDetails,
3607 ) -> String {
3608 self.type_mapper.map(openapi_type, details).rust_type
3613 }
3614
3615 #[allow(dead_code)]
3616 fn fallback_discriminator_value(&self, schema_name: &str) -> String {
3617 self.fallback_discriminator_value_for_field(schema_name, "type")
3618 }
3619
3620 fn fallback_discriminator_value_for_field(
3621 &self,
3622 schema_name: &str,
3623 field_name: &str,
3624 ) -> String {
3625 if let Some(ref_schema) = self.schemas.get(schema_name) {
3627 if let Some(extracted) =
3628 self.extract_discriminator_value_for_field(ref_schema, field_name)
3629 {
3630 return extracted;
3631 }
3632 }
3633
3634 self.generate_discriminator_value_from_name(schema_name)
3636 }
3637
3638 fn generate_discriminator_value_from_name(&self, schema_name: &str) -> String {
3639 let mut result = String::new();
3641 let mut chars = schema_name.chars().peekable();
3642 let mut first = true;
3643
3644 while let Some(c) = chars.next() {
3645 if c.is_uppercase()
3646 && !first
3647 && chars
3648 .peek()
3649 .map(|&next| next.is_lowercase())
3650 .unwrap_or(false)
3651 {
3652 result.push('.');
3653 }
3654 result.push(c.to_ascii_lowercase());
3655 first = false;
3656 }
3657
3658 if result.ends_with("event") {
3660 result = result[..result.len() - 5].to_string();
3661 }
3662
3663 if schema_name.starts_with("Response") && !result.starts_with("response.") {
3665 result = format!("response.{}", result.trim_start_matches("response"));
3666 }
3667
3668 result
3669 }
3670
3671 fn to_rust_variant_name(&self, schema_name: &str) -> String {
3672 let mut name = schema_name;
3674
3675 if name.starts_with("Response") && name.len() > 8 {
3677 name = &name[8..]; }
3679
3680 if name.ends_with("Event") && name.len() > 5 {
3682 name = &name[..name.len() - 5]; }
3684
3685 name = name.trim_matches('_');
3687
3688 if name.is_empty() {
3690 schema_name.to_string()
3691 } else {
3692 self.discriminator_to_variant_name(name)
3694 }
3695 }
3696
3697 fn hoist_inline_string_enum(
3721 &mut self,
3722 schema: &Schema,
3723 enum_values: Vec<String>,
3724 primary_name: String,
3725 dependencies: &mut HashSet<String>,
3726 ) -> SchemaType {
3727 fn matches_values(existing: &AnalyzedSchema, values: &[String]) -> bool {
3728 matches!(
3729 &existing.schema_type,
3730 SchemaType::StringEnum { values: existing_values }
3731 if existing_values == values
3732 )
3733 }
3734
3735 let mut enum_type_name = primary_name.clone();
3736 let should_insert = match self.resolved_cache.get(&enum_type_name) {
3737 None => true,
3738 Some(existing) if matches_values(existing, &enum_values) => false,
3739 Some(_) => {
3740 let suffix = enum_values
3743 .first()
3744 .map(|v| self.to_pascal_case(v))
3745 .unwrap_or_else(|| "Variant".to_string());
3746 let candidate = format!("{primary_name}{suffix}");
3747
3748 let resolved = match self.resolved_cache.get(&candidate) {
3749 None => Some((candidate.clone(), true)),
3750 Some(existing) if matches_values(existing, &enum_values) => {
3751 Some((candidate.clone(), false))
3752 }
3753 Some(_) => {
3754 let mut found = None;
3757 for n in 2..1000 {
3758 let numbered = format!("{candidate}_{n}");
3759 match self.resolved_cache.get(&numbered) {
3760 None => {
3761 found = Some((numbered, true));
3762 break;
3763 }
3764 Some(existing) if matches_values(existing, &enum_values) => {
3765 found = Some((numbered, false));
3766 break;
3767 }
3768 Some(_) => continue,
3769 }
3770 }
3771 found
3772 }
3773 };
3774
3775 let (resolved_name, insert) = resolved.unwrap_or((candidate, true));
3776 enum_type_name = resolved_name;
3777 insert
3778 }
3779 };
3780
3781 if should_insert {
3784 self.resolved_cache.insert(
3785 enum_type_name.clone(),
3786 AnalyzedSchema {
3787 name: enum_type_name.clone(),
3788 original: serde_json::to_value(schema).unwrap_or(Value::Null),
3789 schema_type: SchemaType::StringEnum {
3790 values: enum_values,
3791 },
3792 dependencies: HashSet::new(),
3793 nullable: false,
3794 description: schema.details().description.clone(),
3795 default: schema.details().default.clone(),
3796 },
3797 );
3798 }
3799
3800 dependencies.insert(enum_type_name.clone());
3802 SchemaType::Reference {
3803 target: enum_type_name,
3804 }
3805 }
3806
3807 fn analyze_array_schema(
3808 &mut self,
3809 schema: &Schema,
3810 parent_schema_name: &str,
3811 dependencies: &mut HashSet<String>,
3812 ) -> Result<SchemaType> {
3813 let details = schema.details();
3814
3815 if let Some(items_schema) = &details.items {
3817 let item_type = match items_schema.as_ref() {
3819 Schema::Reference { reference, .. } => {
3820 let target = self
3822 .extract_schema_name(reference)
3823 .ok_or_else(|| GeneratorError::UnresolvedReference(reference.to_string()))?
3824 .to_string();
3825 dependencies.insert(target.clone());
3826 SchemaType::Reference { target }
3827 }
3828 Schema::RecursiveRef { recursive_ref, .. } => {
3829 if recursive_ref == "#" {
3831 let target = self
3833 .find_recursive_anchor_schema()
3834 .unwrap_or_else(|| parent_schema_name.to_string());
3835 dependencies.insert(target.clone());
3836 SchemaType::Reference { target }
3837 } else {
3838 let target = self
3839 .extract_schema_name(recursive_ref)
3840 .unwrap_or("RecursiveType")
3841 .to_string();
3842 dependencies.insert(target.clone());
3843 SchemaType::Reference { target }
3844 }
3845 }
3846 Schema::Typed { schema_type, .. } => {
3847 match schema_type {
3849 OpenApiSchemaType::String => {
3850 match items_schema
3854 .details()
3855 .string_enum_values()
3856 .filter(|values| !values.is_empty())
3857 {
3858 Some(values) => self.hoist_inline_string_enum(
3859 items_schema,
3860 values,
3861 format!("{parent_schema_name}Item"),
3862 dependencies,
3863 ),
3864 None => SchemaType::Primitive {
3865 rust_type: "String".to_string(),
3866 serde_with: None,
3867 },
3868 }
3869 }
3870 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
3871 let details = items_schema.details();
3872 let rust_type = self.get_number_rust_type(schema_type.clone(), details);
3873 SchemaType::Primitive {
3874 rust_type,
3875 serde_with: None,
3876 }
3877 }
3878 OpenApiSchemaType::Boolean => SchemaType::Primitive {
3879 rust_type: "bool".to_string(),
3880 serde_with: None,
3881 },
3882 OpenApiSchemaType::Object => {
3883 let object_type_name = format!("{parent_schema_name}Item");
3885
3886 let object_type =
3888 self.analyze_object_schema(items_schema, dependencies)?;
3889
3890 let inline_schema = AnalyzedSchema {
3892 name: object_type_name.clone(),
3893 original: serde_json::to_value(items_schema).unwrap_or(Value::Null),
3894 schema_type: object_type,
3895 dependencies: dependencies.clone(),
3896 nullable: false,
3897 description: items_schema.details().description.clone(),
3898 default: None,
3899 };
3900
3901 self.resolved_cache
3903 .insert(object_type_name.clone(), inline_schema);
3904 dependencies.insert(object_type_name.clone());
3905
3906 SchemaType::Reference {
3908 target: object_type_name,
3909 }
3910 }
3911 OpenApiSchemaType::Array => {
3912 self.analyze_array_schema(
3914 items_schema,
3915 parent_schema_name,
3916 dependencies,
3917 )?
3918 }
3919 _ => SchemaType::Primitive {
3920 rust_type: "serde_json::Value".to_string(),
3921 serde_with: None,
3922 },
3923 }
3924 }
3925 Schema::OneOf { .. } | Schema::AnyOf { .. } => {
3926 let analyzed = self.analyze_schema_value(items_schema, "ArrayItem")?;
3928
3929 match &analyzed.schema_type {
3931 SchemaType::DiscriminatedUnion { .. } | SchemaType::Union { .. } => {
3932 let union_name = format!("{parent_schema_name}ItemUnion");
3935
3936 let mut union_schema = analyzed;
3938 union_schema.name = union_name.clone();
3939
3940 self.resolved_cache.insert(union_name.clone(), union_schema);
3942
3943 dependencies.insert(union_name.clone());
3945
3946 SchemaType::Reference { target: union_name }
3948 }
3949 _ => analyzed.schema_type,
3950 }
3951 }
3952 Schema::Untyped { .. } => {
3953 if let Some(inferred) = items_schema.inferred_type() {
3955 match inferred {
3956 OpenApiSchemaType::Object => {
3957 let object_type_name = format!("{parent_schema_name}Item");
3959
3960 let object_type =
3962 self.analyze_object_schema(items_schema, dependencies)?;
3963
3964 let inline_schema = AnalyzedSchema {
3966 name: object_type_name.clone(),
3967 original: serde_json::to_value(items_schema)
3968 .unwrap_or(Value::Null),
3969 schema_type: object_type,
3970 dependencies: dependencies.clone(),
3971 nullable: false,
3972 description: items_schema.details().description.clone(),
3973 default: None,
3974 };
3975
3976 self.resolved_cache
3978 .insert(object_type_name.clone(), inline_schema);
3979 dependencies.insert(object_type_name.clone());
3980
3981 SchemaType::Reference {
3983 target: object_type_name,
3984 }
3985 }
3986 OpenApiSchemaType::String => {
3987 match items_schema
3990 .details()
3991 .string_enum_values()
3992 .filter(|values| !values.is_empty())
3993 {
3994 Some(values) => self.hoist_inline_string_enum(
3995 items_schema,
3996 values,
3997 format!("{parent_schema_name}Item"),
3998 dependencies,
3999 ),
4000 None => SchemaType::Primitive {
4001 rust_type: "String".to_string(),
4002 serde_with: None,
4003 },
4004 }
4005 }
4006 OpenApiSchemaType::Integer | OpenApiSchemaType::Number => {
4007 let details = items_schema.details();
4008 let rust_type = self.get_number_rust_type(inferred, details);
4009 SchemaType::Primitive {
4010 rust_type,
4011 serde_with: None,
4012 }
4013 }
4014 OpenApiSchemaType::Boolean => SchemaType::Primitive {
4015 rust_type: "bool".to_string(),
4016 serde_with: None,
4017 },
4018 _ => SchemaType::Primitive {
4019 rust_type: "serde_json::Value".to_string(),
4020 serde_with: None,
4021 },
4022 }
4023 } else {
4024 SchemaType::Primitive {
4025 rust_type: "serde_json::Value".to_string(),
4026 serde_with: None,
4027 }
4028 }
4029 }
4030 _ => SchemaType::Primitive {
4031 rust_type: "serde_json::Value".to_string(),
4032 serde_with: None,
4033 },
4034 };
4035
4036 Ok(SchemaType::Array {
4037 item_type: Box::new(item_type),
4038 })
4039 } else {
4040 Ok(SchemaType::Primitive {
4042 rust_type: "Vec<serde_json::Value>".to_string(),
4043 serde_with: None,
4044 })
4045 }
4046 }
4047
4048 fn get_number_rust_type(
4049 &self,
4050 schema_type: OpenApiSchemaType,
4051 details: &crate::openapi::SchemaDetails,
4052 ) -> String {
4053 let format = details.format.as_deref();
4057 match schema_type {
4058 OpenApiSchemaType::Integer => self.type_mapper.integer_format(format).rust_type,
4059 OpenApiSchemaType::Number => self.type_mapper.number_format(format).rust_type,
4060 _ => self.type_mapper.dynamic_json().rust_type,
4061 }
4062 }
4063
4064 fn analyze_anyof_union(
4065 &mut self,
4066 any_of_schemas: &[Schema],
4067 discriminator: Option<&Discriminator>,
4068 dependencies: &mut HashSet<String>,
4069 context_name: &str,
4070 ) -> Result<SchemaType> {
4071 let filtered_owned: Vec<Schema>;
4076 let any_of_schemas: &[Schema] = if any_of_schemas
4077 .iter()
4078 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4079 {
4080 filtered_owned = any_of_schemas
4081 .iter()
4082 .filter(|s| !matches!(s.schema_type(), Some(OpenApiSchemaType::Null)))
4083 .cloned()
4084 .collect();
4085 if filtered_owned.is_empty() {
4086 return Ok(SchemaType::Primitive {
4087 rust_type: "serde_json::Value".to_string(),
4088 serde_with: None,
4089 });
4090 }
4091 if filtered_owned.len() == 1 {
4092 return self
4093 .analyze_schema_value(&filtered_owned[0], context_name)
4094 .map(|a| a.schema_type);
4095 }
4096 &filtered_owned
4097 } else {
4098 any_of_schemas
4099 };
4100
4101 let has_refs = any_of_schemas.iter().any(|s| s.is_reference());
4103 let has_objects = any_of_schemas.iter().any(|s| {
4104 matches!(s.schema_type(), Some(OpenApiSchemaType::Object))
4105 || s.inferred_type() == Some(OpenApiSchemaType::Object)
4106 });
4107 let has_arrays = any_of_schemas
4108 .iter()
4109 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Array)));
4110
4111 let all_string_like = any_of_schemas.iter().all(|s| {
4114 matches!(s.schema_type(), Some(OpenApiSchemaType::String))
4115 || s.details().const_value.is_some()
4116 });
4117
4118 if (has_refs || has_objects || has_arrays || any_of_schemas.len() > 1) && !all_string_like {
4119 if let Some(disc) = discriminator {
4121 return self.analyze_oneof_union(
4123 any_of_schemas,
4124 Some(disc),
4125 context_name,
4126 dependencies,
4127 );
4128 }
4129
4130 if let Some(disc_field) = self.detect_discriminator_field(any_of_schemas) {
4132 return self.analyze_oneof_union(
4133 any_of_schemas,
4134 Some(&Discriminator {
4135 property_name: disc_field,
4136 mapping: None,
4137 default_mapping: None,
4138 extensions: crate::extensions::Extensions::default(),
4139 }),
4140 context_name,
4141 dependencies,
4142 );
4143 }
4144
4145 let mut variants = Vec::new();
4147
4148 for schema in any_of_schemas {
4149 if let Some(ref_str) = schema.reference() {
4150 if let Some(target) = self.extract_schema_name(ref_str) {
4151 dependencies.insert(target.to_string());
4152 variants.push(SchemaRef {
4153 target: target.to_string(),
4154 nullable: false,
4155 });
4156 }
4157 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Object))
4158 || schema.inferred_type() == Some(OpenApiSchemaType::Object)
4159 {
4160 let inline_index = variants.len();
4162 let inline_type_name = self.generate_inline_type_name(schema, inline_index);
4163
4164 self.add_inline_schema(&inline_type_name, schema, dependencies)?;
4166
4167 variants.push(SchemaRef {
4168 target: inline_type_name,
4169 nullable: false,
4170 });
4171 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::Array)) {
4172 let array_type =
4174 self.analyze_array_schema(schema, context_name, dependencies)?;
4175
4176 let array_type_name = if let Some(items_schema) = &schema.details().items {
4178 if let Some(ref_str) = items_schema.reference() {
4179 if let Some(item_type_name) = self.extract_schema_name(ref_str) {
4180 dependencies.insert(item_type_name.to_string());
4181 format!("{item_type_name}Array")
4182 } else {
4183 self.generate_context_aware_name(
4184 context_name,
4185 "Array",
4186 variants.len(),
4187 Some(schema),
4188 )
4189 }
4190 } else {
4191 self.generate_context_aware_name(
4192 context_name,
4193 "Array",
4194 variants.len(),
4195 Some(schema),
4196 )
4197 }
4198 } else {
4199 self.generate_context_aware_name(
4200 context_name,
4201 "Array",
4202 variants.len(),
4203 Some(schema),
4204 )
4205 };
4206
4207 self.resolved_cache.insert(
4209 array_type_name.clone(),
4210 AnalyzedSchema {
4211 name: array_type_name.clone(),
4212 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4213 schema_type: array_type,
4214 dependencies: HashSet::new(),
4215 nullable: false,
4216 description: Some("Array variant in union".to_string()),
4217 default: None,
4218 },
4219 );
4220
4221 dependencies.insert(array_type_name.clone());
4223
4224 variants.push(SchemaRef {
4225 target: array_type_name,
4226 nullable: false,
4227 });
4228 } else if let Some(schema_type) = schema.schema_type() {
4229 let primitive_unions = self
4239 .type_mapper
4240 .config_shape_primitive_unions()
4241 .unwrap_or(true);
4242
4243 if primitive_unions {
4244 let mapped = self.type_mapper.map(schema_type.clone(), schema.details());
4245 variants.push(SchemaRef {
4246 target: mapped.rust_type,
4247 nullable: false,
4248 });
4249 } else {
4250 let inline_index = variants.len();
4251 let inline_type_name = match schema_type {
4252 OpenApiSchemaType::String => {
4253 if inline_index == 0 {
4254 format!("{context_name}String")
4255 } else {
4256 format!("{context_name}StringVariant{inline_index}")
4257 }
4258 }
4259 OpenApiSchemaType::Number => {
4260 if inline_index == 0 {
4261 format!("{context_name}Number")
4262 } else {
4263 format!("{context_name}NumberVariant{inline_index}")
4264 }
4265 }
4266 OpenApiSchemaType::Integer => {
4267 if inline_index == 0 {
4268 format!("{context_name}Integer")
4269 } else {
4270 format!("{context_name}IntegerVariant{inline_index}")
4271 }
4272 }
4273 OpenApiSchemaType::Boolean => {
4274 if inline_index == 0 {
4275 format!("{context_name}Boolean")
4276 } else {
4277 format!("{context_name}BooleanVariant{inline_index}")
4278 }
4279 }
4280 _ => format!("{context_name}Variant{inline_index}"),
4281 };
4282
4283 let rust_type =
4284 self.openapi_type_to_rust_type(schema_type.clone(), schema.details());
4285
4286 self.resolved_cache.insert(
4287 inline_type_name.clone(),
4288 AnalyzedSchema {
4289 name: inline_type_name.clone(),
4290 original: serde_json::to_value(schema).unwrap_or(Value::Null),
4291 schema_type: SchemaType::Primitive {
4292 rust_type,
4293 serde_with: None,
4294 },
4295 dependencies: HashSet::new(),
4296 nullable: false,
4297 description: schema.details().description.clone(),
4298 default: None,
4299 },
4300 );
4301
4302 dependencies.insert(inline_type_name.clone());
4303
4304 variants.push(SchemaRef {
4305 target: inline_type_name,
4306 nullable: false,
4307 });
4308 }
4309 }
4310 }
4311
4312 if !variants.is_empty() {
4313 return Ok(SchemaType::Union { variants });
4314 }
4315 }
4316
4317 let all_strings = any_of_schemas.iter().all(|schema| {
4319 matches!(schema.schema_type(), Some(OpenApiSchemaType::String))
4320 || schema.details().const_value.is_some()
4321 });
4322
4323 if all_strings {
4324 let mut enum_values = Vec::new();
4326 let mut has_open_string = false;
4327
4328 for schema in any_of_schemas {
4329 if let Some(const_val) = &schema.details().const_value {
4330 if let Some(const_str) = const_val.as_str() {
4331 enum_values.push(const_str.to_string());
4332 }
4333 } else if matches!(schema.schema_type(), Some(OpenApiSchemaType::String)) {
4334 has_open_string = true;
4335 }
4336 }
4337
4338 if !enum_values.is_empty() {
4339 if has_open_string {
4340 return Ok(SchemaType::ExtensibleEnum {
4343 known_values: enum_values,
4344 });
4345 } else {
4346 return Ok(SchemaType::StringEnum {
4348 values: enum_values,
4349 });
4350 }
4351 }
4352 }
4353
4354 Ok(SchemaType::Primitive {
4356 rust_type: "serde_json::Value".to_string(),
4357 serde_with: None,
4358 })
4359 }
4360
4361 fn find_recursive_anchor_schema(&self) -> Option<String> {
4363 for (schema_name, schema) in &self.schemas {
4365 let details = schema.details();
4366 if details.recursive_anchor == Some(true) {
4367 return Some(schema_name.clone());
4368 }
4369 }
4370
4371 None
4375 }
4376
4377 fn should_use_dynamic_json(&self, schema: &Schema) -> bool {
4380 if let Schema::AnyOf { any_of, .. } = schema {
4382 if any_of.len() == 2 {
4383 let has_null = any_of
4384 .iter()
4385 .any(|s| matches!(s.schema_type(), Some(OpenApiSchemaType::Null)));
4386 let has_empty_object = any_of.iter().any(|s| self.is_dynamic_object_pattern(s));
4387
4388 if has_null && has_empty_object {
4389 return true;
4390 }
4391 }
4392 }
4393
4394 self.is_dynamic_object_pattern(schema)
4396 }
4397
4398 fn is_dynamic_object_pattern(&self, schema: &Schema) -> bool {
4400 let is_object = match schema.schema_type() {
4402 Some(OpenApiSchemaType::Object) => true,
4403 None => schema.inferred_type() == Some(OpenApiSchemaType::Object),
4404 _ => false,
4405 };
4406
4407 if !is_object {
4408 return false;
4409 }
4410
4411 let details = schema.details();
4412
4413 if self.has_explicit_additional_properties(schema) {
4416 return false;
4417 }
4418
4419 let no_properties = details
4421 .properties
4422 .as_ref()
4423 .map(|props| props.is_empty())
4424 .unwrap_or(true);
4425
4426 if no_properties {
4427 let has_structural_constraints = details
4430 .required
4431 .as_ref()
4432 .map(|req| req.iter().any(|r| r != "type"))
4433 .unwrap_or(false)
4434 || details.pattern_properties.is_some()
4435 || details.property_names.is_some()
4436 || details.min_properties.is_some()
4437 || details.max_properties.is_some()
4438 || details.dependent_required.is_some()
4439 || details.dependent_schemas.is_some()
4440 || details.if_schema.is_some()
4441 || details.then_schema.is_some()
4442 || details.else_schema.is_some();
4443
4444 return !has_structural_constraints;
4445 }
4446
4447 false
4448 }
4449
4450 fn has_explicit_additional_properties(&self, schema: &Schema) -> bool {
4452 let details = schema.details();
4453
4454 matches!(
4456 &details.additional_properties,
4457 Some(crate::openapi::AdditionalProperties::Boolean(true))
4458 | Some(crate::openapi::AdditionalProperties::Schema(_))
4459 )
4460 }
4461
4462 fn analyze_operations(&mut self, analysis: &mut SchemaAnalysis) -> Result<()> {
4464 let spec: crate::openapi::OpenApiSpec = serde_json::from_value(self.openapi_spec.clone())
4465 .map_err(GeneratorError::ParseError)?;
4466 let mut canonical_operation_ids = HashSet::new();
4471
4472 if let Some(paths) = &spec.paths {
4473 for (path, path_item) in paths {
4474 let resolved = self.resolve_path_item(path_item, &spec)?;
4476 let pi: &crate::openapi::PathItem = resolved.as_ref().unwrap_or(path_item);
4477 self.ingest_path_item_operations(path, pi, analysis, &mut canonical_operation_ids)?;
4478 }
4479 }
4480 if let Some(webhooks) = &spec.webhooks {
4487 for (name, path_item) in webhooks {
4488 let synthetic_path = format!("/__webhook__/{name}");
4489 self.ingest_path_item_operations(
4490 &synthetic_path,
4491 path_item,
4492 analysis,
4493 &mut canonical_operation_ids,
4494 )?;
4495 }
4496 }
4497 Ok(())
4498 }
4499
4500 fn resolve_path_item(
4504 &self,
4505 path_item: &crate::openapi::PathItem,
4506 spec: &crate::openapi::OpenApiSpec,
4507 ) -> Result<Option<crate::openapi::PathItem>> {
4508 let Some(reference) = &path_item.reference else {
4509 return Ok(None);
4510 };
4511 let target_name = reference
4512 .strip_prefix("#/components/pathItems/")
4513 .ok_or_else(|| {
4514 GeneratorError::UnresolvedReference(format!(
4515 "Path Item $ref must point at #/components/pathItems/{{name}}, got {reference}"
4516 ))
4517 })?;
4518 let pi = spec
4519 .components
4520 .as_ref()
4521 .and_then(|c| c.path_items.as_ref())
4522 .and_then(|map| map.get(target_name))
4523 .ok_or_else(|| {
4524 GeneratorError::UnresolvedReference(format!(
4525 "Path Item ref {reference} not found in components/pathItems"
4526 ))
4527 })?;
4528 Ok(Some(pi.clone()))
4529 }
4530
4531 fn ingest_path_item_operations(
4532 &mut self,
4533 path: &str,
4534 path_item: &crate::openapi::PathItem,
4535 analysis: &mut SchemaAnalysis,
4536 canonical_operation_ids: &mut HashSet<String>,
4537 ) -> Result<()> {
4538 for (method, operation) in path_item.operations() {
4539 let raw_operation_id = operation
4541 .operation_id
4542 .clone()
4543 .unwrap_or_else(|| Self::generate_operation_id(method, path));
4544
4545 let operation_id = if canonical_operation_ids
4556 .contains(&Self::canonical_operation_id(&raw_operation_id))
4557 {
4558 let method_lower = method.to_lowercase();
4559 let mut candidate = format!("{}_{}", raw_operation_id, method_lower);
4560 let mut suffix = 2;
4561 while canonical_operation_ids.contains(&Self::canonical_operation_id(&candidate)) {
4562 candidate = format!("{}_{}_{}", raw_operation_id, method_lower, suffix);
4563 suffix += 1;
4564 }
4565 eprintln!(
4566 "⚠️ duplicate operationId `{}` at `{} {}` — disambiguated to `{}`",
4567 raw_operation_id, method, path, candidate
4568 );
4569 candidate
4570 } else {
4571 raw_operation_id.clone()
4572 };
4573
4574 let (op_info, responses) = self.analyze_single_operation(
4575 &operation_id,
4576 method,
4577 path,
4578 operation,
4579 path_item.parameters.as_ref(),
4580 analysis,
4581 )?;
4582 analysis
4583 .operation_id_aliases
4584 .entry(raw_operation_id)
4585 .or_default()
4586 .push(operation_id.clone());
4587 canonical_operation_ids.insert(Self::canonical_operation_id(&operation_id));
4588 analysis
4589 .operation_responses
4590 .insert(operation_id.clone(), responses);
4591 analysis.operations.insert(operation_id, op_info);
4592 }
4593 Ok(())
4594 }
4595
4596 fn canonical_operation_id(operation_id: &str) -> String {
4597 use heck::ToPascalCase;
4598 operation_id.replace('.', "_").to_pascal_case()
4599 }
4600
4601 fn generate_operation_id(method: &str, path: &str) -> String {
4604 let mut operation_id = method.to_lowercase();
4606
4607 let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
4609
4610 for part in path_parts {
4611 if part.is_empty() {
4612 continue;
4613 }
4614
4615 let cleaned_part = if part.starts_with('{') && part.ends_with('}') {
4617 &part[1..part.len() - 1]
4618 } else {
4619 part
4620 };
4621
4622 let pascal_case_part = cleaned_part
4624 .split(&['-', '_'][..])
4625 .map(|s| {
4626 let mut chars = s.chars();
4627 match chars.next() {
4628 None => String::new(),
4629 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4630 }
4631 })
4632 .collect::<String>();
4633
4634 operation_id.push_str(&pascal_case_part);
4635 }
4636
4637 operation_id
4638 }
4639
4640 fn analyze_single_operation(
4642 &mut self,
4643 operation_id: &str,
4644 method: &str,
4645 path: &str,
4646 operation: &crate::openapi::Operation,
4647 path_item_parameters: Option<&Vec<crate::openapi::Parameter>>,
4648 _analysis: &mut SchemaAnalysis,
4649 ) -> Result<(OperationInfo, BTreeMap<String, OperationResponse>)> {
4650 let raw_path_item = self
4651 .openapi_spec
4652 .get("paths")
4653 .and_then(|paths| paths.get(path))
4654 .cloned();
4655 let raw_operation = raw_path_item
4656 .as_ref()
4657 .and_then(|path_item| path_item.get(method.to_ascii_lowercase()))
4658 .cloned();
4659 let request_body = operation
4660 .request_body
4661 .as_ref()
4662 .map(|request_body| self.resolve_request_body(request_body))
4663 .transpose()?;
4664 let mut op_info = OperationInfo {
4665 operation_id: operation_id.to_string(),
4666 method: method.to_uppercase(),
4667 path: normalize_operation_path(path),
4668 summary: operation.summary.clone(),
4669 description: operation.description.clone(),
4670 request_body: None,
4671 request_body_required: request_body
4673 .as_ref()
4674 .and_then(|rb| rb.required)
4675 .unwrap_or(false),
4676 response_schemas: BTreeMap::new(),
4677 parameters: Vec::new(),
4678 supports_streaming: false, stream_parameter: None, tags: operation.tags.clone().unwrap_or_default(),
4681 };
4682 let mut operation_responses = BTreeMap::new();
4683
4684 if let Some(request_body) = &request_body {
4686 use crate::openapi::{
4687 is_binary_media_type, is_form_urlencoded_media_type, is_json_media_type,
4688 media_type_essence,
4689 };
4690 if let Some((content_type, maybe_schema)) = request_body.best_content() {
4691 op_info.request_body = if is_json_media_type(content_type) {
4692 match maybe_schema {
4693 Some(s) => {
4694 let validation_schema = self
4695 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4696 .unwrap_or(
4697 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4698 );
4699 Some(
4700 self.resolve_or_inline_schema(s, operation_id, "Request")
4701 .map(|name| RequestBodyContent::Json {
4702 schema_name: name,
4703 media_type: content_type.to_string(),
4704 validation_schema,
4705 })?,
4706 )
4707 }
4708 None => Some(RequestBodyContent::SchemaLess {
4709 media_type: content_type.to_string(),
4710 }),
4711 }
4712 } else if is_form_urlencoded_media_type(content_type) {
4713 match maybe_schema {
4714 Some(s) => {
4715 let validation_schema = self
4716 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4717 .unwrap_or(
4718 serde_json::to_value(s).map_err(GeneratorError::ParseError)?,
4719 );
4720 Some(
4721 self.resolve_or_inline_schema(s, operation_id, "Request")
4722 .map(|name| RequestBodyContent::FormUrlEncoded {
4723 schema_name: name,
4724 media_type: content_type.to_string(),
4725 validation_schema,
4726 })?,
4727 )
4728 }
4729 None => Some(RequestBodyContent::SchemaLess {
4730 media_type: content_type.to_string(),
4731 }),
4732 }
4733 } else if media_type_essence(content_type)
4734 .eq_ignore_ascii_case("multipart/form-data")
4735 {
4736 match maybe_schema {
4737 Some(schema) => {
4738 let validation_schema = self
4739 .raw_request_body_schema(raw_operation.as_ref(), content_type)
4740 .unwrap_or(
4741 serde_json::to_value(schema)
4742 .map_err(GeneratorError::ParseError)?,
4743 );
4744 Some(
4745 self.resolve_or_inline_schema(schema, operation_id, "Request")
4746 .map(|schema_name| RequestBodyContent::Multipart {
4747 schema_name,
4748 media_type: content_type.to_string(),
4749 validation_schema,
4750 })?,
4751 )
4752 }
4753 None => Some(RequestBodyContent::SchemaLess {
4754 media_type: content_type.to_string(),
4755 }),
4756 }
4757 } else if is_binary_media_type(content_type, maybe_schema) {
4758 if media_type_essence(content_type)
4759 .eq_ignore_ascii_case("application/octet-stream")
4760 {
4761 Some(RequestBodyContent::OctetStream {
4762 media_type: content_type.to_string(),
4763 })
4764 } else {
4765 Some(RequestBodyContent::Binary {
4766 media_type: content_type.to_string(),
4767 })
4768 }
4769 } else if crate::openapi::is_text_media_type(content_type) {
4770 Some(RequestBodyContent::TextPlain {
4775 media_type: content_type.to_string(),
4776 })
4777 } else {
4778 None
4779 };
4780 }
4781 if op_info.request_body.is_none() {
4782 let mut media_types = request_body
4783 .content
4784 .as_ref()
4785 .map(|content| content.keys().cloned().collect::<Vec<_>>())
4786 .unwrap_or_default();
4787 media_types.sort();
4788 if !media_types.is_empty() {
4789 op_info.request_body = Some(RequestBodyContent::Unsupported { media_types });
4790 }
4791 }
4792 }
4793
4794 if let Some(responses) = &operation.responses {
4796 for (status_code, response) in responses {
4797 let response = self.resolve_response(response)?;
4798 let supports_streaming = response.content.as_ref().is_some_and(|content| {
4804 content
4805 .keys()
4806 .any(|ct| crate::openapi::is_event_stream_media_type(ct))
4807 });
4808 if supports_streaming {
4809 op_info.supports_streaming = true;
4810 }
4811
4812 let mut response_info = OperationResponse {
4813 supports_streaming,
4814 has_content: response
4815 .content
4816 .as_ref()
4817 .is_some_and(|content| !content.is_empty()),
4818 ..Default::default()
4819 };
4820 if let Some((media_type, schema)) = response.json_content() {
4821 if let Some(schema_ref) = schema.reference() {
4822 if let Some(schema_name) = self.extract_schema_name(schema_ref) {
4824 op_info
4825 .response_schemas
4826 .insert(status_code.clone(), schema_name.to_string());
4827 response_info.schema_name = Some(schema_name.to_string());
4828 response_info.media_type = Some(media_type.to_string());
4829 response_info.body = Some(OperationResponseBody::Json {
4830 schema_name: schema_name.to_string(),
4831 media_type: media_type.to_string(),
4832 });
4833 }
4834 } else {
4835 let synthetic_name =
4837 self.generate_inline_response_type_name(operation_id, status_code);
4838
4839 let mut deps = HashSet::new();
4841 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
4842
4843 op_info
4844 .response_schemas
4845 .insert(status_code.clone(), synthetic_name.clone());
4846 response_info.body = Some(OperationResponseBody::Json {
4847 schema_name: synthetic_name.clone(),
4848 media_type: media_type.to_string(),
4849 });
4850 response_info.schema_name = Some(synthetic_name);
4851 response_info.media_type = Some(media_type.to_string());
4852 }
4853 }
4854 if response_info.body.is_none()
4855 && let Some(content) = response.content.as_ref()
4856 {
4857 let selected = content
4858 .iter()
4859 .find(|(media_type, media)| {
4860 matches!(
4861 crate::openapi::classify_response_media_type(
4862 media_type,
4863 media.schema.as_ref()
4864 ),
4865 crate::openapi::ResponseMediaKind::Text
4866 )
4867 })
4868 .or_else(|| {
4869 content.iter().find(|(media_type, media)| {
4870 matches!(
4871 crate::openapi::classify_response_media_type(
4872 media_type,
4873 media.schema.as_ref()
4874 ),
4875 crate::openapi::ResponseMediaKind::Binary
4876 ) && !crate::openapi::is_wildcard_media_type(media_type)
4877 })
4878 })
4879 .or_else(|| {
4880 content.iter().find(|(media_type, media)| {
4881 matches!(
4882 crate::openapi::classify_response_media_type(
4883 media_type,
4884 media.schema.as_ref()
4885 ),
4886 crate::openapi::ResponseMediaKind::Binary
4887 )
4888 })
4889 });
4890 if let Some((media_type, media)) = selected {
4891 response_info.body = match crate::openapi::classify_response_media_type(
4892 media_type,
4893 media.schema.as_ref(),
4894 ) {
4895 crate::openapi::ResponseMediaKind::Text => {
4896 Some(OperationResponseBody::Text {
4897 media_type: media_type.clone(),
4898 })
4899 }
4900 crate::openapi::ResponseMediaKind::Binary => {
4901 Some(OperationResponseBody::Binary {
4902 media_type: media_type.clone(),
4903 wildcard: crate::openapi::is_wildcard_media_type(media_type),
4904 })
4905 }
4906 _ => None,
4907 };
4908 }
4909 }
4910 response_info.unsupported_media_types = response
4911 .content
4912 .as_ref()
4913 .into_iter()
4914 .flat_map(|content| content.iter())
4915 .filter(|(media_type, content)| {
4916 match crate::openapi::classify_response_media_type(
4917 media_type,
4918 content.schema.as_ref(),
4919 ) {
4920 crate::openapi::ResponseMediaKind::Json => content.schema.is_none(),
4921 crate::openapi::ResponseMediaKind::Unsupported => true,
4922 crate::openapi::ResponseMediaKind::EventStream
4923 | crate::openapi::ResponseMediaKind::Text
4924 | crate::openapi::ResponseMediaKind::Binary => false,
4925 }
4926 })
4927 .map(|(media_type, _)| media_type.clone())
4928 .collect();
4929 operation_responses.insert(status_code.clone(), response_info);
4930 }
4931 }
4932
4933 if op_info.supports_streaming
4936 && let Some(parameters) = &operation.parameters
4937 {
4938 for param in parameters {
4939 if let Some(name) = param.name.as_deref() {
4940 if name.eq_ignore_ascii_case("stream") {
4941 op_info.stream_parameter = Some(name.to_string());
4942 break;
4943 }
4944 }
4945 }
4946 }
4947
4948 if let Some(parameters) = &operation.parameters {
4950 for (index, param) in parameters.iter().enumerate() {
4951 let resolved = self.resolve_parameter(param).into_owned();
4955 let validation_schema = raw_operation
4956 .as_ref()
4957 .and_then(|operation| operation.get("parameters"))
4958 .and_then(Value::as_array)
4959 .and_then(|parameters| parameters.get(index))
4960 .and_then(|parameter| self.raw_parameter_schema(parameter));
4961 if let Some(param_info) =
4962 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4963 {
4964 op_info.parameters.push(param_info);
4965 }
4966 }
4967 }
4968
4969 if let Some(path_params) = path_item_parameters {
4971 let existing_keys: std::collections::HashSet<(String, String)> = op_info
4972 .parameters
4973 .iter()
4974 .map(|p| (p.name.clone(), p.location.clone()))
4975 .collect();
4976 for (index, param) in path_params.iter().enumerate() {
4977 let resolved = self.resolve_parameter(param).into_owned();
4978 let validation_schema = raw_path_item
4979 .as_ref()
4980 .and_then(|path_item| path_item.get("parameters"))
4981 .and_then(Value::as_array)
4982 .and_then(|parameters| parameters.get(index))
4983 .and_then(|parameter| self.raw_parameter_schema(parameter));
4984 if let Some(param_info) =
4985 self.analyze_parameter(&resolved, operation_id, validation_schema)?
4986 {
4987 if !existing_keys
4988 .contains(&(param_info.name.clone(), param_info.location.clone()))
4989 {
4990 op_info.parameters.push(param_info);
4991 }
4992 }
4993 }
4994 }
4995
4996 let mut declared_path_names: std::collections::HashSet<String> = op_info
5004 .parameters
5005 .iter()
5006 .filter(|p| p.location == "path")
5007 .map(|p| p.name.clone())
5008 .collect();
5009 let bytes = path.as_bytes().iter();
5010 let mut current = String::new();
5011 let mut in_brace = false;
5012 let mut synthesized: Vec<String> = Vec::new();
5013 for b in bytes {
5014 match *b {
5015 b'{' => {
5016 in_brace = true;
5017 current.clear();
5018 }
5019 b'}' if in_brace => {
5020 in_brace = false;
5021 if !current.is_empty() && !declared_path_names.contains(¤t) {
5022 synthesized.push(current.clone());
5023 declared_path_names.insert(current.clone());
5024 }
5025 }
5026 _ if in_brace => current.push(*b as char),
5027 _ => {}
5028 }
5029 }
5030 for name in synthesized {
5031 eprintln!(
5032 "⚠️ path `{}` references `{{{}}}` but the spec doesn't declare it as a parameter — synthesizing as required String",
5033 path, name
5034 );
5035 op_info.parameters.push(ParameterInfo {
5036 name,
5037 location: "path".to_string(),
5038 required: true,
5039 schema_ref: None,
5040 rust_type: "String".to_string(),
5041 description: None,
5042 enum_values: None,
5043 enum_varnames: None,
5044 rust_ident: None,
5045 query_serialization: None,
5046 validation_schema: None,
5047 });
5048 }
5049
5050 let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
5058 for p in op_info.parameters.iter_mut() {
5059 let raw = base_param_ident(&p.name);
5060 let mut chosen = raw.clone();
5061 let mut suffix = 2;
5062 while !used.insert(chosen.clone()) {
5063 chosen = format!("{raw}_{suffix}");
5064 suffix += 1;
5065 }
5066 p.rust_ident = Some(chosen);
5067 }
5068
5069 Ok((op_info, operation_responses))
5070 }
5071
5072 fn resolve_request_body(
5074 &self,
5075 request_body: &crate::openapi::RequestBody,
5076 ) -> Result<crate::openapi::RequestBody> {
5077 let mut current = request_body.clone();
5078 let mut visited = HashSet::new();
5079 while let Some(reference) = current.reference.clone() {
5080 if !visited.insert(reference.clone()) {
5081 return Err(GeneratorError::CircularDependency(format!(
5082 "request body reference {reference}"
5083 )));
5084 }
5085
5086 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5087 GeneratorError::UnresolvedReference(format!(
5088 "external request body reference `{reference}` is not supported"
5089 ))
5090 })?;
5091 if !pointer.is_empty() && !pointer.starts_with('/') {
5092 return Err(GeneratorError::UnresolvedReference(format!(
5093 "request body reference `{reference}` is not a local JSON Pointer"
5094 )));
5095 }
5096 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5097 GeneratorError::UnresolvedReference(format!(
5098 "request body reference `{reference}` does not exist"
5099 ))
5100 })?;
5101 let object = value.as_object().ok_or_else(|| {
5102 GeneratorError::InvalidSchema(format!(
5103 "request body reference `{reference}` must target an object"
5104 ))
5105 })?;
5106 if !["$ref", "description", "required", "content"]
5107 .iter()
5108 .any(|field| object.contains_key(*field))
5109 {
5110 return Err(GeneratorError::InvalidSchema(format!(
5111 "request body reference `{reference}` does not target a structurally compatible OpenAPI Request Body Object"
5112 )));
5113 }
5114 current = serde_json::from_value(value.clone()).map_err(|error| {
5115 GeneratorError::InvalidSchema(format!(
5116 "request body reference `{reference}` is not a valid OpenAPI Request Body Object: {error}"
5117 ))
5118 })?;
5119 }
5120 Ok(current)
5121 }
5122
5123 fn resolve_response(
5130 &self,
5131 response: &crate::openapi::Response,
5132 ) -> Result<crate::openapi::Response> {
5133 let mut current = response.clone();
5134 let mut visited = HashSet::new();
5135 while let Some(reference) = current.reference.clone() {
5136 if !visited.insert(reference.clone()) {
5137 return Err(GeneratorError::CircularDependency(format!(
5138 "response reference {reference}"
5139 )));
5140 }
5141
5142 let pointer = reference.strip_prefix('#').ok_or_else(|| {
5143 GeneratorError::UnresolvedReference(format!(
5144 "external response reference `{reference}` is not supported"
5145 ))
5146 })?;
5147 if !pointer.is_empty() && !pointer.starts_with('/') {
5148 return Err(GeneratorError::UnresolvedReference(format!(
5149 "response reference `{reference}` is not a local JSON Pointer"
5150 )));
5151 }
5152 let value = self.openapi_spec.pointer(pointer).ok_or_else(|| {
5153 GeneratorError::UnresolvedReference(format!(
5154 "response reference `{reference}` does not exist"
5155 ))
5156 })?;
5157 let object = value.as_object().ok_or_else(|| {
5158 GeneratorError::InvalidSchema(format!(
5159 "response reference `{reference}` must target an object"
5160 ))
5161 })?;
5162 if !["$ref", "description", "headers", "content", "links"]
5163 .iter()
5164 .any(|field| object.contains_key(*field))
5165 {
5166 return Err(GeneratorError::InvalidSchema(format!(
5167 "response reference `{reference}` does not target a structurally compatible OpenAPI Response Object"
5168 )));
5169 }
5170 current = serde_json::from_value(value.clone()).map_err(|error| {
5171 GeneratorError::InvalidSchema(format!(
5172 "response reference `{reference}` is not a valid OpenAPI Response Object: {error}"
5173 ))
5174 })?;
5175 }
5176 Ok(current)
5177 }
5178
5179 fn generate_inline_response_type_name(&self, operation_id: &str, status_code: &str) -> String {
5186 use heck::ToPascalCase;
5187 let base_name = operation_id.replace('.', "_").to_pascal_case();
5188 let suffix = Self::status_code_suffix(status_code);
5189 format!("{}Response{}", base_name, suffix)
5190 }
5191
5192 fn status_code_suffix(status_code: &str) -> String {
5199 match status_code {
5200 "" | "200" => String::new(),
5201 "default" | "Default" => "Default".to_string(),
5202 other if other.chars().all(|c| c.is_ascii_digit()) => other.to_string(),
5203 other => other.to_ascii_lowercase(),
5204 }
5205 }
5206
5207 fn generate_inline_request_type_name(&self, operation_id: &str) -> String {
5209 use heck::ToPascalCase;
5210 let base_name = operation_id.replace('.', "_").to_pascal_case();
5214 format!("{}Request", base_name)
5215 }
5216
5217 fn resolve_or_inline_schema(
5220 &mut self,
5221 schema: &crate::openapi::Schema,
5222 operation_id: &str,
5223 suffix: &str,
5224 ) -> Result<String> {
5225 if let Some(schema_ref) = schema.reference()
5226 && let Some(schema_name) = self.extract_schema_name(schema_ref)
5227 {
5228 return Ok(schema_name.to_string());
5229 }
5230 let synthetic_name = if suffix == "Request" {
5232 self.generate_inline_request_type_name(operation_id)
5233 } else {
5234 self.generate_inline_response_type_name(operation_id, "")
5235 };
5236 let mut deps = HashSet::new();
5237 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5238 Ok(synthetic_name)
5239 }
5240
5241 fn resolve_parameter<'a>(
5244 &'a self,
5245 param: &'a crate::openapi::Parameter,
5246 ) -> std::borrow::Cow<'a, crate::openapi::Parameter> {
5247 if let Some(ref_str) = param.reference.as_deref() {
5248 if let Some(param_name) = ref_str.strip_prefix("#/components/parameters/") {
5249 if let Some(resolved) = self.component_parameters.get(param_name) {
5250 return std::borrow::Cow::Borrowed(resolved);
5251 }
5252 }
5253 }
5254 std::borrow::Cow::Borrowed(param)
5255 }
5256
5257 fn referenced_schema_is_string_enum(&self, name: &str) -> bool {
5270 if self.resolve_cached_schema(name).is_some_and(|schema| {
5271 matches!(
5272 schema.schema_type,
5273 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. }
5274 )
5275 }) {
5276 return true;
5277 }
5278 let Some(schema_value) = self
5279 .openapi_spec
5280 .get("components")
5281 .and_then(|c| c.get("schemas"))
5282 .and_then(|s| s.get(name))
5283 else {
5284 return false;
5285 };
5286 let is_string_type = schema_value
5287 .get("type")
5288 .and_then(|v| v.as_str())
5289 .map(|s| s == "string")
5290 .unwrap_or(false);
5291 let has_enum_or_const =
5292 schema_value.get("enum").is_some() || schema_value.get("const").is_some();
5293 is_string_type && has_enum_or_const
5294 }
5295
5296 fn resolve_raw_local_reference(&self, value: &Value) -> Option<Value> {
5297 let Some(reference) = value.get("$ref").and_then(Value::as_str) else {
5298 return Some(value.clone());
5299 };
5300 let pointer = reference.strip_prefix('#')?;
5301 self.openapi_spec.pointer(pointer).cloned()
5302 }
5303
5304 fn raw_request_body_schema(
5305 &self,
5306 operation: Option<&Value>,
5307 content_type: &str,
5308 ) -> Option<Value> {
5309 let request_body = operation?.get("requestBody")?;
5310 self.resolve_raw_local_reference(request_body)?
5311 .get("content")?
5312 .get(content_type)?
5313 .get("schema")
5314 .cloned()
5315 }
5316
5317 fn raw_parameter_schema(&self, parameter: &Value) -> Option<Value> {
5318 self.resolve_raw_local_reference(parameter)?
5319 .get("schema")
5320 .cloned()
5321 }
5322
5323 fn analyze_parameter(
5324 &mut self,
5325 param: &crate::openapi::Parameter,
5326 operation_id: &str,
5327 raw_validation_schema: Option<Value>,
5328 ) -> Result<Option<ParameterInfo>> {
5329 use heck::ToPascalCase;
5330
5331 let name = param.name.as_deref().unwrap_or("");
5332 let location = param.location.as_deref().unwrap_or("");
5333 let required = param.required.unwrap_or(false);
5334 let validation_schema = match raw_validation_schema {
5335 Some(schema) => Some(schema),
5336 None => param
5337 .schema
5338 .as_ref()
5339 .map(serde_json::to_value)
5340 .transpose()
5341 .map_err(GeneratorError::ParseError)?,
5342 };
5343
5344 let mut rust_type = "String".to_string();
5345 let mut schema_ref = None;
5346 let mut enum_values: Option<Vec<String>> = None;
5347 let mut enum_varnames: Option<Vec<String>> = None;
5348 let mut query_serialization: Option<QuerySerialization> = None;
5349
5350 let is_query = location == "query";
5356 let is_simple_header = location == "header"
5357 && matches!(param.style.as_deref(), None | Some("simple"))
5358 && param.explode != Some(true);
5359 let form_style = matches!(param.style.as_deref(), None | Some("form"));
5360 let form_exploded = form_style && param.explode.unwrap_or(true);
5361 let deep_object =
5362 param.style.as_deref() == Some("deepObject") && param.explode != Some(false);
5363
5364 let object_serialization = if !is_query {
5365 None
5366 } else if deep_object {
5367 Some(QuerySerialization::DeepObject)
5368 } else if form_exploded {
5369 Some(QuerySerialization::FormExplodedObject)
5370 } else if form_style {
5371 Some(QuerySerialization::FormObject)
5372 } else {
5373 None
5374 };
5375
5376 if let Some(schema) = ¶m.schema {
5377 if let Some(ref_str) = schema.reference() {
5378 if let Some(name) = self.extract_schema_name(ref_str) {
5384 if self.referenced_schema_is_string_enum(name) {
5385 schema_ref = Some(name.to_string());
5386 } else if object_serialization.is_some()
5387 && self.referenced_schema_is_object(name)
5388 {
5389 schema_ref = Some(name.to_string());
5390 query_serialization = if form_exploded && self.uses_aws_query_conventions()
5391 {
5392 match self.referenced_array_struct_item_type(name, 1) {
5393 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5394 Some(QuerySerialization::FormExplodedNestedObject {
5395 properties,
5396 })
5397 }
5398 _ => object_serialization.clone(),
5399 }
5400 } else {
5401 object_serialization.clone()
5402 };
5403 } else if (is_query && form_style || is_simple_header)
5404 && let Some(item_type) = self.referenced_array_param_item_type(name)
5405 {
5406 schema_ref = Some(name.to_string());
5412 query_serialization = Some(if is_simple_header {
5413 QuerySerialization::SimpleHeaderArray { item_type }
5414 } else if form_exploded {
5415 QuerySerialization::FormExplodedArray { item_type }
5416 } else {
5417 QuerySerialization::FormArray { item_type }
5418 });
5419 }
5420 }
5421 } else if object_serialization.is_some() && Self::schema_is_inline_object(schema) {
5422 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5427 let param_pascal = name.to_pascal_case();
5428 let synthetic_name = format!("{op_pascal}{param_pascal}");
5429 let mut deps = HashSet::new();
5430 self.add_inline_schema(&synthetic_name, schema, &mut deps)?;
5431 schema_ref = Some(synthetic_name.clone());
5432 query_serialization = if form_exploded && self.uses_aws_query_conventions() {
5433 match self.referenced_array_struct_item_type(&synthetic_name, 1) {
5434 Some(ArrayItemType::NestedStructRef { properties, .. }) => {
5435 Some(QuerySerialization::FormExplodedNestedObject { properties })
5436 }
5437 _ => object_serialization.clone(),
5438 }
5439 } else {
5440 object_serialization.clone()
5441 };
5442 } else if (is_query && form_style || is_simple_header)
5443 && matches!(
5444 schema.schema_type(),
5445 Some(crate::openapi::SchemaType::Array)
5446 )
5447 && let Some(item_type) = self.array_param_item_type(schema)
5448 {
5449 query_serialization = Some(if is_simple_header {
5457 QuerySerialization::SimpleHeaderArray { item_type }
5458 } else if form_exploded {
5459 QuerySerialization::FormExplodedArray { item_type }
5460 } else {
5461 QuerySerialization::FormArray { item_type }
5462 });
5463 } else if let Some(schema_type) = schema.schema_type() {
5464 let format = schema.details().format.clone();
5470 rust_type = match schema_type {
5471 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5472 crate::openapi::SchemaType::Integer => {
5473 self.type_mapper.integer_format(format.as_deref()).rust_type
5474 }
5475 crate::openapi::SchemaType::Number => {
5476 self.type_mapper.number_format(format.as_deref()).rust_type
5477 }
5478 crate::openapi::SchemaType::String => "String".to_string(),
5479 _ => "String".to_string(),
5480 };
5481
5482 if matches!(schema_type, crate::openapi::SchemaType::String) {
5483 let details = schema.details();
5484 if details.is_string_enum() {
5485 if let Some(values) = details.string_enum_values() {
5486 if !values.is_empty() {
5487 let op_pascal = operation_id.replace('.', "_").to_pascal_case();
5488 let param_pascal = name.to_pascal_case();
5489 rust_type = format!("{op_pascal}{param_pascal}");
5490 enum_varnames = details
5495 .extra
5496 .get("x-enum-varnames")
5497 .and_then(Value::as_array)
5498 .map(|raw| {
5499 raw.iter()
5500 .filter_map(Value::as_str)
5501 .map(str::to_owned)
5502 .collect::<Vec<_>>()
5503 })
5504 .filter(|names| names.len() == values.len());
5505 enum_values = Some(values);
5506 }
5507 }
5508 }
5509 }
5510 }
5511
5512 if is_query && query_serialization.is_none() {
5513 let referenced_name = schema
5514 .reference()
5515 .and_then(|reference| self.extract_schema_name(reference));
5516 let is_object = referenced_name
5517 .is_some_and(|name| self.referenced_schema_is_object(name))
5518 || Self::schema_is_inline_object(schema);
5519 let is_array = referenced_name
5520 .is_some_and(|name| self.referenced_schema_is_array(name))
5521 || matches!(
5522 schema.schema_type(),
5523 Some(crate::openapi::SchemaType::Array)
5524 );
5525 let is_composed = referenced_name
5526 .is_some_and(|name| self.referenced_schema_is_composed_query_shape(name));
5527 let reason = if param.style.as_deref() == Some("deepObject")
5528 && param.explode == Some(false)
5529 {
5530 Some("style=deepObject with explode=false is undefined by OpenAPI".to_string())
5531 } else if param.style.as_deref() == Some("deepObject") && !is_object {
5532 Some("style=deepObject is defined only for object query parameters".to_string())
5533 } else if is_object {
5534 Some(format!(
5535 "object query parameters do not support style={}",
5536 param.style.as_deref().unwrap_or("form")
5537 ))
5538 } else if is_array && form_style {
5539 Some(
5540 "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"
5541 .to_string(),
5542 )
5543 } else if is_array {
5544 Some(format!(
5545 "array query parameters do not yet support style={}",
5546 param.style.as_deref().unwrap_or("form")
5547 ))
5548 } else if is_composed {
5549 Some(
5550 "composed or union query schemas cannot be projected to an unambiguous flat wire shape"
5551 .to_string(),
5552 )
5553 } else {
5554 None
5555 };
5556 if let Some(reason) = reason {
5557 query_serialization = Some(QuerySerialization::Unsupported { reason });
5558 }
5559 }
5560 }
5561
5562 Ok(Some(ParameterInfo {
5563 name: name.to_string(),
5564 location: location.to_string(),
5565 required,
5566 schema_ref,
5567 rust_type,
5568 description: param.description.clone(),
5569 enum_values,
5570 enum_varnames,
5571 rust_ident: None,
5572 query_serialization,
5573 validation_schema,
5574 }))
5575 }
5576
5577 fn array_param_item_type(&self, schema: &crate::openapi::Schema) -> Option<ArrayItemType> {
5586 let items = schema.details().items.as_deref()?;
5587 let unwrapped = unwrap_annotation_allof(items);
5591 if let Some(ref_str) = unwrapped.reference() {
5592 let name = self.extract_schema_name(ref_str)?;
5593 return self
5594 .referenced_array_scalar_item_type(name)
5595 .or_else(|| self.referenced_array_struct_item_type(name, 1));
5596 }
5597 let format = unwrapped.details().format.clone();
5598 let scalar = match unwrapped.schema_type()? {
5599 crate::openapi::SchemaType::String => "String".to_string(),
5600 crate::openapi::SchemaType::Integer => {
5601 self.type_mapper.integer_format(format.as_deref()).rust_type
5602 }
5603 crate::openapi::SchemaType::Number => {
5604 self.type_mapper.number_format(format.as_deref()).rust_type
5605 }
5606 crate::openapi::SchemaType::Boolean => "bool".to_string(),
5607 _ => return None,
5608 };
5609 Some(ArrayItemType::Scalar(scalar))
5610 }
5611
5612 fn referenced_array_param_item_type(&self, name: &str) -> Option<ArrayItemType> {
5615 let schema = self.resolve_cached_schema(name)?;
5616 let SchemaType::Array { item_type } = &schema.schema_type else {
5617 return None;
5618 };
5619 self.analyzed_array_item_type(item_type)
5620 }
5621
5622 fn analyzed_array_item_type(&self, item_type: &SchemaType) -> Option<ArrayItemType> {
5623 self.analyzed_array_item_type_at_depth(item_type, 1)
5624 }
5625
5626 fn referenced_array_struct_item_type(
5631 &self,
5632 name: &str,
5633 nested_array_depth: usize,
5634 ) -> Option<ArrayItemType> {
5635 let resolved = self.resolve_cached_schema(name)?;
5636 let SchemaType::Object {
5637 properties,
5638 required,
5639 additional_properties,
5640 } = &resolved.schema_type
5641 else {
5642 return None;
5643 };
5644 if properties.is_empty()
5645 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5646 {
5647 return None;
5648 }
5649 let mut projected = Vec::with_capacity(properties.len());
5650 let mut has_array = false;
5651 for (wire_name, property) in properties {
5652 let value_type = if let Some(scalar) = self.query_scalar_type(&property.schema_type) {
5653 QueryStructPropertyType::Scalar(scalar)
5654 } else {
5655 if nested_array_depth == 0 {
5656 return None;
5657 }
5658 if let Some(array) = self.resolve_query_array_type(&property.schema_type) {
5659 let item_type =
5660 self.analyzed_array_item_type_at_depth(array, nested_array_depth - 1)?;
5661 if matches!(item_type, ArrayItemType::NestedStructRef { .. }) {
5662 return None;
5663 }
5664 has_array = true;
5665 QueryStructPropertyType::Array { item_type }
5666 } else {
5667 has_array = true;
5668 QueryStructPropertyType::Object {
5669 properties: self.query_flat_object_properties(&property.schema_type)?,
5670 }
5671 }
5672 };
5673 projected.push(QueryStructProperty {
5674 wire_name: wire_name.clone(),
5675 required: required.contains(wire_name),
5676 value_type,
5677 });
5678 }
5679 if has_array {
5680 Some(ArrayItemType::NestedStructRef {
5681 schema_name: name.to_string(),
5682 properties: projected,
5683 })
5684 } else {
5685 Some(ArrayItemType::FlatStructRef {
5686 schema_name: name.to_string(),
5687 properties: projected,
5688 })
5689 }
5690 }
5691
5692 fn analyzed_array_item_type_at_depth(
5693 &self,
5694 item_type: &SchemaType,
5695 nested_array_depth: usize,
5696 ) -> Option<ArrayItemType> {
5697 match item_type {
5698 SchemaType::Primitive { rust_type, .. } => {
5699 Some(ArrayItemType::Scalar(rust_type.clone()))
5700 }
5701 SchemaType::Reference { target } => self
5702 .referenced_array_scalar_item_type(target)
5703 .or_else(|| self.referenced_array_struct_item_type(target, nested_array_depth)),
5704 _ => None,
5705 }
5706 }
5707
5708 fn resolve_query_array_type<'a>(
5709 &'a self,
5710 schema_type: &'a SchemaType,
5711 ) -> Option<&'a SchemaType> {
5712 match schema_type {
5713 SchemaType::Array { item_type } => Some(item_type),
5714 SchemaType::Reference { target } => {
5715 let resolved = self.resolve_cached_schema(target)?;
5716 let SchemaType::Array { item_type } = &resolved.schema_type else {
5717 return None;
5718 };
5719 Some(item_type)
5720 }
5721 _ => None,
5722 }
5723 }
5724
5725 fn query_flat_object_properties(
5726 &self,
5727 schema_type: &SchemaType,
5728 ) -> Option<Vec<QueryStructProperty>> {
5729 let schema_type = match schema_type {
5730 SchemaType::Reference { target } => &self.resolve_cached_schema(target)?.schema_type,
5731 other => other,
5732 };
5733 let SchemaType::Object {
5734 properties,
5735 required,
5736 additional_properties,
5737 } = schema_type
5738 else {
5739 return None;
5740 };
5741 if properties.is_empty()
5742 || !matches!(additional_properties, ObjectAdditionalProperties::Forbidden)
5743 {
5744 return None;
5745 }
5746 properties
5747 .iter()
5748 .map(|(wire_name, property)| {
5749 Some(QueryStructProperty {
5750 wire_name: wire_name.clone(),
5751 required: required.contains(wire_name),
5752 value_type: QueryStructPropertyType::Scalar(
5753 self.query_scalar_type(&property.schema_type)?,
5754 ),
5755 })
5756 })
5757 .collect()
5758 }
5759
5760 fn query_scalar_type(&self, schema_type: &SchemaType) -> Option<QueryScalarType> {
5761 match schema_type {
5762 SchemaType::Primitive { rust_type, .. } => match rust_type.as_str() {
5763 "String" => Some(QueryScalarType::String),
5764 "bool" => Some(QueryScalarType::Boolean),
5765 value if value.starts_with('i') || value.starts_with('u') => {
5766 Some(QueryScalarType::Integer)
5767 }
5768 value if value.starts_with('f') => Some(QueryScalarType::Number),
5769 "serde_json::Value" => None,
5770 _ => Some(QueryScalarType::String),
5771 },
5772 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => {
5773 Some(QueryScalarType::String)
5774 }
5775 SchemaType::Reference { target } => {
5776 let resolved = self.resolve_cached_schema(target)?;
5777 self.query_scalar_type(&resolved.schema_type)
5778 }
5779 _ => None,
5780 }
5781 }
5782
5783 fn referenced_array_scalar_item_type(&self, name: &str) -> Option<ArrayItemType> {
5791 let resolved = self.resolve_cached_schema(name)?;
5792 let supported = match &resolved.schema_type {
5793 SchemaType::StringEnum { .. } | SchemaType::ExtensibleEnum { .. } => true,
5794 SchemaType::Primitive { .. } => resolved
5795 .original
5796 .get("type")
5797 .is_some_and(Self::query_scalar_type_value),
5798 _ => false,
5799 };
5800 supported.then(|| ArrayItemType::SchemaRef(name.to_string()))
5801 }
5802
5803 fn query_scalar_type_value(value: &Value) -> bool {
5804 const SCALARS: [&str; 4] = ["string", "integer", "number", "boolean"];
5805 if let Some(value) = value.as_str() {
5806 return SCALARS.contains(&value);
5807 }
5808 let Some(values) = value.as_array() else {
5809 return false;
5810 };
5811 if !values.iter().all(Value::is_string) {
5812 return false;
5813 }
5814 let mut non_null = values
5815 .iter()
5816 .filter_map(Value::as_str)
5817 .filter(|value| *value != "null");
5818 let Some(scalar) = non_null.next() else {
5819 return false;
5820 };
5821 non_null.next().is_none() && SCALARS.contains(&scalar)
5822 }
5823
5824 fn referenced_schema_is_object(&self, name: &str) -> bool {
5828 self.resolve_cached_schema(name)
5829 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Object { .. }))
5830 }
5831
5832 fn referenced_schema_is_array(&self, name: &str) -> bool {
5833 self.resolve_cached_schema(name)
5834 .is_some_and(|schema| matches!(schema.schema_type, SchemaType::Array { .. }))
5835 }
5836
5837 fn referenced_schema_is_composed_query_shape(&self, name: &str) -> bool {
5838 self.resolve_cached_schema(name).is_some_and(|schema| {
5839 matches!(
5840 schema.schema_type,
5841 SchemaType::Composition { .. }
5842 | SchemaType::Union { .. }
5843 | SchemaType::DiscriminatedUnion { .. }
5844 )
5845 })
5846 }
5847
5848 fn resolve_cached_schema(&self, name: &str) -> Option<&AnalyzedSchema> {
5849 let mut current = name;
5850 let mut visited = HashSet::new();
5851 loop {
5852 if !visited.insert(current) {
5853 return None;
5854 }
5855 let schema = self.resolved_cache.get(current)?;
5856 if let SchemaType::Reference { target } = &schema.schema_type {
5857 current = target;
5858 } else {
5859 return Some(schema);
5860 }
5861 }
5862 }
5863
5864 fn schema_is_inline_object(schema: &crate::openapi::Schema) -> bool {
5866 match schema.schema_type() {
5867 Some(crate::openapi::SchemaType::Object) => true,
5868 None => schema.details().properties.is_some(),
5869 _ => false,
5870 }
5871 }
5872}