Skip to main content

fraiseql_core/compiler/
parser.rs

1//! Schema parser - JSON → Authoring IR.
2//!
3//! Parses JSON schema definitions from Python/TypeScript 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::error::{FraiseQLError, Result};
47
48/// Schema parser.
49///
50/// Transforms JSON schema from authoring languages into internal IR.
51pub struct SchemaParser {
52    // Parser state (if needed in future)
53}
54
55impl SchemaParser {
56    /// Create new schema parser.
57    #[must_use]
58    pub fn new() -> Self {
59        Self {}
60    }
61
62    /// Parse JSON schema into IR.
63    ///
64    /// # Arguments
65    ///
66    /// * `schema_json` - JSON schema string from decorators
67    ///
68    /// # Returns
69    ///
70    /// Parsed Authoring IR
71    ///
72    /// # Errors
73    ///
74    /// Returns error if JSON is malformed or missing required fields.
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// use fraiseql_core::compiler::parser::SchemaParser;
80    ///
81    /// let parser = SchemaParser::new();
82    /// let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
83    /// let ir = parser.parse(json).unwrap();
84    /// assert!(ir.types.is_empty());
85    /// ```
86    pub fn parse(&self, schema_json: &str) -> Result<AuthoringIR> {
87        // Parse JSON
88        let value: Value = serde_json::from_str(schema_json).map_err(|e| FraiseQLError::Parse {
89            message:  format!("Failed to parse schema JSON: {e}"),
90            location: "root".to_string(),
91        })?;
92
93        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
94            message:  "Schema must be a JSON object".to_string(),
95            location: "root".to_string(),
96        })?;
97
98        let types = obj.get("types").map_or(Ok(vec![]), |v| self.parse_types(v))?;
99        let queries = obj.get("queries").map_or(Ok(vec![]), |v| self.parse_queries(v))?;
100        let mutations = obj.get("mutations").map_or(Ok(vec![]), |v| self.parse_mutations(v))?;
101        let subscriptions =
102            obj.get("subscriptions").map_or(Ok(vec![]), |v| self.parse_subscriptions(v))?;
103        let fact_tables = obj
104            .get("fact_tables")
105            .and_then(Value::as_object)
106            .map_or_else(std::collections::HashMap::new, |o| {
107                o.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
108            });
109        let enums = obj.get("enums").map_or(Ok(vec![]), |v| EnumValidator::parse_enums(v))?;
110        let interfaces =
111            obj.get("interfaces").map_or(Ok(vec![]), |v| self.parse_interfaces(v))?;
112        let unions = obj.get("unions").map_or(Ok(vec![]), |v| self.parse_unions(v))?;
113        let input_types =
114            obj.get("input_types").map_or(Ok(vec![]), |v| self.parse_input_types(v))?;
115        let scalars = obj.get("scalars").map_or(Ok(vec![]), |v| self.parse_scalars(v))?;
116
117        // Warn about unsupported fragments feature
118        if obj.contains_key("fragments") {
119            eprintln!(
120                "Warning: 'fragments' feature in schema is not yet supported and will be ignored"
121            );
122        }
123
124        Ok(AuthoringIR {
125            types,
126            enums,
127            interfaces,
128            unions,
129            input_types,
130            scalars,
131            queries,
132            mutations,
133            subscriptions,
134            fact_tables,
135        })
136    }
137
138    fn parse_types(&self, value: &Value) -> Result<Vec<IRType>> {
139        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
140            message:  "types must be an array".to_string(),
141            location: "types".to_string(),
142        })?;
143
144        array
145            .iter()
146            .enumerate()
147            .map(|(i, type_val)| self.parse_type(type_val, i))
148            .collect()
149    }
150
151    fn parse_type(&self, value: &Value, index: usize) -> Result<IRType> {
152        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
153            message:  format!("Type at index {index} must be an object"),
154            location: format!("types[{index}]"),
155        })?;
156
157        let name = obj
158            .get("name")
159            .and_then(|v| v.as_str())
160            .ok_or_else(|| FraiseQLError::Parse {
161                message:  format!("Type at index {index} missing 'name' field"),
162                location: format!("types[{index}].name"),
163            })?
164            .to_string();
165
166        let fields = if let Some(fields_val) = obj.get("fields") {
167            self.parse_fields(fields_val, &name)?
168        } else {
169            Vec::new()
170        };
171
172        Ok(IRType {
173            name,
174            fields,
175            sql_source: obj.get("sql_source").and_then(|v| v.as_str()).map(String::from),
176            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
177        })
178    }
179
180    fn parse_fields(&self, value: &Value, type_name: &str) -> Result<Vec<IRField>> {
181        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
182            message:  format!("fields for type {type_name} must be an array"),
183            location: format!("{type_name}.fields"),
184        })?;
185
186        array
187            .iter()
188            .enumerate()
189            .map(|(i, field_val)| self.parse_field(field_val, type_name, i))
190            .collect()
191    }
192
193    fn parse_field(&self, value: &Value, type_name: &str, index: usize) -> Result<IRField> {
194        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
195            message:  format!("Field at index {index} in type {type_name} must be an object"),
196            location: format!("{type_name}.fields[{index}]"),
197        })?;
198
199        let name = obj
200            .get("name")
201            .and_then(|v| v.as_str())
202            .ok_or_else(|| FraiseQLError::Parse {
203                message:  format!("Field at index {index} in type {type_name} missing 'name'"),
204                location: format!("{type_name}.fields[{index}].name"),
205            })?
206            .to_string();
207
208        let field_type = obj
209            .get("type")
210            .and_then(|v| v.as_str())
211            .ok_or_else(|| FraiseQLError::Parse {
212                message:  format!("Field '{name}' in type {type_name} missing 'type'"),
213                location: format!("{type_name}.fields.{name}.type"),
214            })?
215            .to_string();
216
217        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
218
219        Ok(IRField {
220            name,
221            field_type,
222            nullable,
223            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
224            sql_column: obj.get("sql_column").and_then(|v| v.as_str()).map(String::from),
225        })
226    }
227
228    fn parse_queries(&self, value: &Value) -> Result<Vec<IRQuery>> {
229        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
230            message:  "queries must be an array".to_string(),
231            location: "queries".to_string(),
232        })?;
233
234        array
235            .iter()
236            .enumerate()
237            .map(|(i, query_val)| self.parse_query(query_val, i))
238            .collect()
239    }
240
241    fn parse_query(&self, value: &Value, index: usize) -> Result<IRQuery> {
242        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
243            message:  format!("Query at index {index} must be an object"),
244            location: format!("queries[{index}]"),
245        })?;
246
247        let name = obj
248            .get("name")
249            .and_then(|v| v.as_str())
250            .ok_or_else(|| FraiseQLError::Parse {
251                message:  format!("Query at index {index} missing 'name'"),
252                location: format!("queries[{index}].name"),
253            })?
254            .to_string();
255
256        let return_type = obj
257            .get("return_type")
258            .and_then(|v| v.as_str())
259            .ok_or_else(|| FraiseQLError::Parse {
260                message:  format!("Query '{name}' missing 'return_type'"),
261                location: format!("queries.{name}.return_type"),
262            })?
263            .to_string();
264
265        let returns_list = obj.get("returns_list").and_then(|v| v.as_bool()).unwrap_or(false);
266
267        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
268
269        let arguments = if let Some(args_val) = obj.get("arguments") {
270            self.parse_arguments(args_val, &name)?
271        } else {
272            Vec::new()
273        };
274
275        let auto_params = if let Some(auto_val) = obj.get("auto_params") {
276            self.parse_auto_params(auto_val)?
277        } else {
278            AutoParams::default()
279        };
280
281        Ok(IRQuery {
282            name,
283            return_type,
284            returns_list,
285            nullable,
286            arguments,
287            sql_source: obj.get("sql_source").and_then(|v| v.as_str()).map(String::from),
288            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
289            auto_params,
290        })
291    }
292
293    fn parse_mutations(&self, value: &Value) -> Result<Vec<IRMutation>> {
294        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
295            message:  "mutations must be an array".to_string(),
296            location: "mutations".to_string(),
297        })?;
298
299        array
300            .iter()
301            .enumerate()
302            .map(|(i, mutation_val)| self.parse_mutation(mutation_val, i))
303            .collect()
304    }
305
306    fn parse_mutation(&self, value: &Value, index: usize) -> Result<IRMutation> {
307        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
308            message:  format!("Mutation at index {index} must be an object"),
309            location: format!("mutations[{index}]"),
310        })?;
311
312        let name = obj
313            .get("name")
314            .and_then(|v| v.as_str())
315            .ok_or_else(|| FraiseQLError::Parse {
316                message:  format!("Mutation at index {index} missing 'name'"),
317                location: format!("mutations[{index}].name"),
318            })?
319            .to_string();
320
321        let return_type = obj
322            .get("return_type")
323            .and_then(|v| v.as_str())
324            .ok_or_else(|| FraiseQLError::Parse {
325                message:  format!("Mutation '{name}' missing 'return_type'"),
326                location: format!("mutations.{name}.return_type"),
327            })?
328            .to_string();
329
330        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
331
332        let arguments = if let Some(args_val) = obj.get("arguments") {
333            self.parse_arguments(args_val, &name)?
334        } else {
335            Vec::new()
336        };
337
338        let operation = if let Some(s) = obj.get("operation").and_then(|v| v.as_str()) {
339            match s.to_lowercase().as_str() {
340                "create" => MutationOperation::Create,
341                "update" => MutationOperation::Update,
342                "delete" => MutationOperation::Delete,
343                "custom" => MutationOperation::Custom,
344                other => {
345                    return Err(FraiseQLError::Parse {
346                        message:  format!(
347                            "Mutation '{name}' has unknown operation {other:?}. \
348                             Valid values are: create, update, delete, custom"
349                        ),
350                        location: format!("mutations.{name}.operation"),
351                    })
352                },
353            }
354        } else {
355            MutationOperation::Custom
356        };
357
358        Ok(IRMutation {
359            name,
360            return_type,
361            nullable,
362            arguments,
363            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
364            operation,
365        })
366    }
367
368    fn parse_subscriptions(&self, value: &Value) -> Result<Vec<IRSubscription>> {
369        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
370            message:  "subscriptions must be an array".to_string(),
371            location: "subscriptions".to_string(),
372        })?;
373
374        array
375            .iter()
376            .enumerate()
377            .map(|(i, sub_val)| self.parse_subscription(sub_val, i))
378            .collect()
379    }
380
381    fn parse_subscription(&self, value: &Value, index: usize) -> Result<IRSubscription> {
382        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
383            message:  format!("Subscription at index {index} must be an object"),
384            location: format!("subscriptions[{index}]"),
385        })?;
386
387        let name = obj
388            .get("name")
389            .and_then(|v| v.as_str())
390            .ok_or_else(|| FraiseQLError::Parse {
391                message:  format!("Subscription at index {index} missing 'name'"),
392                location: format!("subscriptions[{index}].name"),
393            })?
394            .to_string();
395
396        let return_type = obj
397            .get("return_type")
398            .and_then(|v| v.as_str())
399            .ok_or_else(|| FraiseQLError::Parse {
400                message:  format!("Subscription '{name}' missing 'return_type'"),
401                location: format!("subscriptions.{name}.return_type"),
402            })?
403            .to_string();
404
405        let arguments = if let Some(args_val) = obj.get("arguments") {
406            self.parse_arguments(args_val, &name)?
407        } else {
408            Vec::new()
409        };
410
411        Ok(IRSubscription {
412            name,
413            return_type,
414            arguments,
415            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
416        })
417    }
418
419    fn parse_arguments(&self, value: &Value, parent_name: &str) -> Result<Vec<IRArgument>> {
420        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
421            message:  format!("arguments for '{parent_name}' must be an array"),
422            location: format!("{parent_name}.arguments"),
423        })?;
424
425        array
426            .iter()
427            .enumerate()
428            .map(|(i, arg_val)| self.parse_argument(arg_val, parent_name, i))
429            .collect()
430    }
431
432    fn parse_argument(&self, value: &Value, parent_name: &str, index: usize) -> Result<IRArgument> {
433        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
434            message:  format!("Argument at index {index} for '{parent_name}' must be an object"),
435            location: format!("{parent_name}.arguments[{index}]"),
436        })?;
437
438        let name = obj
439            .get("name")
440            .and_then(|v| v.as_str())
441            .ok_or_else(|| FraiseQLError::Parse {
442                message:  format!("Argument at index {index} for '{parent_name}' missing 'name'"),
443                location: format!("{parent_name}.arguments[{index}].name"),
444            })?
445            .to_string();
446
447        let arg_type = obj
448            .get("type")
449            .and_then(|v| v.as_str())
450            .ok_or_else(|| FraiseQLError::Parse {
451                message:  format!("Argument '{name}' for '{parent_name}' missing 'type'"),
452                location: format!("{parent_name}.arguments.{name}.type"),
453            })?
454            .to_string();
455
456        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
457
458        Ok(IRArgument {
459            name,
460            arg_type,
461            nullable,
462            default_value: obj.get("default_value").cloned(),
463            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
464        })
465    }
466
467    fn parse_auto_params(&self, value: &Value) -> Result<AutoParams> {
468        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
469            message:  "auto_params must be an object".to_string(),
470            location: "auto_params".to_string(),
471        })?;
472
473        Ok(AutoParams {
474            has_where:    obj.get("has_where").and_then(|v| v.as_bool()).unwrap_or(false),
475            has_order_by: obj.get("has_order_by").and_then(|v| v.as_bool()).unwrap_or(false),
476            has_limit:    obj.get("has_limit").and_then(|v| v.as_bool()).unwrap_or(false),
477            has_offset:   obj.get("has_offset").and_then(|v| v.as_bool()).unwrap_or(false),
478        })
479    }
480
481    fn parse_interfaces(&self, value: &Value) -> Result<Vec<IRInterface>> {
482        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
483            message:  "interfaces must be an array".to_string(),
484            location: "interfaces".to_string(),
485        })?;
486
487        array
488            .iter()
489            .enumerate()
490            .map(|(i, interface_val)| self.parse_interface(interface_val, i))
491            .collect()
492    }
493
494    fn parse_interface(&self, value: &Value, index: usize) -> Result<IRInterface> {
495        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
496            message:  format!("Interface at index {index} must be an object"),
497            location: format!("interfaces[{index}]"),
498        })?;
499
500        let name = obj
501            .get("name")
502            .and_then(|v| v.as_str())
503            .ok_or_else(|| FraiseQLError::Parse {
504                message:  format!("Interface at index {index} missing 'name' field"),
505                location: format!("interfaces[{index}].name"),
506            })?
507            .to_string();
508
509        let fields = if let Some(fields_val) = obj.get("fields") {
510            self.parse_fields(fields_val, &name)?
511        } else {
512            Vec::new()
513        };
514
515        Ok(IRInterface {
516            name,
517            fields,
518            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
519        })
520    }
521
522    fn parse_unions(&self, value: &Value) -> Result<Vec<IRUnion>> {
523        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
524            message:  "unions must be an array".to_string(),
525            location: "unions".to_string(),
526        })?;
527
528        array
529            .iter()
530            .enumerate()
531            .map(|(i, union_val)| self.parse_union(union_val, i))
532            .collect()
533    }
534
535    fn parse_union(&self, value: &Value, index: usize) -> Result<IRUnion> {
536        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
537            message:  format!("Union at index {index} must be an object"),
538            location: format!("unions[{index}]"),
539        })?;
540
541        let name = obj
542            .get("name")
543            .and_then(|v| v.as_str())
544            .ok_or_else(|| FraiseQLError::Parse {
545                message:  format!("Union at index {index} missing 'name' field"),
546                location: format!("unions[{index}].name"),
547            })?
548            .to_string();
549
550        let types = if let Some(types_val) = obj.get("types") {
551            let array = types_val.as_array().ok_or_else(|| FraiseQLError::Parse {
552                message:  format!("'types' for union {name} must be an array"),
553                location: format!("unions.{name}.types"),
554            })?;
555
556            array
557                .iter()
558                .enumerate()
559                .map(|(i, type_val)| {
560                    type_val.as_str().ok_or_else(|| FraiseQLError::Parse {
561                        message:  format!("Type at index {i} in union {name} must be a string"),
562                        location: format!("unions.{name}.types[{i}]"),
563                    })
564                })
565                .collect::<Result<Vec<_>>>()?
566                .iter()
567                .map(|s| (*s).to_string())
568                .collect()
569        } else {
570            Vec::new()
571        };
572
573        Ok(IRUnion {
574            name,
575            types,
576            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
577        })
578    }
579
580    fn parse_input_types(&self, value: &Value) -> Result<Vec<IRInputType>> {
581        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
582            message:  "input_types must be an array".to_string(),
583            location: "input_types".to_string(),
584        })?;
585
586        array
587            .iter()
588            .enumerate()
589            .map(|(i, input_type_val)| self.parse_input_type(input_type_val, i))
590            .collect()
591    }
592
593    fn parse_input_type(&self, value: &Value, index: usize) -> Result<IRInputType> {
594        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
595            message:  format!("Input type at index {index} must be an object"),
596            location: format!("input_types[{index}]"),
597        })?;
598
599        let name = obj
600            .get("name")
601            .and_then(|v| v.as_str())
602            .ok_or_else(|| FraiseQLError::Parse {
603                message:  format!("Input type at index {index} missing 'name' field"),
604                location: format!("input_types[{index}].name"),
605            })?
606            .to_string();
607
608        let fields = if let Some(fields_val) = obj.get("fields") {
609            self.parse_input_fields(fields_val, &name)?
610        } else {
611            Vec::new()
612        };
613
614        Ok(IRInputType {
615            name,
616            fields,
617            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
618        })
619    }
620
621    fn parse_input_fields(&self, value: &Value, type_name: &str) -> Result<Vec<IRInputField>> {
622        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
623            message:  format!("fields for input type {type_name} must be an array"),
624            location: format!("{type_name}.fields"),
625        })?;
626
627        array
628            .iter()
629            .enumerate()
630            .map(|(i, field_val)| self.parse_input_field(field_val, type_name, i))
631            .collect()
632    }
633
634    fn parse_input_field(
635        &self,
636        value: &Value,
637        type_name: &str,
638        index: usize,
639    ) -> Result<IRInputField> {
640        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
641            message:  format!("Input field at index {index} in type {type_name} must be an object"),
642            location: format!("{type_name}.fields[{index}]"),
643        })?;
644
645        let name = obj
646            .get("name")
647            .and_then(|v| v.as_str())
648            .ok_or_else(|| FraiseQLError::Parse {
649                message:  format!(
650                    "Input field at index {index} in type {type_name} missing 'name'"
651                ),
652                location: format!("{type_name}.fields[{index}].name"),
653            })?
654            .to_string();
655
656        let field_type = obj
657            .get("type")
658            .and_then(|v| v.as_str())
659            .ok_or_else(|| FraiseQLError::Parse {
660                message:  format!("Input field '{name}' in type {type_name} missing 'type'"),
661                location: format!("{type_name}.fields.{name}.type"),
662            })?
663            .to_string();
664
665        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
666
667        Ok(IRInputField {
668            name,
669            field_type,
670            nullable,
671            default_value: obj.get("default_value").cloned(),
672            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
673        })
674    }
675
676    fn parse_scalars(&self, value: &Value) -> Result<Vec<IRScalar>> {
677        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
678            message:  "scalars must be an array".to_string(),
679            location: "scalars".to_string(),
680        })?;
681
682        array
683            .iter()
684            .enumerate()
685            .map(|(i, scalar_val)| self.parse_scalar(scalar_val, i))
686            .collect()
687    }
688
689    fn parse_scalar(&self, value: &Value, index: usize) -> Result<IRScalar> {
690        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
691            message:  format!("Scalar at index {index} must be an object"),
692            location: format!("scalars[{index}]"),
693        })?;
694
695        let name = obj
696            .get("name")
697            .and_then(|v| v.as_str())
698            .ok_or_else(|| FraiseQLError::Parse {
699                message:  format!("Scalar at index {index} missing 'name' field"),
700                location: format!("scalars[{index}].name"),
701            })?
702            .to_string();
703
704        let description = obj.get("description").and_then(|v| v.as_str()).map(String::from);
705        let specified_by_url =
706            obj.get("specified_by_url").and_then(|v| v.as_str()).map(String::from);
707        let base_type = obj.get("base_type").and_then(|v| v.as_str()).map(String::from);
708
709        // Parse validation rules if present
710        let validation_rules = if let Some(rules_val) = obj.get("validation_rules") {
711            serde_json::from_value(rules_val.clone()).unwrap_or_default()
712        } else {
713            Vec::new()
714        };
715
716        Ok(IRScalar {
717            name,
718            description,
719            specified_by_url,
720            validation_rules,
721            base_type,
722        })
723    }
724}
725
726impl Default for SchemaParser {
727    fn default() -> Self {
728        Self::new()
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    #[test]
737    fn test_parse_empty_schema() {
738        let parser = SchemaParser::new();
739        let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
740        let ir = parser.parse(json).unwrap();
741
742        assert!(ir.types.is_empty());
743        assert!(ir.queries.is_empty());
744        assert!(ir.mutations.is_empty());
745        assert!(ir.subscriptions.is_empty());
746    }
747
748    #[test]
749    fn test_parse_minimal_schema() {
750        let parser = SchemaParser::new();
751        let json = r"{}";
752        let ir = parser.parse(json).unwrap();
753
754        assert!(ir.types.is_empty());
755        assert!(ir.queries.is_empty());
756    }
757
758    #[test]
759    fn test_parse_type_with_fields() {
760        let parser = SchemaParser::new();
761        let json = r#"{
762            "types": [{
763                "name": "User",
764                "fields": [
765                    {"name": "id", "type": "Int!", "nullable": false},
766                    {"name": "name", "type": "String!", "nullable": false}
767                ],
768                "sql_source": "v_user"
769            }]
770        }"#;
771
772        let ir = parser.parse(json).unwrap();
773        assert_eq!(ir.types.len(), 1);
774        assert_eq!(ir.types[0].name, "User");
775        assert_eq!(ir.types[0].fields.len(), 2);
776        assert_eq!(ir.types[0].sql_source, Some("v_user".to_string()));
777    }
778
779    #[test]
780    fn test_parse_query_with_auto_params() {
781        let parser = SchemaParser::new();
782        let json = r#"{
783            "queries": [{
784                "name": "users",
785                "return_type": "User",
786                "returns_list": true,
787                "nullable": false,
788                "sql_source": "v_user",
789                "auto_params": {
790                    "has_where": true,
791                    "has_limit": true
792                }
793            }]
794        }"#;
795
796        let ir = parser.parse(json).unwrap();
797        assert_eq!(ir.queries.len(), 1);
798        assert_eq!(ir.queries[0].name, "users");
799        assert!(ir.queries[0].returns_list);
800        assert!(ir.queries[0].auto_params.has_where);
801        assert!(ir.queries[0].auto_params.has_limit);
802    }
803
804    #[test]
805    fn test_parse_mutation() {
806        let parser = SchemaParser::new();
807        let json = r#"{
808            "mutations": [{
809                "name": "createUser",
810                "return_type": "User",
811                "nullable": false,
812                "operation": "create",
813                "arguments": [
814                    {"name": "input", "type": "CreateUserInput!", "nullable": false}
815                ]
816            }]
817        }"#;
818
819        let ir = parser.parse(json).unwrap();
820        assert_eq!(ir.mutations.len(), 1);
821        assert_eq!(ir.mutations[0].name, "createUser");
822        assert_eq!(ir.mutations[0].operation, MutationOperation::Create);
823        assert_eq!(ir.mutations[0].arguments.len(), 1);
824    }
825
826    #[test]
827    fn test_parse_mutation_operation_case_insensitive() {
828        // Test that operation strings are accepted in any case (lowercase, uppercase, mixed)
829        let parser = SchemaParser::new();
830
831        let cases: &[(&str, MutationOperation)] = &[
832            ("create", MutationOperation::Create),
833            ("CREATE", MutationOperation::Create),
834            ("Create", MutationOperation::Create),
835            ("update", MutationOperation::Update),
836            ("UPDATE", MutationOperation::Update),
837            ("delete", MutationOperation::Delete),
838            ("DELETE", MutationOperation::Delete),
839            ("custom", MutationOperation::Custom),
840            ("CUSTOM", MutationOperation::Custom),
841        ];
842
843        for (op_str, expected) in cases {
844            let json = format!(
845                r#"{{"mutations": [{{"name": "m", "return_type": "T", "nullable": false, "operation": "{op_str}", "arguments": []}}]}}"#
846            );
847            let ir = parser.parse(&json).unwrap_or_else(|e| {
848                panic!("Expected parse to succeed for operation {op_str:?}, got error: {e}")
849            });
850            assert_eq!(
851                ir.mutations[0].operation, *expected,
852                "operation {op_str:?} should map to {expected:?}"
853            );
854        }
855    }
856
857    #[test]
858    fn test_parse_mutation_operation_missing_defaults_to_custom() {
859        let parser = SchemaParser::new();
860        let json =
861            r#"{"mutations": [{"name": "m", "return_type": "T", "nullable": false, "arguments": []}]}"#;
862        let ir = parser.parse(json).unwrap();
863        assert_eq!(ir.mutations[0].operation, MutationOperation::Custom);
864    }
865
866    #[test]
867    fn test_parse_mutation_operation_typo_returns_error() {
868        let parser = SchemaParser::new();
869        let invalid_ops = &["creat", "CREAT", "updaet", "delet", "FUNCTION", "insert"];
870        for op in invalid_ops {
871            let json = format!(
872                r#"{{"mutations": [{{"name": "m", "return_type": "T", "nullable": false, "operation": "{op}", "arguments": []}}]}}"#
873            );
874            let result = parser.parse(&json);
875            assert!(
876                result.is_err(),
877                "Expected parse error for unknown operation {op:?}, but got Ok"
878            );
879            let err = result.unwrap_err().to_string();
880            assert!(
881                err.contains("unknown operation"),
882                "Error for {op:?} should mention 'unknown operation', got: {err}"
883            );
884        }
885    }
886
887    #[test]
888    fn test_parse_invalid_json() {
889        let parser = SchemaParser::new();
890        let json = "not valid json";
891        let result = parser.parse(json);
892        assert!(result.is_err());
893    }
894
895    #[test]
896    fn test_parse_missing_required_field() {
897        let parser = SchemaParser::new();
898        let json = r#"{
899            "types": [{
900                "fields": []
901            }]
902        }"#;
903        let result = parser.parse(json);
904        assert!(result.is_err());
905    }
906
907    #[test]
908    fn test_parse_interface_basic() {
909        let parser = SchemaParser::new();
910        let json = r#"{
911            "interfaces": [{
912                "name": "Node",
913                "fields": [
914                    {"name": "id", "type": "ID!", "nullable": false}
915                ]
916            }]
917        }"#;
918
919        let ir = parser.parse(json).unwrap();
920        assert_eq!(ir.interfaces.len(), 1);
921        assert_eq!(ir.interfaces[0].name, "Node");
922        assert_eq!(ir.interfaces[0].fields.len(), 1);
923        assert_eq!(ir.interfaces[0].fields[0].name, "id");
924    }
925
926    #[test]
927    fn test_parse_interface_with_multiple_fields() {
928        let parser = SchemaParser::new();
929        let json = r#"{
930            "interfaces": [{
931                "name": "Timestamped",
932                "fields": [
933                    {"name": "createdAt", "type": "String!", "nullable": false},
934                    {"name": "updatedAt", "type": "String!", "nullable": false}
935                ],
936                "description": "Records creation and update times"
937            }]
938        }"#;
939
940        let ir = parser.parse(json).unwrap();
941        assert_eq!(ir.interfaces[0].fields.len(), 2);
942        assert_eq!(
943            ir.interfaces[0].description,
944            Some("Records creation and update times".to_string())
945        );
946    }
947
948    #[test]
949    fn test_parse_interface_with_empty_fields() {
950        let parser = SchemaParser::new();
951        let json = r#"{
952            "interfaces": [{
953                "name": "Empty",
954                "fields": []
955            }]
956        }"#;
957
958        let ir = parser.parse(json).unwrap();
959        assert_eq!(ir.interfaces.len(), 1);
960        assert_eq!(ir.interfaces[0].fields.len(), 0);
961    }
962
963    #[test]
964    fn test_parse_multiple_interfaces() {
965        let parser = SchemaParser::new();
966        let json = r#"{
967            "interfaces": [
968                {"name": "Node", "fields": []},
969                {"name": "Auditable", "fields": []},
970                {"name": "Publishable", "fields": []}
971            ]
972        }"#;
973
974        let ir = parser.parse(json).unwrap();
975        assert_eq!(ir.interfaces.len(), 3);
976        assert_eq!(ir.interfaces[0].name, "Node");
977        assert_eq!(ir.interfaces[1].name, "Auditable");
978        assert_eq!(ir.interfaces[2].name, "Publishable");
979    }
980
981    #[test]
982    fn test_parse_interface_missing_name() {
983        let parser = SchemaParser::new();
984        let json = r#"{"interfaces": [{"fields": []}]}"#;
985        let result = parser.parse(json);
986        assert!(result.is_err());
987    }
988
989    #[test]
990    fn test_parse_union_basic() {
991        let parser = SchemaParser::new();
992        let json = r#"{
993            "unions": [{
994                "name": "SearchResult",
995                "types": ["User", "Post"]
996            }]
997        }"#;
998
999        let ir = parser.parse(json).unwrap();
1000        assert_eq!(ir.unions.len(), 1);
1001        assert_eq!(ir.unions[0].name, "SearchResult");
1002        assert_eq!(ir.unions[0].types.len(), 2);
1003        assert_eq!(ir.unions[0].types[0], "User");
1004        assert_eq!(ir.unions[0].types[1], "Post");
1005    }
1006
1007    #[test]
1008    fn test_parse_union_single_type() {
1009        let parser = SchemaParser::new();
1010        let json = r#"{
1011            "unions": [{
1012                "name": "Result",
1013                "types": ["Error"]
1014            }]
1015        }"#;
1016
1017        let ir = parser.parse(json).unwrap();
1018        assert_eq!(ir.unions[0].types.len(), 1);
1019        assert_eq!(ir.unions[0].types[0], "Error");
1020    }
1021
1022    #[test]
1023    fn test_parse_union_with_description() {
1024        let parser = SchemaParser::new();
1025        let json = r#"{
1026            "unions": [{
1027                "name": "SearchResult",
1028                "types": ["User", "Post", "Comment"],
1029                "description": "Results from search"
1030            }]
1031        }"#;
1032
1033        let ir = parser.parse(json).unwrap();
1034        assert_eq!(ir.unions[0].description, Some("Results from search".to_string()));
1035        assert_eq!(ir.unions[0].types.len(), 3);
1036    }
1037
1038    #[test]
1039    fn test_parse_multiple_unions() {
1040        let parser = SchemaParser::new();
1041        let json = r#"{
1042            "unions": [
1043                {"name": "SearchResult", "types": ["User", "Post"]},
1044                {"name": "Error", "types": ["ValidationError", "NotFoundError"]},
1045                {"name": "Response", "types": ["Success", "Error"]}
1046            ]
1047        }"#;
1048
1049        let ir = parser.parse(json).unwrap();
1050        assert_eq!(ir.unions.len(), 3);
1051        assert_eq!(ir.unions[0].name, "SearchResult");
1052        assert_eq!(ir.unions[1].name, "Error");
1053        assert_eq!(ir.unions[2].name, "Response");
1054    }
1055
1056    #[test]
1057    fn test_parse_union_missing_name() {
1058        let parser = SchemaParser::new();
1059        let json = r#"{"unions": [{"types": []}]}"#;
1060        let result = parser.parse(json);
1061        assert!(result.is_err());
1062    }
1063
1064    #[test]
1065    fn test_parse_union_empty_types() {
1066        let parser = SchemaParser::new();
1067        let json = r#"{
1068            "unions": [{
1069                "name": "Empty",
1070                "types": []
1071            }]
1072        }"#;
1073
1074        let ir = parser.parse(json).unwrap();
1075        assert_eq!(ir.unions[0].types.len(), 0);
1076    }
1077
1078    #[test]
1079    fn test_parse_input_type_basic() {
1080        let parser = SchemaParser::new();
1081        let json = r#"{
1082            "input_types": [{
1083                "name": "UserInput",
1084                "fields": [
1085                    {"name": "name", "type": "String!", "nullable": false}
1086                ]
1087            }]
1088        }"#;
1089
1090        let ir = parser.parse(json).unwrap();
1091        assert_eq!(ir.input_types.len(), 1);
1092        assert_eq!(ir.input_types[0].name, "UserInput");
1093        assert_eq!(ir.input_types[0].fields.len(), 1);
1094        assert_eq!(ir.input_types[0].fields[0].name, "name");
1095    }
1096
1097    #[test]
1098    fn test_parse_input_type_with_multiple_fields() {
1099        let parser = SchemaParser::new();
1100        let json = r#"{
1101            "input_types": [{
1102                "name": "CreateUserInput",
1103                "fields": [
1104                    {"name": "name", "type": "String!", "nullable": false},
1105                    {"name": "email", "type": "String!", "nullable": false},
1106                    {"name": "age", "type": "Int", "nullable": true}
1107                ],
1108                "description": "Input for creating users"
1109            }]
1110        }"#;
1111
1112        let ir = parser.parse(json).unwrap();
1113        assert_eq!(ir.input_types[0].fields.len(), 3);
1114        assert!(!ir.input_types[0].fields[0].nullable);
1115        assert!(ir.input_types[0].fields[2].nullable);
1116        assert_eq!(ir.input_types[0].description, Some("Input for creating users".to_string()));
1117    }
1118
1119    #[test]
1120    fn test_parse_input_field_with_default_value() {
1121        let parser = SchemaParser::new();
1122        let json = r#"{
1123            "input_types": [{
1124                "name": "QueryInput",
1125                "fields": [
1126                    {"name": "limit", "type": "Int", "nullable": true, "default_value": 10},
1127                    {"name": "active", "type": "Boolean", "nullable": true, "default_value": true}
1128                ]
1129            }]
1130        }"#;
1131
1132        let ir = parser.parse(json).unwrap();
1133        assert_eq!(ir.input_types[0].fields[0].default_value, Some(serde_json::json!(10)));
1134        assert_eq!(ir.input_types[0].fields[1].default_value, Some(serde_json::json!(true)));
1135    }
1136
1137    #[test]
1138    fn test_parse_input_type_with_empty_fields() {
1139        let parser = SchemaParser::new();
1140        let json = r#"{
1141            "input_types": [{
1142                "name": "EmptyInput",
1143                "fields": []
1144            }]
1145        }"#;
1146
1147        let ir = parser.parse(json).unwrap();
1148        assert_eq!(ir.input_types[0].fields.len(), 0);
1149    }
1150
1151    #[test]
1152    fn test_parse_multiple_input_types() {
1153        let parser = SchemaParser::new();
1154        let json = r#"{
1155            "input_types": [
1156                {"name": "UserInput", "fields": []},
1157                {"name": "PostInput", "fields": []},
1158                {"name": "FilterInput", "fields": []}
1159            ]
1160        }"#;
1161
1162        let ir = parser.parse(json).unwrap();
1163        assert_eq!(ir.input_types.len(), 3);
1164        assert_eq!(ir.input_types[0].name, "UserInput");
1165        assert_eq!(ir.input_types[1].name, "PostInput");
1166        assert_eq!(ir.input_types[2].name, "FilterInput");
1167    }
1168
1169    #[test]
1170    fn test_parse_input_type_missing_name() {
1171        let parser = SchemaParser::new();
1172        let json = r#"{"input_types": [{"fields": []}]}"#;
1173        let result = parser.parse(json);
1174        assert!(result.is_err());
1175    }
1176
1177    #[test]
1178    fn test_parse_complete_schema_with_all_features() {
1179        let parser = SchemaParser::new();
1180        let json = r#"{
1181            "types": [{"name": "User", "fields": []}],
1182            "interfaces": [{"name": "Node", "fields": []}],
1183            "unions": [{"name": "SearchResult", "types": ["User"]}],
1184            "input_types": [{"name": "UserInput", "fields": []}],
1185            "queries": [{"name": "users", "return_type": "User", "returns_list": true}],
1186            "mutations": [{"name": "createUser", "return_type": "User", "operation": "create"}]
1187        }"#;
1188
1189        let ir = parser.parse(json).unwrap();
1190        assert_eq!(ir.types.len(), 1);
1191        assert_eq!(ir.interfaces.len(), 1);
1192        assert_eq!(ir.unions.len(), 1);
1193        assert_eq!(ir.input_types.len(), 1);
1194        assert_eq!(ir.queries.len(), 1);
1195        assert_eq!(ir.mutations.len(), 1);
1196    }
1197
1198    #[test]
1199    fn test_parse_scalars() {
1200        let parser = SchemaParser::new();
1201        let json = r#"{
1202            "scalars": [
1203                {
1204                    "name": "Email",
1205                    "description": "Valid email address",
1206                    "specified_by_url": "https://html.spec.whatwg.org/",
1207                    "validation_rules": [],
1208                    "base_type": null
1209                },
1210                {
1211                    "name": "ISBN",
1212                    "description": "International Standard Book Number",
1213                    "specified_by_url": null,
1214                    "validation_rules": [],
1215                    "base_type": null
1216                }
1217            ]
1218        }"#;
1219
1220        let ir = parser.parse(json).unwrap();
1221        assert_eq!(ir.scalars.len(), 2);
1222        assert_eq!(ir.scalars[0].name, "Email");
1223        assert_eq!(ir.scalars[0].description, Some("Valid email address".to_string()));
1224        assert_eq!(
1225            ir.scalars[0].specified_by_url,
1226            Some("https://html.spec.whatwg.org/".to_string())
1227        );
1228        assert_eq!(ir.scalars[1].name, "ISBN");
1229    }
1230
1231    #[test]
1232    fn test_parse_schema_with_scalars_and_types() {
1233        let parser = SchemaParser::new();
1234        let json = r#"{
1235            "scalars": [{"name": "Email", "description": null, "specified_by_url": null, "validation_rules": [], "base_type": null}],
1236            "types": [{"name": "User", "fields": []}],
1237            "queries": [{"name": "users", "return_type": "User", "returns_list": true}]
1238        }"#;
1239
1240        let ir = parser.parse(json).unwrap();
1241        assert_eq!(ir.scalars.len(), 1);
1242        assert_eq!(ir.types.len(), 1);
1243        assert_eq!(ir.queries.len(), 1);
1244    }
1245}