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        if schema.all_of.len() == 1 {
36            return schema_or_ref_to_ir_type(&schema.all_of[0]);
37        }
38        let parts: Vec<IrType> = schema
39            .all_of
40            .iter()
41            .map(|sub| match sub {
42                SchemaOrRef::Ref { .. } => schema_or_ref_to_ir_type(sub),
43                SchemaOrRef::Schema(s) => {
44                    if s.properties.is_empty() {
45                        schema_to_ir_type(s)
46                    } else {
47                        let fields: Vec<(String, IrType, bool)> = s
48                            .properties
49                            .iter()
50                            .map(|(name, prop)| {
51                                (
52                                    name.clone(),
53                                    schema_or_ref_to_ir_type(prop),
54                                    s.required.contains(name),
55                                )
56                            })
57                            .collect();
58                        IrType::Object(fields)
59                    }
60                }
61            })
62            .collect();
63        return IrType::Intersection(parts);
64    }
65
66    // Handle enum
67    if !schema.enum_values.is_empty() {
68        let string_variants: Vec<String> = schema
69            .enum_values
70            .iter()
71            .filter_map(|v| v.as_str().map(|s| s.to_string()))
72            .collect();
73        if string_variants.len() == 1 {
74            return IrType::StringLiteral(string_variants.into_iter().next().unwrap());
75        }
76        if string_variants.len() > 1 {
77            return IrType::Union(
78                string_variants
79                    .into_iter()
80                    .map(IrType::StringLiteral)
81                    .collect(),
82            );
83        }
84        return IrType::String; // fallback for non-string enums
85    }
86
87    // Handle const
88    if let Some(ref val) = schema.const_value {
89        if let Some(s) = val.as_str() {
90            return IrType::StringLiteral(s.to_string());
91        }
92        return IrType::String;
93    }
94
95    // Handle type
96    match &schema.schema_type {
97        Some(TypeSet::Single(t)) => match t {
98            SchemaType::String => match schema.format.as_deref() {
99                Some("date-time" | "date") => IrType::DateTime,
100                Some("binary" | "byte") => IrType::Binary,
101                _ => IrType::String,
102            },
103            SchemaType::Number => IrType::Number,
104            SchemaType::Integer => IrType::Integer,
105            SchemaType::Boolean => IrType::Boolean,
106            SchemaType::Null => IrType::Null,
107            SchemaType::Array => match &schema.items {
108                Some(items) => IrType::Array(Box::new(schema_or_ref_to_ir_type(items))),
109                None => IrType::Array(Box::new(IrType::Any)),
110            },
111            SchemaType::Object => resolve_object_type(schema),
112        },
113        Some(TypeSet::Multiple(types)) => {
114            let non_null: Vec<_> = types.iter().filter(|t| **t != SchemaType::Null).collect();
115            let has_null = types.contains(&SchemaType::Null);
116            if non_null.len() == 1 {
117                let single = Schema {
118                    schema_type: Some(TypeSet::Single(non_null[0].clone())),
119                    ..schema.clone()
120                };
121                let base = schema_to_ir_type(&single);
122                if has_null {
123                    IrType::Union(vec![base, IrType::Null])
124                } else {
125                    base
126                }
127            } else if non_null.is_empty() && has_null {
128                IrType::Null
129            } else {
130                // Multiple non-null types — build union of all
131                let mut variants: Vec<IrType> = non_null
132                    .iter()
133                    .map(|t| {
134                        let s = Schema {
135                            schema_type: Some(TypeSet::Single((*t).clone())),
136                            ..schema.clone()
137                        };
138                        schema_to_ir_type(&s)
139                    })
140                    .collect();
141                if has_null {
142                    variants.push(IrType::Null);
143                }
144                IrType::Union(variants)
145            }
146        }
147        None => {
148            // No type specified — check if it has properties (implicit object)
149            if !schema.properties.is_empty() {
150                resolve_object_type(schema)
151            } else if schema.items.is_some() {
152                match &schema.items {
153                    Some(items) => IrType::Array(Box::new(schema_or_ref_to_ir_type(items))),
154                    None => IrType::Array(Box::new(IrType::Any)),
155                }
156            } else {
157                IrType::Any
158            }
159        }
160    }
161}
162
163fn resolve_object_type(schema: &Schema) -> IrType {
164    if schema.properties.is_empty() {
165        match &schema.additional_properties {
166            Some(AdditionalProperties::Schema(s)) => {
167                IrType::Map(Box::new(schema_or_ref_to_ir_type(s)))
168            }
169            Some(AdditionalProperties::Bool(true)) => IrType::Map(Box::new(IrType::Any)),
170            Some(AdditionalProperties::Bool(false)) | None => IrType::Any,
171        }
172    } else {
173        let fields: Vec<(String, IrType, bool)> = schema
174            .properties
175            .iter()
176            .map(|(name, prop)| {
177                let required = schema.required.contains(name);
178                (name.clone(), schema_or_ref_to_ir_type(prop), required)
179            })
180            .collect();
181        IrType::Object(fields)
182    }
183}
184
185/// Convert a named component schema to an `IrSchema`.
186pub fn schema_or_ref_to_ir_schema(
187    name: &str,
188    schema_or_ref: &SchemaOrRef,
189) -> Result<IrSchema, TransformError> {
190    match schema_or_ref {
191        SchemaOrRef::Ref { ref_path } => {
192            let target = ref_path.rsplit('/').next().unwrap_or("Unknown");
193            Ok(IrSchema::Alias(IrAliasSchema {
194                name: normalize_name(name),
195                description: None,
196                target: IrType::Ref(normalize_name(target).pascal_case),
197            }))
198        }
199        SchemaOrRef::Schema(schema) => schema_to_ir_schema(name, schema),
200    }
201}
202
203/// Convert a named `Schema` to an `IrSchema`.
204pub fn schema_to_ir_schema(name: &str, schema: &Schema) -> Result<IrSchema, TransformError> {
205    let normalized = normalize_name(name);
206
207    // Check for enum
208    if !schema.enum_values.is_empty() {
209        let variants: Vec<String> = schema
210            .enum_values
211            .iter()
212            .filter_map(|v| v.as_str().map(|s| s.to_string()))
213            .collect();
214        return Ok(IrSchema::Enum(IrEnumSchema {
215            name: normalized,
216            description: schema.description.clone(),
217            variants,
218        }));
219    }
220
221    // Check for oneOf / anyOf (union)
222    if !schema.one_of.is_empty() || !schema.any_of.is_empty() {
223        let variants_src = if !schema.one_of.is_empty() {
224            &schema.one_of
225        } else {
226            &schema.any_of
227        };
228        let variants: Vec<IrType> = variants_src.iter().map(schema_or_ref_to_ir_type).collect();
229        let discriminator = schema.discriminator.as_ref().map(|d| IrDiscriminator {
230            property_name: d.property_name.clone(),
231            mapping: d
232                .mapping
233                .iter()
234                .map(|(k, v)| (k.clone(), v.clone()))
235                .collect(),
236        });
237        return Ok(IrSchema::Union(IrUnionSchema {
238            name: normalized,
239            description: schema.description.clone(),
240            variants,
241            discriminator,
242        }));
243    }
244
245    // Check for allOf
246    if !schema.all_of.is_empty() {
247        let has_refs = schema
248            .all_of
249            .iter()
250            .any(|s| matches!(s, SchemaOrRef::Ref { .. }));
251        if has_refs {
252            // Build intersection: refs stay as Ref, inline schemas become Objects
253            let mut parts: Vec<IrType> = schema
254                .all_of
255                .iter()
256                .map(|sub| match sub {
257                    SchemaOrRef::Ref { .. } => schema_or_ref_to_ir_type(sub),
258                    SchemaOrRef::Schema(s) => {
259                        let fields = build_fields(&s.properties, &s.required);
260                        if fields.is_empty() {
261                            schema_to_ir_type(s)
262                        } else {
263                            let inline_fields: Vec<(String, IrType, bool)> = fields
264                                .into_iter()
265                                .map(|f| (f.original_name, f.field_type, f.required))
266                                .collect();
267                            IrType::Object(inline_fields)
268                        }
269                    }
270                })
271                .collect();
272            // Add extra properties from the parent schema if any
273            if !schema.properties.is_empty() {
274                let extra_fields = build_fields(&schema.properties, &schema.required);
275                let inline_fields: Vec<(String, IrType, bool)> = extra_fields
276                    .into_iter()
277                    .map(|f| (f.original_name, f.field_type, f.required))
278                    .collect();
279                parts.push(IrType::Object(inline_fields));
280            }
281            return Ok(IrSchema::Alias(IrAliasSchema {
282                name: normalized,
283                description: schema.description.clone(),
284                target: IrType::Intersection(parts),
285            }));
286        }
287        // No refs — safe to flatten merge as before
288        let merged = merge_all_of(&schema.all_of, &schema.properties, &schema.required);
289        return Ok(IrSchema::Object(IrObjectSchema {
290            name: normalized,
291            description: schema.description.clone(),
292            fields: merged,
293            additional_properties: None,
294        }));
295    }
296
297    // Check if it's a simple type alias
298    match &schema.schema_type {
299        Some(TypeSet::Single(SchemaType::Object)) | None if !schema.properties.is_empty() => {
300            // Object with properties
301            let fields = build_fields(&schema.properties, &schema.required);
302            let additional = schema
303                .additional_properties
304                .as_ref()
305                .and_then(|ap| match ap {
306                    AdditionalProperties::Schema(s) => Some(schema_or_ref_to_ir_type(s)),
307                    AdditionalProperties::Bool(true) => Some(IrType::Any),
308                    _ => None,
309                });
310            Ok(IrSchema::Object(IrObjectSchema {
311                name: normalized,
312                description: schema.description.clone(),
313                fields,
314                additional_properties: additional,
315            }))
316        }
317        _ => {
318            // Simple alias (string, number, array, etc.)
319            let target = schema_to_ir_type(schema);
320            Ok(IrSchema::Alias(IrAliasSchema {
321                name: normalized,
322                description: schema.description.clone(),
323                target,
324            }))
325        }
326    }
327}
328
329fn build_fields(properties: &IndexMap<String, SchemaOrRef>, required: &[String]) -> Vec<IrField> {
330    properties
331        .iter()
332        .map(|(name, prop)| {
333            let (description, read_only, write_only) = match prop {
334                SchemaOrRef::Schema(s) => (
335                    s.description.clone(),
336                    s.read_only.unwrap_or(false),
337                    s.write_only.unwrap_or(false),
338                ),
339                _ => (None, false, false),
340            };
341            IrField {
342                name: normalize_name(name),
343                original_name: name.clone(),
344                field_type: schema_or_ref_to_ir_type(prop),
345                required: required.contains(name),
346                description,
347                read_only,
348                write_only,
349            }
350        })
351        .collect()
352}
353
354fn merge_all_of(
355    all_of: &[SchemaOrRef],
356    extra_properties: &IndexMap<String, SchemaOrRef>,
357    extra_required: &[String],
358) -> Vec<IrField> {
359    let mut fields = Vec::new();
360
361    for item in all_of {
362        if let SchemaOrRef::Schema(schema) = item {
363            fields.extend(build_fields(&schema.properties, &schema.required));
364            // Recursively merge nested allOf
365            if !schema.all_of.is_empty() {
366                fields.extend(merge_all_of(&schema.all_of, &IndexMap::new(), &[]));
367            }
368        }
369    }
370
371    // Add extra properties from the parent schema
372    fields.extend(build_fields(extra_properties, extra_required));
373
374    fields
375}