Skip to main content

oag_core/transform/
schema_resolver.rs

1use indexmap::IndexMap;
2
3use crate::error::TransformError;
4use crate::ir::{
5    IrAliasSchema, IrDiscriminator, IrEnumSchema, IrField, IrObjectSchema, IrSchema, IrType,
6    IrUnionSchema,
7};
8use crate::parse::schema::{AdditionalProperties, Schema, SchemaOrRef, SchemaType, TypeSet};
9
10use super::name_normalizer::normalize_name;
11
12/// Convert a parsed `SchemaOrRef` to an `IrType`.
13pub fn schema_or_ref_to_ir_type(schema_or_ref: &SchemaOrRef) -> IrType {
14    match schema_or_ref {
15        SchemaOrRef::Ref { ref_path } => {
16            let name = ref_path.rsplit('/').next().unwrap_or("Unknown");
17            IrType::Ref(normalize_name(name).pascal_case)
18        }
19        SchemaOrRef::Schema(schema) => schema_to_ir_type(schema),
20    }
21}
22
23/// Convert a parsed `Schema` to an `IrType`.
24pub fn schema_to_ir_type(schema: &Schema) -> IrType {
25    // Handle composition first
26    if !schema.one_of.is_empty() {
27        let variants: Vec<IrType> = schema.one_of.iter().map(schema_or_ref_to_ir_type).collect();
28        return IrType::Union(variants);
29    }
30    if !schema.any_of.is_empty() {
31        let variants: Vec<IrType> = schema.any_of.iter().map(schema_or_ref_to_ir_type).collect();
32        return IrType::Union(variants);
33    }
34    if !schema.all_of.is_empty() {
35        // For allOf, we merge into an inline object or return the single ref
36        if schema.all_of.len() == 1 {
37            return schema_or_ref_to_ir_type(&schema.all_of[0]);
38        }
39        // Merge allOf properties
40        let mut merged_fields = Vec::new();
41        for sub in &schema.all_of {
42            match sub {
43                SchemaOrRef::Schema(s) => {
44                    for (name, prop) in &s.properties {
45                        let required = s.required.contains(name);
46                        merged_fields.push((
47                            name.clone(),
48                            schema_or_ref_to_ir_type(prop),
49                            required,
50                        ));
51                    }
52                }
53                SchemaOrRef::Ref { .. } => {
54                    // If allOf contains refs, treat as first ref
55                    return schema_or_ref_to_ir_type(sub);
56                }
57            }
58        }
59        if !merged_fields.is_empty() {
60            return IrType::Object(merged_fields);
61        }
62    }
63
64    // Handle enum
65    if !schema.enum_values.is_empty() {
66        return IrType::String; // string enum — the actual variants are in IrEnumSchema
67    }
68
69    // Handle const
70    if schema.const_value.is_some() {
71        return IrType::String;
72    }
73
74    // Handle type
75    match &schema.schema_type {
76        Some(TypeSet::Single(t)) => match t {
77            SchemaType::String => match schema.format.as_deref() {
78                Some("date-time" | "date") => IrType::DateTime,
79                Some("binary" | "byte") => IrType::Binary,
80                _ => IrType::String,
81            },
82            SchemaType::Number => IrType::Number,
83            SchemaType::Integer => IrType::Integer,
84            SchemaType::Boolean => IrType::Boolean,
85            SchemaType::Null => IrType::Null,
86            SchemaType::Array => match &schema.items {
87                Some(items) => IrType::Array(Box::new(schema_or_ref_to_ir_type(items))),
88                None => IrType::Array(Box::new(IrType::Any)),
89            },
90            SchemaType::Object => resolve_object_type(schema),
91        },
92        Some(TypeSet::Multiple(types)) => {
93            let non_null: Vec<_> = types.iter().filter(|t| **t != SchemaType::Null).collect();
94            if non_null.len() == 1 {
95                // Nullable type — we just return the non-null type
96                let single = Schema {
97                    schema_type: Some(TypeSet::Single(non_null[0].clone())),
98                    ..schema.clone()
99                };
100                schema_to_ir_type(&single)
101            } else {
102                IrType::Any
103            }
104        }
105        None => {
106            // No type specified — check if it has properties (implicit object)
107            if !schema.properties.is_empty() {
108                resolve_object_type(schema)
109            } else if schema.items.is_some() {
110                match &schema.items {
111                    Some(items) => IrType::Array(Box::new(schema_or_ref_to_ir_type(items))),
112                    None => IrType::Array(Box::new(IrType::Any)),
113                }
114            } else {
115                IrType::Any
116            }
117        }
118    }
119}
120
121fn resolve_object_type(schema: &Schema) -> IrType {
122    if schema.properties.is_empty() {
123        // Check for additionalProperties (Record/Map type)
124        match &schema.additional_properties {
125            Some(AdditionalProperties::Schema(s)) => {
126                IrType::Map(Box::new(schema_or_ref_to_ir_type(s)))
127            }
128            Some(AdditionalProperties::Bool(true)) | None => {
129                if schema.properties.is_empty() && schema.additional_properties.is_some() {
130                    IrType::Map(Box::new(IrType::Any))
131                } else {
132                    IrType::Any
133                }
134            }
135            Some(AdditionalProperties::Bool(false)) => IrType::Any,
136        }
137    } else {
138        let fields: Vec<(String, IrType, bool)> = schema
139            .properties
140            .iter()
141            .map(|(name, prop)| {
142                let required = schema.required.contains(name);
143                (name.clone(), schema_or_ref_to_ir_type(prop), required)
144            })
145            .collect();
146        IrType::Object(fields)
147    }
148}
149
150/// Convert a named component schema to an `IrSchema`.
151pub fn schema_or_ref_to_ir_schema(
152    name: &str,
153    schema_or_ref: &SchemaOrRef,
154) -> Result<IrSchema, TransformError> {
155    match schema_or_ref {
156        SchemaOrRef::Ref { ref_path } => {
157            let target = ref_path.rsplit('/').next().unwrap_or("Unknown");
158            Ok(IrSchema::Alias(IrAliasSchema {
159                name: normalize_name(name),
160                description: None,
161                target: IrType::Ref(normalize_name(target).pascal_case),
162            }))
163        }
164        SchemaOrRef::Schema(schema) => schema_to_ir_schema(name, schema),
165    }
166}
167
168/// Convert a named `Schema` to an `IrSchema`.
169pub fn schema_to_ir_schema(name: &str, schema: &Schema) -> Result<IrSchema, TransformError> {
170    let normalized = normalize_name(name);
171
172    // Check for enum
173    if !schema.enum_values.is_empty() {
174        let variants: Vec<String> = schema
175            .enum_values
176            .iter()
177            .filter_map(|v| v.as_str().map(|s| s.to_string()))
178            .collect();
179        return Ok(IrSchema::Enum(IrEnumSchema {
180            name: normalized,
181            description: schema.description.clone(),
182            variants,
183        }));
184    }
185
186    // Check for oneOf / anyOf (union)
187    if !schema.one_of.is_empty() || !schema.any_of.is_empty() {
188        let variants_src = if !schema.one_of.is_empty() {
189            &schema.one_of
190        } else {
191            &schema.any_of
192        };
193        let variants: Vec<IrType> = variants_src.iter().map(schema_or_ref_to_ir_type).collect();
194        let discriminator = schema.discriminator.as_ref().map(|d| IrDiscriminator {
195            property_name: d.property_name.clone(),
196            mapping: d
197                .mapping
198                .iter()
199                .map(|(k, v)| (k.clone(), v.clone()))
200                .collect(),
201        });
202        return Ok(IrSchema::Union(IrUnionSchema {
203            name: normalized,
204            description: schema.description.clone(),
205            variants,
206            discriminator,
207        }));
208    }
209
210    // Check for allOf (merge into object)
211    if !schema.all_of.is_empty() {
212        let merged = merge_all_of(&schema.all_of, &schema.properties, &schema.required);
213        return Ok(IrSchema::Object(IrObjectSchema {
214            name: normalized,
215            description: schema.description.clone(),
216            fields: merged,
217            additional_properties: None,
218        }));
219    }
220
221    // Check if it's a simple type alias
222    match &schema.schema_type {
223        Some(TypeSet::Single(SchemaType::Object)) | None if !schema.properties.is_empty() => {
224            // Object with properties
225            let fields = build_fields(&schema.properties, &schema.required);
226            let additional = schema
227                .additional_properties
228                .as_ref()
229                .and_then(|ap| match ap {
230                    AdditionalProperties::Schema(s) => Some(schema_or_ref_to_ir_type(s)),
231                    AdditionalProperties::Bool(true) => Some(IrType::Any),
232                    _ => None,
233                });
234            Ok(IrSchema::Object(IrObjectSchema {
235                name: normalized,
236                description: schema.description.clone(),
237                fields,
238                additional_properties: additional,
239            }))
240        }
241        _ => {
242            // Simple alias (string, number, array, etc.)
243            let target = schema_to_ir_type(schema);
244            Ok(IrSchema::Alias(IrAliasSchema {
245                name: normalized,
246                description: schema.description.clone(),
247                target,
248            }))
249        }
250    }
251}
252
253fn build_fields(properties: &IndexMap<String, SchemaOrRef>, required: &[String]) -> Vec<IrField> {
254    properties
255        .iter()
256        .map(|(name, prop)| {
257            let (description, read_only, write_only) = match prop {
258                SchemaOrRef::Schema(s) => (
259                    s.description.clone(),
260                    s.read_only.unwrap_or(false),
261                    s.write_only.unwrap_or(false),
262                ),
263                _ => (None, false, false),
264            };
265            IrField {
266                name: normalize_name(name),
267                original_name: name.clone(),
268                field_type: schema_or_ref_to_ir_type(prop),
269                required: required.contains(name),
270                description,
271                read_only,
272                write_only,
273            }
274        })
275        .collect()
276}
277
278fn merge_all_of(
279    all_of: &[SchemaOrRef],
280    extra_properties: &IndexMap<String, SchemaOrRef>,
281    extra_required: &[String],
282) -> Vec<IrField> {
283    let mut fields = Vec::new();
284
285    for item in all_of {
286        if let SchemaOrRef::Schema(schema) = item {
287            fields.extend(build_fields(&schema.properties, &schema.required));
288            // Recursively merge nested allOf
289            if !schema.all_of.is_empty() {
290                fields.extend(merge_all_of(&schema.all_of, &IndexMap::new(), &[]));
291            }
292        }
293    }
294
295    // Add extra properties from the parent schema
296    fields.extend(build_fields(extra_properties, extra_required));
297
298    fields
299}