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}
750
751#[cfg(test)]
752mod tests {
753    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
754
755    use super::*;
756
757    #[test]
758    fn test_parse_empty_schema() {
759        let parser = SchemaParser::new();
760        let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
761        let ir = parser.parse(json).unwrap();
762
763        assert!(ir.types.is_empty());
764        assert!(ir.queries.is_empty());
765        assert!(ir.mutations.is_empty());
766        assert!(ir.subscriptions.is_empty());
767    }
768
769    #[test]
770    fn test_parse_minimal_schema() {
771        let parser = SchemaParser::new();
772        let json = r"{}";
773        let ir = parser.parse(json).unwrap();
774
775        assert!(ir.types.is_empty());
776        assert!(ir.queries.is_empty());
777    }
778
779    #[test]
780    fn test_parse_type_with_fields() {
781        let parser = SchemaParser::new();
782        let json = r#"{
783            "types": [{
784                "name": "User",
785                "fields": [
786                    {"name": "id", "type": "Int!", "nullable": false},
787                    {"name": "name", "type": "String!", "nullable": false}
788                ],
789                "sql_source": "v_user"
790            }]
791        }"#;
792
793        let ir = parser.parse(json).unwrap();
794        assert_eq!(ir.types.len(), 1);
795        assert_eq!(ir.types[0].name, "User");
796        assert_eq!(ir.types[0].fields.len(), 2);
797        assert_eq!(ir.types[0].sql_source, Some("v_user".to_string()));
798    }
799
800    #[test]
801    fn test_parse_query_with_auto_params() {
802        let parser = SchemaParser::new();
803        let json = r#"{
804            "queries": [{
805                "name": "users",
806                "return_type": "User",
807                "returns_list": true,
808                "nullable": false,
809                "sql_source": "v_user",
810                "auto_params": {
811                    "has_where": true,
812                    "has_limit": true
813                }
814            }]
815        }"#;
816
817        let ir = parser.parse(json).unwrap();
818        assert_eq!(ir.queries.len(), 1);
819        assert_eq!(ir.queries[0].name, "users");
820        assert!(ir.queries[0].returns_list);
821        assert!(ir.queries[0].auto_params.has_where);
822        assert!(ir.queries[0].auto_params.has_limit);
823    }
824
825    #[test]
826    fn test_parse_mutation() {
827        let parser = SchemaParser::new();
828        let json = r#"{
829            "mutations": [{
830                "name": "createUser",
831                "return_type": "User",
832                "nullable": false,
833                "operation": "create",
834                "arguments": [
835                    {"name": "input", "type": "CreateUserInput!", "nullable": false}
836                ]
837            }]
838        }"#;
839
840        let ir = parser.parse(json).unwrap();
841        assert_eq!(ir.mutations.len(), 1);
842        assert_eq!(ir.mutations[0].name, "createUser");
843        assert_eq!(ir.mutations[0].operation, MutationOperation::Create);
844        assert_eq!(ir.mutations[0].arguments.len(), 1);
845    }
846
847    #[test]
848    fn test_parse_mutation_operation_case_insensitive() {
849        // Test that operation strings are accepted in any case (lowercase, uppercase, mixed)
850        let parser = SchemaParser::new();
851
852        let cases: &[(&str, MutationOperation)] = &[
853            ("create", MutationOperation::Create),
854            ("CREATE", MutationOperation::Create),
855            ("Create", MutationOperation::Create),
856            ("update", MutationOperation::Update),
857            ("UPDATE", MutationOperation::Update),
858            ("delete", MutationOperation::Delete),
859            ("DELETE", MutationOperation::Delete),
860            ("custom", MutationOperation::Custom),
861            ("CUSTOM", MutationOperation::Custom),
862        ];
863
864        for (op_str, expected) in cases {
865            let json = format!(
866                r#"{{"mutations": [{{"name": "m", "return_type": "T", "nullable": false, "operation": "{op_str}", "arguments": []}}]}}"#
867            );
868            let ir = parser.parse(&json).unwrap_or_else(|e| {
869                panic!("Expected parse to succeed for operation {op_str:?}, got error: {e}")
870            });
871            assert_eq!(
872                ir.mutations[0].operation, *expected,
873                "operation {op_str:?} should map to {expected:?}"
874            );
875        }
876    }
877
878    #[test]
879    fn test_parse_mutation_operation_missing_defaults_to_custom() {
880        let parser = SchemaParser::new();
881        let json = r#"{"mutations": [{"name": "m", "return_type": "T", "nullable": false, "arguments": []}]}"#;
882        let ir = parser.parse(json).unwrap();
883        assert_eq!(ir.mutations[0].operation, MutationOperation::Custom);
884    }
885
886    #[test]
887    fn test_parse_mutation_operation_typo_returns_error() {
888        let parser = SchemaParser::new();
889        let invalid_ops = &["creat", "CREAT", "updaet", "delet", "FUNCTION", "insert"];
890        for op in invalid_ops {
891            let json = format!(
892                r#"{{"mutations": [{{"name": "m", "return_type": "T", "nullable": false, "operation": "{op}", "arguments": []}}]}}"#
893            );
894            let result = parser.parse(&json);
895            assert!(
896                result.is_err(),
897                "Expected parse error for unknown operation {op:?}, but got Ok"
898            );
899            let err = result.unwrap_err().to_string();
900            assert!(
901                err.contains("unknown operation"),
902                "Error for {op:?} should mention 'unknown operation', got: {err}"
903            );
904        }
905    }
906
907    #[test]
908    fn test_parse_invalid_json() {
909        let parser = SchemaParser::new();
910        let json = "not valid json";
911        let result = parser.parse(json);
912        assert!(
913            matches!(result, Err(FraiseQLError::Parse { .. })),
914            "expected Parse error for invalid JSON, got: {result:?}"
915        );
916    }
917
918    #[test]
919    fn test_parse_missing_required_field() {
920        let parser = SchemaParser::new();
921        let json = r#"{
922            "types": [{
923                "fields": []
924            }]
925        }"#;
926        let result = parser.parse(json);
927        assert!(
928            matches!(result, Err(FraiseQLError::Parse { .. })),
929            "expected Parse error for missing required field, got: {result:?}"
930        );
931    }
932
933    #[test]
934    fn test_parse_interface_basic() {
935        let parser = SchemaParser::new();
936        let json = r#"{
937            "interfaces": [{
938                "name": "Node",
939                "fields": [
940                    {"name": "id", "type": "ID!", "nullable": false}
941                ]
942            }]
943        }"#;
944
945        let ir = parser.parse(json).unwrap();
946        assert_eq!(ir.interfaces.len(), 1);
947        assert_eq!(ir.interfaces[0].name, "Node");
948        assert_eq!(ir.interfaces[0].fields.len(), 1);
949        assert_eq!(ir.interfaces[0].fields[0].name, "id");
950    }
951
952    #[test]
953    fn test_parse_interface_with_multiple_fields() {
954        let parser = SchemaParser::new();
955        let json = r#"{
956            "interfaces": [{
957                "name": "Timestamped",
958                "fields": [
959                    {"name": "createdAt", "type": "String!", "nullable": false},
960                    {"name": "updatedAt", "type": "String!", "nullable": false}
961                ],
962                "description": "Records creation and update times"
963            }]
964        }"#;
965
966        let ir = parser.parse(json).unwrap();
967        assert_eq!(ir.interfaces[0].fields.len(), 2);
968        assert_eq!(
969            ir.interfaces[0].description,
970            Some("Records creation and update times".to_string())
971        );
972    }
973
974    #[test]
975    fn test_parse_interface_with_empty_fields() {
976        let parser = SchemaParser::new();
977        let json = r#"{
978            "interfaces": [{
979                "name": "Empty",
980                "fields": []
981            }]
982        }"#;
983
984        let ir = parser.parse(json).unwrap();
985        assert_eq!(ir.interfaces.len(), 1);
986        assert_eq!(ir.interfaces[0].fields.len(), 0);
987    }
988
989    #[test]
990    fn test_parse_multiple_interfaces() {
991        let parser = SchemaParser::new();
992        let json = r#"{
993            "interfaces": [
994                {"name": "Node", "fields": []},
995                {"name": "Auditable", "fields": []},
996                {"name": "Publishable", "fields": []}
997            ]
998        }"#;
999
1000        let ir = parser.parse(json).unwrap();
1001        assert_eq!(ir.interfaces.len(), 3);
1002        assert_eq!(ir.interfaces[0].name, "Node");
1003        assert_eq!(ir.interfaces[1].name, "Auditable");
1004        assert_eq!(ir.interfaces[2].name, "Publishable");
1005    }
1006
1007    #[test]
1008    fn test_parse_interface_missing_name() {
1009        let parser = SchemaParser::new();
1010        let json = r#"{"interfaces": [{"fields": []}]}"#;
1011        let result = parser.parse(json);
1012        assert!(
1013            matches!(result, Err(FraiseQLError::Parse { .. })),
1014            "expected Parse error for interface missing name, got: {result:?}"
1015        );
1016    }
1017
1018    #[test]
1019    fn test_parse_union_basic() {
1020        let parser = SchemaParser::new();
1021        let json = r#"{
1022            "unions": [{
1023                "name": "SearchResult",
1024                "types": ["User", "Post"]
1025            }]
1026        }"#;
1027
1028        let ir = parser.parse(json).unwrap();
1029        assert_eq!(ir.unions.len(), 1);
1030        assert_eq!(ir.unions[0].name, "SearchResult");
1031        assert_eq!(ir.unions[0].types.len(), 2);
1032        assert_eq!(ir.unions[0].types[0], "User");
1033        assert_eq!(ir.unions[0].types[1], "Post");
1034    }
1035
1036    #[test]
1037    fn test_parse_union_single_type() {
1038        let parser = SchemaParser::new();
1039        let json = r#"{
1040            "unions": [{
1041                "name": "Result",
1042                "types": ["Error"]
1043            }]
1044        }"#;
1045
1046        let ir = parser.parse(json).unwrap();
1047        assert_eq!(ir.unions[0].types.len(), 1);
1048        assert_eq!(ir.unions[0].types[0], "Error");
1049    }
1050
1051    #[test]
1052    fn test_parse_union_with_description() {
1053        let parser = SchemaParser::new();
1054        let json = r#"{
1055            "unions": [{
1056                "name": "SearchResult",
1057                "types": ["User", "Post", "Comment"],
1058                "description": "Results from search"
1059            }]
1060        }"#;
1061
1062        let ir = parser.parse(json).unwrap();
1063        assert_eq!(ir.unions[0].description, Some("Results from search".to_string()));
1064        assert_eq!(ir.unions[0].types.len(), 3);
1065    }
1066
1067    #[test]
1068    fn test_parse_multiple_unions() {
1069        let parser = SchemaParser::new();
1070        let json = r#"{
1071            "unions": [
1072                {"name": "SearchResult", "types": ["User", "Post"]},
1073                {"name": "Error", "types": ["ValidationError", "NotFoundError"]},
1074                {"name": "Response", "types": ["Success", "Error"]}
1075            ]
1076        }"#;
1077
1078        let ir = parser.parse(json).unwrap();
1079        assert_eq!(ir.unions.len(), 3);
1080        assert_eq!(ir.unions[0].name, "SearchResult");
1081        assert_eq!(ir.unions[1].name, "Error");
1082        assert_eq!(ir.unions[2].name, "Response");
1083    }
1084
1085    #[test]
1086    fn test_parse_union_missing_name() {
1087        let parser = SchemaParser::new();
1088        let json = r#"{"unions": [{"types": []}]}"#;
1089        let result = parser.parse(json);
1090        assert!(
1091            matches!(result, Err(FraiseQLError::Parse { .. })),
1092            "expected Parse error for union missing name, got: {result:?}"
1093        );
1094    }
1095
1096    #[test]
1097    fn test_parse_union_empty_types() {
1098        let parser = SchemaParser::new();
1099        let json = r#"{
1100            "unions": [{
1101                "name": "Empty",
1102                "types": []
1103            }]
1104        }"#;
1105
1106        let ir = parser.parse(json).unwrap();
1107        assert_eq!(ir.unions[0].types.len(), 0);
1108    }
1109
1110    #[test]
1111    fn test_parse_input_type_basic() {
1112        let parser = SchemaParser::new();
1113        let json = r#"{
1114            "input_types": [{
1115                "name": "UserInput",
1116                "fields": [
1117                    {"name": "name", "type": "String!", "nullable": false}
1118                ]
1119            }]
1120        }"#;
1121
1122        let ir = parser.parse(json).unwrap();
1123        assert_eq!(ir.input_types.len(), 1);
1124        assert_eq!(ir.input_types[0].name, "UserInput");
1125        assert_eq!(ir.input_types[0].fields.len(), 1);
1126        assert_eq!(ir.input_types[0].fields[0].name, "name");
1127    }
1128
1129    #[test]
1130    fn test_parse_input_type_with_multiple_fields() {
1131        let parser = SchemaParser::new();
1132        let json = r#"{
1133            "input_types": [{
1134                "name": "CreateUserInput",
1135                "fields": [
1136                    {"name": "name", "type": "String!", "nullable": false},
1137                    {"name": "email", "type": "String!", "nullable": false},
1138                    {"name": "age", "type": "Int", "nullable": true}
1139                ],
1140                "description": "Input for creating users"
1141            }]
1142        }"#;
1143
1144        let ir = parser.parse(json).unwrap();
1145        assert_eq!(ir.input_types[0].fields.len(), 3);
1146        assert!(!ir.input_types[0].fields[0].nullable);
1147        assert!(ir.input_types[0].fields[2].nullable);
1148        assert_eq!(ir.input_types[0].description, Some("Input for creating users".to_string()));
1149    }
1150
1151    #[test]
1152    fn test_parse_input_field_with_default_value() {
1153        let parser = SchemaParser::new();
1154        let json = r#"{
1155            "input_types": [{
1156                "name": "QueryInput",
1157                "fields": [
1158                    {"name": "limit", "type": "Int", "nullable": true, "default_value": 10},
1159                    {"name": "active", "type": "Boolean", "nullable": true, "default_value": true}
1160                ]
1161            }]
1162        }"#;
1163
1164        let ir = parser.parse(json).unwrap();
1165        assert_eq!(ir.input_types[0].fields[0].default_value, Some(GraphQLValue::Int(10)));
1166        assert_eq!(ir.input_types[0].fields[1].default_value, Some(GraphQLValue::Boolean(true)));
1167    }
1168
1169    #[test]
1170    fn test_parse_input_type_with_empty_fields() {
1171        let parser = SchemaParser::new();
1172        let json = r#"{
1173            "input_types": [{
1174                "name": "EmptyInput",
1175                "fields": []
1176            }]
1177        }"#;
1178
1179        let ir = parser.parse(json).unwrap();
1180        assert_eq!(ir.input_types[0].fields.len(), 0);
1181    }
1182
1183    #[test]
1184    fn test_parse_multiple_input_types() {
1185        let parser = SchemaParser::new();
1186        let json = r#"{
1187            "input_types": [
1188                {"name": "UserInput", "fields": []},
1189                {"name": "PostInput", "fields": []},
1190                {"name": "FilterInput", "fields": []}
1191            ]
1192        }"#;
1193
1194        let ir = parser.parse(json).unwrap();
1195        assert_eq!(ir.input_types.len(), 3);
1196        assert_eq!(ir.input_types[0].name, "UserInput");
1197        assert_eq!(ir.input_types[1].name, "PostInput");
1198        assert_eq!(ir.input_types[2].name, "FilterInput");
1199    }
1200
1201    #[test]
1202    fn test_parse_input_type_missing_name() {
1203        let parser = SchemaParser::new();
1204        let json = r#"{"input_types": [{"fields": []}]}"#;
1205        let result = parser.parse(json);
1206        assert!(
1207            matches!(result, Err(FraiseQLError::Parse { .. })),
1208            "expected Parse error for input type missing name, got: {result:?}"
1209        );
1210    }
1211
1212    #[test]
1213    fn test_parse_complete_schema_with_all_features() {
1214        let parser = SchemaParser::new();
1215        let json = r#"{
1216            "types": [{"name": "User", "fields": []}],
1217            "interfaces": [{"name": "Node", "fields": []}],
1218            "unions": [{"name": "SearchResult", "types": ["User"]}],
1219            "input_types": [{"name": "UserInput", "fields": []}],
1220            "queries": [{"name": "users", "return_type": "User", "returns_list": true}],
1221            "mutations": [{"name": "createUser", "return_type": "User", "operation": "create"}]
1222        }"#;
1223
1224        let ir = parser.parse(json).unwrap();
1225        assert_eq!(ir.types.len(), 1);
1226        assert_eq!(ir.interfaces.len(), 1);
1227        assert_eq!(ir.unions.len(), 1);
1228        assert_eq!(ir.input_types.len(), 1);
1229        assert_eq!(ir.queries.len(), 1);
1230        assert_eq!(ir.mutations.len(), 1);
1231    }
1232
1233    #[test]
1234    fn test_parse_scalars() {
1235        let parser = SchemaParser::new();
1236        let json = r#"{
1237            "scalars": [
1238                {
1239                    "name": "Email",
1240                    "description": "Valid email address",
1241                    "specified_by_url": "https://html.spec.whatwg.org/",
1242                    "validation_rules": [],
1243                    "base_type": null
1244                },
1245                {
1246                    "name": "ISBN",
1247                    "description": "International Standard Book Number",
1248                    "specified_by_url": null,
1249                    "validation_rules": [],
1250                    "base_type": null
1251                }
1252            ]
1253        }"#;
1254
1255        let ir = parser.parse(json).unwrap();
1256        assert_eq!(ir.scalars.len(), 2);
1257        assert_eq!(ir.scalars[0].name, "Email");
1258        assert_eq!(ir.scalars[0].description, Some("Valid email address".to_string()));
1259        assert_eq!(
1260            ir.scalars[0].specified_by_url,
1261            Some("https://html.spec.whatwg.org/".to_string())
1262        );
1263        assert_eq!(ir.scalars[1].name, "ISBN");
1264    }
1265
1266    #[test]
1267    fn test_parse_schema_with_scalars_and_types() {
1268        let parser = SchemaParser::new();
1269        let json = r#"{
1270            "scalars": [{"name": "Email", "description": null, "specified_by_url": null, "validation_rules": [], "base_type": null}],
1271            "types": [{"name": "User", "fields": []}],
1272            "queries": [{"name": "users", "return_type": "User", "returns_list": true}]
1273        }"#;
1274
1275        let ir = parser.parse(json).unwrap();
1276        assert_eq!(ir.scalars.len(), 1);
1277        assert_eq!(ir.types.len(), 1);
1278        assert_eq!(ir.queries.len(), 1);
1279    }
1280}