Skip to main content

fraiseql_core/compiler/
parser.rs

1//! Schema parser - JSON → Authoring IR.
2//!
3//! Parses JSON schema definitions emitted by authoring-language decorators
4//! into internal Intermediate Representation.
5//!
6//! Supports parsing all GraphQL schema elements:
7//! - **Types**: Object definitions with fields
8//! - **Interfaces**: Abstract type contracts that types can implement
9//! - **Unions**: Type combinations allowing multiple member types
10//! - **Input Types**: Input object definitions for mutations and filters
11//! - **Enums**: Enumeration type definitions
12//! - **Queries**: Root query definitions
13//! - **Mutations**: Root mutation definitions
14//! - **Subscriptions**: Root subscription definitions
15//!
16//! # Example
17//!
18//! ```rust
19//! use fraiseql_core::compiler::parser::SchemaParser;
20//!
21//! let parser = SchemaParser::new();
22//! let schema_json = r#"{
23//!     "types": [{"name": "User", "fields": []}],
24//!     "interfaces": [{"name": "Node", "fields": []}],
25//!     "unions": [{"name": "SearchResult", "types": ["User"]}],
26//!     "input_types": [{"name": "UserInput", "fields": []}],
27//!     "queries": [{"name": "users", "return_type": "User", "returns_list": true}]
28//! }"#;
29//! let ir = parser.parse(schema_json).unwrap();
30//! assert_eq!(ir.types.len(), 1);
31//! assert_eq!(ir.interfaces.len(), 1);
32//! assert_eq!(ir.unions.len(), 1);
33//! assert_eq!(ir.input_types.len(), 1);
34//! assert_eq!(ir.queries.len(), 1);
35//! ```
36
37use serde_json::Value;
38
39use super::{
40    enum_validator::EnumValidator,
41    ir::{
42        AuthoringIR, AutoParams, IRArgument, IRField, IRInputField, IRInputType, IRInterface,
43        IRMutation, IRQuery, IRScalar, IRSubscription, IRType, IRUnion, MutationOperation,
44    },
45};
46use crate::{
47    error::{FraiseQLError, Result},
48    schema::GraphQLValue,
49};
50
51/// Schema parser.
52///
53/// Transforms JSON schema from authoring languages into internal IR.
54pub struct SchemaParser {
55    // Parser state (if needed in future)
56}
57
58impl SchemaParser {
59    /// Create new schema parser.
60    #[must_use]
61    pub const fn new() -> Self {
62        Self {}
63    }
64
65    /// Parse JSON schema into IR.
66    ///
67    /// # Arguments
68    ///
69    /// * `schema_json` - JSON schema string from decorators
70    ///
71    /// # Returns
72    ///
73    /// Parsed Authoring IR
74    ///
75    /// # Errors
76    ///
77    /// Returns error if JSON is malformed or missing required fields.
78    ///
79    /// # Example
80    ///
81    /// ```rust
82    /// use fraiseql_core::compiler::parser::SchemaParser;
83    ///
84    /// let parser = SchemaParser::new();
85    /// let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
86    /// let ir = parser.parse(json).unwrap();
87    /// assert!(ir.types.is_empty());
88    /// ```
89    pub fn parse(&self, schema_json: &str) -> Result<AuthoringIR> {
90        // Parse JSON
91        let value: Value = serde_json::from_str(schema_json).map_err(|e| FraiseQLError::Parse {
92            message:  format!("Failed to parse schema JSON: {e}"),
93            location: "root".to_string(),
94        })?;
95
96        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
97            message:  "Schema must be a JSON object".to_string(),
98            location: "root".to_string(),
99        })?;
100
101        let types = obj.get("types").map_or(Ok(vec![]), |v| self.parse_types(v))?;
102        let queries = obj.get("queries").map_or(Ok(vec![]), |v| self.parse_queries(v))?;
103        let mutations = obj.get("mutations").map_or(Ok(vec![]), |v| self.parse_mutations(v))?;
104        let subscriptions =
105            obj.get("subscriptions").map_or(Ok(vec![]), |v| self.parse_subscriptions(v))?;
106        let fact_tables = obj
107            .get("fact_tables")
108            .and_then(Value::as_object)
109            .map_or_else::<Result<_>, _, _>(
110                || Ok(std::collections::HashMap::new()),
111                |o| {
112                    o.iter()
113                        .map(|(k, v)| {
114                            let meta: crate::compiler::fact_table::FactTableMetadata =
115                                serde_json::from_value(v.clone()).map_err(|e| {
116                                    FraiseQLError::Parse {
117                                        message:  format!(
118                                            "Invalid fact table metadata for '{}': {e}",
119                                            k
120                                        ),
121                                        location: format!("fact_tables.{}", k),
122                                    }
123                                })?;
124                            Ok((k.clone(), meta))
125                        })
126                        .collect()
127                },
128            )?;
129        let enums = obj.get("enums").map_or(Ok(vec![]), EnumValidator::parse_enums)?;
130        let interfaces = obj.get("interfaces").map_or(Ok(vec![]), |v| self.parse_interfaces(v))?;
131        let unions = obj.get("unions").map_or(Ok(vec![]), |v| self.parse_unions(v))?;
132        let input_types =
133            obj.get("input_types").map_or(Ok(vec![]), |v| self.parse_input_types(v))?;
134        let scalars = obj.get("scalars").map_or(Ok(vec![]), |v| self.parse_scalars(v))?;
135
136        // Warn about unsupported fragments feature
137        if obj.contains_key("fragments") {
138            tracing::warn!(
139                "'fragments' feature in schema is not yet supported and will be ignored"
140            );
141        }
142
143        Ok(AuthoringIR {
144            types,
145            enums,
146            interfaces,
147            unions,
148            input_types,
149            scalars,
150            queries,
151            mutations,
152            subscriptions,
153            fact_tables,
154        })
155    }
156
157    fn parse_types(&self, value: &Value) -> Result<Vec<IRType>> {
158        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
159            message:  "types must be an array".to_string(),
160            location: "types".to_string(),
161        })?;
162
163        array
164            .iter()
165            .enumerate()
166            .map(|(i, type_val)| self.parse_type(type_val, i))
167            .collect()
168    }
169
170    fn parse_type(&self, value: &Value, index: usize) -> Result<IRType> {
171        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
172            message:  format!("Type at index {index} must be an object"),
173            location: format!("types[{index}]"),
174        })?;
175
176        let name = obj
177            .get("name")
178            .and_then(|v| v.as_str())
179            .ok_or_else(|| FraiseQLError::Parse {
180                message:  format!("Type at index {index} missing 'name' field"),
181                location: format!("types[{index}].name"),
182            })?
183            .to_string();
184
185        let fields = if let Some(fields_val) = obj.get("fields") {
186            self.parse_fields(fields_val, &name)?
187        } else {
188            Vec::new()
189        };
190
191        Ok(IRType {
192            name,
193            fields,
194            sql_source: obj.get("sql_source").and_then(|v| v.as_str()).map(String::from),
195            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
196        })
197    }
198
199    fn parse_fields(&self, value: &Value, type_name: &str) -> Result<Vec<IRField>> {
200        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
201            message:  format!("fields for type {type_name} must be an array"),
202            location: format!("{type_name}.fields"),
203        })?;
204
205        array
206            .iter()
207            .enumerate()
208            .map(|(i, field_val)| self.parse_field(field_val, type_name, i))
209            .collect()
210    }
211
212    fn parse_field(&self, value: &Value, type_name: &str, index: usize) -> Result<IRField> {
213        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
214            message:  format!("Field at index {index} in type {type_name} must be an object"),
215            location: format!("{type_name}.fields[{index}]"),
216        })?;
217
218        let name = obj
219            .get("name")
220            .and_then(|v| v.as_str())
221            .ok_or_else(|| FraiseQLError::Parse {
222                message:  format!("Field at index {index} in type {type_name} missing 'name'"),
223                location: format!("{type_name}.fields[{index}].name"),
224            })?
225            .to_string();
226
227        let field_type = obj
228            .get("type")
229            .and_then(|v| v.as_str())
230            .ok_or_else(|| FraiseQLError::Parse {
231                message:  format!("Field '{name}' in type {type_name} missing 'type'"),
232                location: format!("{type_name}.fields.{name}.type"),
233            })?
234            .to_string();
235
236        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
237
238        Ok(IRField {
239            name,
240            field_type,
241            nullable,
242            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
243            sql_column: obj.get("sql_column").and_then(|v| v.as_str()).map(String::from),
244        })
245    }
246
247    fn parse_queries(&self, value: &Value) -> Result<Vec<IRQuery>> {
248        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
249            message:  "queries must be an array".to_string(),
250            location: "queries".to_string(),
251        })?;
252
253        array
254            .iter()
255            .enumerate()
256            .map(|(i, query_val)| self.parse_query(query_val, i))
257            .collect()
258    }
259
260    fn parse_query(&self, value: &Value, index: usize) -> Result<IRQuery> {
261        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
262            message:  format!("Query at index {index} must be an object"),
263            location: format!("queries[{index}]"),
264        })?;
265
266        let name = obj
267            .get("name")
268            .and_then(|v| v.as_str())
269            .ok_or_else(|| FraiseQLError::Parse {
270                message:  format!("Query at index {index} missing 'name'"),
271                location: format!("queries[{index}].name"),
272            })?
273            .to_string();
274
275        let return_type = obj
276            .get("return_type")
277            .and_then(|v| v.as_str())
278            .ok_or_else(|| FraiseQLError::Parse {
279                message:  format!("Query '{name}' missing 'return_type'"),
280                location: format!("queries.{name}.return_type"),
281            })?
282            .to_string();
283
284        let returns_list = obj.get("returns_list").and_then(|v| v.as_bool()).unwrap_or(false);
285
286        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
287
288        let arguments = if let Some(args_val) = obj.get("arguments") {
289            self.parse_arguments(args_val, &name)?
290        } else {
291            Vec::new()
292        };
293
294        let auto_params = if let Some(auto_val) = obj.get("auto_params") {
295            self.parse_auto_params(auto_val)?
296        } else {
297            AutoParams::default()
298        };
299
300        Ok(IRQuery {
301            name,
302            return_type,
303            returns_list,
304            nullable,
305            arguments,
306            sql_source: obj.get("sql_source").and_then(|v| v.as_str()).map(String::from),
307            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
308            auto_params,
309        })
310    }
311
312    fn parse_mutations(&self, value: &Value) -> Result<Vec<IRMutation>> {
313        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
314            message:  "mutations must be an array".to_string(),
315            location: "mutations".to_string(),
316        })?;
317
318        array
319            .iter()
320            .enumerate()
321            .map(|(i, mutation_val)| self.parse_mutation(mutation_val, i))
322            .collect()
323    }
324
325    fn parse_mutation(&self, value: &Value, index: usize) -> Result<IRMutation> {
326        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
327            message:  format!("Mutation at index {index} must be an object"),
328            location: format!("mutations[{index}]"),
329        })?;
330
331        let name = obj
332            .get("name")
333            .and_then(|v| v.as_str())
334            .ok_or_else(|| FraiseQLError::Parse {
335                message:  format!("Mutation at index {index} missing 'name'"),
336                location: format!("mutations[{index}].name"),
337            })?
338            .to_string();
339
340        let return_type = obj
341            .get("return_type")
342            .and_then(|v| v.as_str())
343            .ok_or_else(|| FraiseQLError::Parse {
344                message:  format!("Mutation '{name}' missing 'return_type'"),
345                location: format!("mutations.{name}.return_type"),
346            })?
347            .to_string();
348
349        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
350
351        let arguments = if let Some(args_val) = obj.get("arguments") {
352            self.parse_arguments(args_val, &name)?
353        } else {
354            Vec::new()
355        };
356
357        let operation = if let Some(s) = obj.get("operation").and_then(|v| v.as_str()) {
358            match s.to_lowercase().as_str() {
359                "create" => MutationOperation::Create,
360                "update" => MutationOperation::Update,
361                "delete" => MutationOperation::Delete,
362                "custom" => MutationOperation::Custom,
363                other => {
364                    return Err(FraiseQLError::Parse {
365                        message:  format!(
366                            "Mutation '{name}' has unknown operation {other:?}. \
367                             Valid values are: create, update, delete, custom"
368                        ),
369                        location: format!("mutations.{name}.operation"),
370                    });
371                },
372            }
373        } else {
374            MutationOperation::Custom
375        };
376
377        Ok(IRMutation {
378            name,
379            return_type,
380            nullable,
381            arguments,
382            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
383            operation,
384        })
385    }
386
387    fn parse_subscriptions(&self, value: &Value) -> Result<Vec<IRSubscription>> {
388        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
389            message:  "subscriptions must be an array".to_string(),
390            location: "subscriptions".to_string(),
391        })?;
392
393        array
394            .iter()
395            .enumerate()
396            .map(|(i, sub_val)| self.parse_subscription(sub_val, i))
397            .collect()
398    }
399
400    fn parse_subscription(&self, value: &Value, index: usize) -> Result<IRSubscription> {
401        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
402            message:  format!("Subscription at index {index} must be an object"),
403            location: format!("subscriptions[{index}]"),
404        })?;
405
406        let name = obj
407            .get("name")
408            .and_then(|v| v.as_str())
409            .ok_or_else(|| FraiseQLError::Parse {
410                message:  format!("Subscription at index {index} missing 'name'"),
411                location: format!("subscriptions[{index}].name"),
412            })?
413            .to_string();
414
415        let return_type = obj
416            .get("return_type")
417            .and_then(|v| v.as_str())
418            .ok_or_else(|| FraiseQLError::Parse {
419                message:  format!("Subscription '{name}' missing 'return_type'"),
420                location: format!("subscriptions.{name}.return_type"),
421            })?
422            .to_string();
423
424        let arguments = if let Some(args_val) = obj.get("arguments") {
425            self.parse_arguments(args_val, &name)?
426        } else {
427            Vec::new()
428        };
429
430        Ok(IRSubscription {
431            name,
432            return_type,
433            arguments,
434            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
435        })
436    }
437
438    fn parse_arguments(&self, value: &Value, parent_name: &str) -> Result<Vec<IRArgument>> {
439        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
440            message:  format!("arguments for '{parent_name}' must be an array"),
441            location: format!("{parent_name}.arguments"),
442        })?;
443
444        array
445            .iter()
446            .enumerate()
447            .map(|(i, arg_val)| self.parse_argument(arg_val, parent_name, i))
448            .collect()
449    }
450
451    fn parse_argument(&self, value: &Value, parent_name: &str, index: usize) -> Result<IRArgument> {
452        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
453            message:  format!("Argument at index {index} for '{parent_name}' must be an object"),
454            location: format!("{parent_name}.arguments[{index}]"),
455        })?;
456
457        let name = obj
458            .get("name")
459            .and_then(|v| v.as_str())
460            .ok_or_else(|| FraiseQLError::Parse {
461                message:  format!("Argument at index {index} for '{parent_name}' missing 'name'"),
462                location: format!("{parent_name}.arguments[{index}].name"),
463            })?
464            .to_string();
465
466        let arg_type = obj
467            .get("type")
468            .and_then(|v| v.as_str())
469            .ok_or_else(|| FraiseQLError::Parse {
470                message:  format!("Argument '{name}' for '{parent_name}' missing 'type'"),
471                location: format!("{parent_name}.arguments.{name}.type"),
472            })?
473            .to_string();
474
475        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
476
477        Ok(IRArgument {
478            name,
479            arg_type,
480            nullable,
481            default_value: obj.get("default_value").map(GraphQLValue::from_json).transpose()?,
482            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
483        })
484    }
485
486    fn parse_auto_params(&self, value: &Value) -> Result<AutoParams> {
487        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
488            message:  "auto_params must be an object".to_string(),
489            location: "auto_params".to_string(),
490        })?;
491
492        Ok(AutoParams {
493            has_where:    obj.get("has_where").and_then(|v| v.as_bool()).unwrap_or(false),
494            has_order_by: obj.get("has_order_by").and_then(|v| v.as_bool()).unwrap_or(false),
495            has_limit:    obj.get("has_limit").and_then(|v| v.as_bool()).unwrap_or(false),
496            has_offset:   obj.get("has_offset").and_then(|v| v.as_bool()).unwrap_or(false),
497        })
498    }
499
500    fn parse_interfaces(&self, value: &Value) -> Result<Vec<IRInterface>> {
501        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
502            message:  "interfaces must be an array".to_string(),
503            location: "interfaces".to_string(),
504        })?;
505
506        array
507            .iter()
508            .enumerate()
509            .map(|(i, interface_val)| self.parse_interface(interface_val, i))
510            .collect()
511    }
512
513    fn parse_interface(&self, value: &Value, index: usize) -> Result<IRInterface> {
514        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
515            message:  format!("Interface at index {index} must be an object"),
516            location: format!("interfaces[{index}]"),
517        })?;
518
519        let name = obj
520            .get("name")
521            .and_then(|v| v.as_str())
522            .ok_or_else(|| FraiseQLError::Parse {
523                message:  format!("Interface at index {index} missing 'name' field"),
524                location: format!("interfaces[{index}].name"),
525            })?
526            .to_string();
527
528        let fields = if let Some(fields_val) = obj.get("fields") {
529            self.parse_fields(fields_val, &name)?
530        } else {
531            Vec::new()
532        };
533
534        Ok(IRInterface {
535            name,
536            fields,
537            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
538        })
539    }
540
541    fn parse_unions(&self, value: &Value) -> Result<Vec<IRUnion>> {
542        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
543            message:  "unions must be an array".to_string(),
544            location: "unions".to_string(),
545        })?;
546
547        array
548            .iter()
549            .enumerate()
550            .map(|(i, union_val)| self.parse_union(union_val, i))
551            .collect()
552    }
553
554    fn parse_union(&self, value: &Value, index: usize) -> Result<IRUnion> {
555        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
556            message:  format!("Union at index {index} must be an object"),
557            location: format!("unions[{index}]"),
558        })?;
559
560        let name = obj
561            .get("name")
562            .and_then(|v| v.as_str())
563            .ok_or_else(|| FraiseQLError::Parse {
564                message:  format!("Union at index {index} missing 'name' field"),
565                location: format!("unions[{index}].name"),
566            })?
567            .to_string();
568
569        let types = if let Some(types_val) = obj.get("types") {
570            let array = types_val.as_array().ok_or_else(|| FraiseQLError::Parse {
571                message:  format!("'types' for union {name} must be an array"),
572                location: format!("unions.{name}.types"),
573            })?;
574
575            array
576                .iter()
577                .enumerate()
578                .map(|(i, type_val)| {
579                    type_val.as_str().ok_or_else(|| FraiseQLError::Parse {
580                        message:  format!("Type at index {i} in union {name} must be a string"),
581                        location: format!("unions.{name}.types[{i}]"),
582                    })
583                })
584                .collect::<Result<Vec<_>>>()?
585                .iter()
586                .map(|s| (*s).to_string())
587                .collect()
588        } else {
589            Vec::new()
590        };
591
592        Ok(IRUnion {
593            name,
594            types,
595            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
596        })
597    }
598
599    fn parse_input_types(&self, value: &Value) -> Result<Vec<IRInputType>> {
600        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
601            message:  "input_types must be an array".to_string(),
602            location: "input_types".to_string(),
603        })?;
604
605        array
606            .iter()
607            .enumerate()
608            .map(|(i, input_type_val)| self.parse_input_type(input_type_val, i))
609            .collect()
610    }
611
612    fn parse_input_type(&self, value: &Value, index: usize) -> Result<IRInputType> {
613        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
614            message:  format!("Input type at index {index} must be an object"),
615            location: format!("input_types[{index}]"),
616        })?;
617
618        let name = obj
619            .get("name")
620            .and_then(|v| v.as_str())
621            .ok_or_else(|| FraiseQLError::Parse {
622                message:  format!("Input type at index {index} missing 'name' field"),
623                location: format!("input_types[{index}].name"),
624            })?
625            .to_string();
626
627        let fields = if let Some(fields_val) = obj.get("fields") {
628            self.parse_input_fields(fields_val, &name)?
629        } else {
630            Vec::new()
631        };
632
633        Ok(IRInputType {
634            name,
635            fields,
636            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
637        })
638    }
639
640    fn parse_input_fields(&self, value: &Value, type_name: &str) -> Result<Vec<IRInputField>> {
641        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
642            message:  format!("fields for input type {type_name} must be an array"),
643            location: format!("{type_name}.fields"),
644        })?;
645
646        array
647            .iter()
648            .enumerate()
649            .map(|(i, field_val)| self.parse_input_field(field_val, type_name, i))
650            .collect()
651    }
652
653    fn parse_input_field(
654        &self,
655        value: &Value,
656        type_name: &str,
657        index: usize,
658    ) -> Result<IRInputField> {
659        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
660            message:  format!("Input field at index {index} in type {type_name} must be an object"),
661            location: format!("{type_name}.fields[{index}]"),
662        })?;
663
664        let name = obj
665            .get("name")
666            .and_then(|v| v.as_str())
667            .ok_or_else(|| FraiseQLError::Parse {
668                message:  format!(
669                    "Input field at index {index} in type {type_name} missing 'name'"
670                ),
671                location: format!("{type_name}.fields[{index}].name"),
672            })?
673            .to_string();
674
675        let field_type = obj
676            .get("type")
677            .and_then(|v| v.as_str())
678            .ok_or_else(|| FraiseQLError::Parse {
679                message:  format!("Input field '{name}' in type {type_name} missing 'type'"),
680                location: format!("{type_name}.fields.{name}.type"),
681            })?
682            .to_string();
683
684        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
685
686        Ok(IRInputField {
687            name,
688            field_type,
689            nullable,
690            default_value: obj.get("default_value").map(GraphQLValue::from_json).transpose()?,
691            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
692        })
693    }
694
695    fn parse_scalars(&self, value: &Value) -> Result<Vec<IRScalar>> {
696        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
697            message:  "scalars must be an array".to_string(),
698            location: "scalars".to_string(),
699        })?;
700
701        array
702            .iter()
703            .enumerate()
704            .map(|(i, scalar_val)| self.parse_scalar(scalar_val, i))
705            .collect()
706    }
707
708    fn parse_scalar(&self, value: &Value, index: usize) -> Result<IRScalar> {
709        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
710            message:  format!("Scalar at index {index} must be an object"),
711            location: format!("scalars[{index}]"),
712        })?;
713
714        let name = obj
715            .get("name")
716            .and_then(|v| v.as_str())
717            .ok_or_else(|| FraiseQLError::Parse {
718                message:  format!("Scalar at index {index} missing 'name' field"),
719                location: format!("scalars[{index}].name"),
720            })?
721            .to_string();
722
723        let description = obj.get("description").and_then(|v| v.as_str()).map(String::from);
724        let specified_by_url =
725            obj.get("specified_by_url").and_then(|v| v.as_str()).map(String::from);
726        let base_type = obj.get("base_type").and_then(|v| v.as_str()).map(String::from);
727
728        // Parse validation rules if present
729        let validation_rules = if let Some(rules_val) = obj.get("validation_rules") {
730            serde_json::from_value(rules_val.clone()).unwrap_or_default()
731        } else {
732            Vec::new()
733        };
734
735        Ok(IRScalar {
736            name,
737            description,
738            specified_by_url,
739            validation_rules,
740            base_type,
741        })
742    }
743}
744
745impl Default for SchemaParser {
746    fn default() -> Self {
747        Self::new()
748    }
749}