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        })
244    }
245
246    fn parse_queries(&self, value: &Value) -> Result<Vec<IRQuery>> {
247        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
248            message:  "queries must be an array".to_string(),
249            location: "queries".to_string(),
250        })?;
251
252        array
253            .iter()
254            .enumerate()
255            .map(|(i, query_val)| self.parse_query(query_val, i))
256            .collect()
257    }
258
259    fn parse_query(&self, value: &Value, index: usize) -> Result<IRQuery> {
260        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
261            message:  format!("Query at index {index} must be an object"),
262            location: format!("queries[{index}]"),
263        })?;
264
265        let name = obj
266            .get("name")
267            .and_then(|v| v.as_str())
268            .ok_or_else(|| FraiseQLError::Parse {
269                message:  format!("Query at index {index} missing 'name'"),
270                location: format!("queries[{index}].name"),
271            })?
272            .to_string();
273
274        let return_type = obj
275            .get("return_type")
276            .and_then(|v| v.as_str())
277            .ok_or_else(|| FraiseQLError::Parse {
278                message:  format!("Query '{name}' missing 'return_type'"),
279                location: format!("queries.{name}.return_type"),
280            })?
281            .to_string();
282
283        let returns_list = obj.get("returns_list").and_then(|v| v.as_bool()).unwrap_or(false);
284
285        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
286
287        let arguments = if let Some(args_val) = obj.get("arguments") {
288            self.parse_arguments(args_val, &name)?
289        } else {
290            Vec::new()
291        };
292
293        let auto_params = if let Some(auto_val) = obj.get("auto_params") {
294            self.parse_auto_params(auto_val)?
295        } else {
296            AutoParams::default()
297        };
298
299        Ok(IRQuery {
300            name,
301            return_type,
302            returns_list,
303            nullable,
304            arguments,
305            sql_source: obj.get("sql_source").and_then(|v| v.as_str()).map(String::from),
306            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
307            auto_params,
308        })
309    }
310
311    fn parse_mutations(&self, value: &Value) -> Result<Vec<IRMutation>> {
312        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
313            message:  "mutations must be an array".to_string(),
314            location: "mutations".to_string(),
315        })?;
316
317        array
318            .iter()
319            .enumerate()
320            .map(|(i, mutation_val)| self.parse_mutation(mutation_val, i))
321            .collect()
322    }
323
324    fn parse_mutation(&self, value: &Value, index: usize) -> Result<IRMutation> {
325        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
326            message:  format!("Mutation at index {index} must be an object"),
327            location: format!("mutations[{index}]"),
328        })?;
329
330        let name = obj
331            .get("name")
332            .and_then(|v| v.as_str())
333            .ok_or_else(|| FraiseQLError::Parse {
334                message:  format!("Mutation at index {index} missing 'name'"),
335                location: format!("mutations[{index}].name"),
336            })?
337            .to_string();
338
339        let return_type = obj
340            .get("return_type")
341            .and_then(|v| v.as_str())
342            .ok_or_else(|| FraiseQLError::Parse {
343                message:  format!("Mutation '{name}' missing 'return_type'"),
344                location: format!("mutations.{name}.return_type"),
345            })?
346            .to_string();
347
348        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(false);
349
350        let arguments = if let Some(args_val) = obj.get("arguments") {
351            self.parse_arguments(args_val, &name)?
352        } else {
353            Vec::new()
354        };
355
356        let operation = if let Some(s) = obj.get("operation").and_then(|v| v.as_str()) {
357            match s.to_lowercase().as_str() {
358                "create" => MutationOperation::Create,
359                "update" => MutationOperation::Update,
360                "delete" => MutationOperation::Delete,
361                "custom" => MutationOperation::Custom,
362                other => {
363                    return Err(FraiseQLError::Parse {
364                        message:  format!(
365                            "Mutation '{name}' has unknown operation {other:?}. \
366                             Valid values are: create, update, delete, custom"
367                        ),
368                        location: format!("mutations.{name}.operation"),
369                    });
370                },
371            }
372        } else {
373            MutationOperation::Custom
374        };
375
376        Ok(IRMutation {
377            name,
378            return_type,
379            nullable,
380            arguments,
381            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
382            operation,
383        })
384    }
385
386    fn parse_subscriptions(&self, value: &Value) -> Result<Vec<IRSubscription>> {
387        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
388            message:  "subscriptions must be an array".to_string(),
389            location: "subscriptions".to_string(),
390        })?;
391
392        array
393            .iter()
394            .enumerate()
395            .map(|(i, sub_val)| self.parse_subscription(sub_val, i))
396            .collect()
397    }
398
399    fn parse_subscription(&self, value: &Value, index: usize) -> Result<IRSubscription> {
400        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
401            message:  format!("Subscription at index {index} must be an object"),
402            location: format!("subscriptions[{index}]"),
403        })?;
404
405        let name = obj
406            .get("name")
407            .and_then(|v| v.as_str())
408            .ok_or_else(|| FraiseQLError::Parse {
409                message:  format!("Subscription at index {index} missing 'name'"),
410                location: format!("subscriptions[{index}].name"),
411            })?
412            .to_string();
413
414        let return_type = obj
415            .get("return_type")
416            .and_then(|v| v.as_str())
417            .ok_or_else(|| FraiseQLError::Parse {
418                message:  format!("Subscription '{name}' missing 'return_type'"),
419                location: format!("subscriptions.{name}.return_type"),
420            })?
421            .to_string();
422
423        let arguments = if let Some(args_val) = obj.get("arguments") {
424            self.parse_arguments(args_val, &name)?
425        } else {
426            Vec::new()
427        };
428
429        Ok(IRSubscription {
430            name,
431            return_type,
432            arguments,
433            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
434        })
435    }
436
437    fn parse_arguments(&self, value: &Value, parent_name: &str) -> Result<Vec<IRArgument>> {
438        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
439            message:  format!("arguments for '{parent_name}' must be an array"),
440            location: format!("{parent_name}.arguments"),
441        })?;
442
443        array
444            .iter()
445            .enumerate()
446            .map(|(i, arg_val)| self.parse_argument(arg_val, parent_name, i))
447            .collect()
448    }
449
450    fn parse_argument(&self, value: &Value, parent_name: &str, index: usize) -> Result<IRArgument> {
451        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
452            message:  format!("Argument at index {index} for '{parent_name}' must be an object"),
453            location: format!("{parent_name}.arguments[{index}]"),
454        })?;
455
456        let name = obj
457            .get("name")
458            .and_then(|v| v.as_str())
459            .ok_or_else(|| FraiseQLError::Parse {
460                message:  format!("Argument at index {index} for '{parent_name}' missing 'name'"),
461                location: format!("{parent_name}.arguments[{index}].name"),
462            })?
463            .to_string();
464
465        let arg_type = obj
466            .get("type")
467            .and_then(|v| v.as_str())
468            .ok_or_else(|| FraiseQLError::Parse {
469                message:  format!("Argument '{name}' for '{parent_name}' missing 'type'"),
470                location: format!("{parent_name}.arguments.{name}.type"),
471            })?
472            .to_string();
473
474        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
475
476        Ok(IRArgument {
477            name,
478            arg_type,
479            nullable,
480            default_value: obj.get("default_value").map(GraphQLValue::from_json).transpose()?,
481            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
482        })
483    }
484
485    fn parse_auto_params(&self, value: &Value) -> Result<AutoParams> {
486        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
487            message:  "auto_params must be an object".to_string(),
488            location: "auto_params".to_string(),
489        })?;
490
491        Ok(AutoParams {
492            has_where:    obj.get("has_where").and_then(|v| v.as_bool()).unwrap_or(false),
493            has_order_by: obj.get("has_order_by").and_then(|v| v.as_bool()).unwrap_or(false),
494            has_limit:    obj.get("has_limit").and_then(|v| v.as_bool()).unwrap_or(false),
495            has_offset:   obj.get("has_offset").and_then(|v| v.as_bool()).unwrap_or(false),
496        })
497    }
498
499    fn parse_interfaces(&self, value: &Value) -> Result<Vec<IRInterface>> {
500        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
501            message:  "interfaces must be an array".to_string(),
502            location: "interfaces".to_string(),
503        })?;
504
505        array
506            .iter()
507            .enumerate()
508            .map(|(i, interface_val)| self.parse_interface(interface_val, i))
509            .collect()
510    }
511
512    fn parse_interface(&self, value: &Value, index: usize) -> Result<IRInterface> {
513        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
514            message:  format!("Interface at index {index} must be an object"),
515            location: format!("interfaces[{index}]"),
516        })?;
517
518        let name = obj
519            .get("name")
520            .and_then(|v| v.as_str())
521            .ok_or_else(|| FraiseQLError::Parse {
522                message:  format!("Interface at index {index} missing 'name' field"),
523                location: format!("interfaces[{index}].name"),
524            })?
525            .to_string();
526
527        let fields = if let Some(fields_val) = obj.get("fields") {
528            self.parse_fields(fields_val, &name)?
529        } else {
530            Vec::new()
531        };
532
533        Ok(IRInterface {
534            name,
535            fields,
536            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
537        })
538    }
539
540    fn parse_unions(&self, value: &Value) -> Result<Vec<IRUnion>> {
541        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
542            message:  "unions must be an array".to_string(),
543            location: "unions".to_string(),
544        })?;
545
546        array
547            .iter()
548            .enumerate()
549            .map(|(i, union_val)| self.parse_union(union_val, i))
550            .collect()
551    }
552
553    fn parse_union(&self, value: &Value, index: usize) -> Result<IRUnion> {
554        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
555            message:  format!("Union at index {index} must be an object"),
556            location: format!("unions[{index}]"),
557        })?;
558
559        let name = obj
560            .get("name")
561            .and_then(|v| v.as_str())
562            .ok_or_else(|| FraiseQLError::Parse {
563                message:  format!("Union at index {index} missing 'name' field"),
564                location: format!("unions[{index}].name"),
565            })?
566            .to_string();
567
568        let types = if let Some(types_val) = obj.get("types") {
569            let array = types_val.as_array().ok_or_else(|| FraiseQLError::Parse {
570                message:  format!("'types' for union {name} must be an array"),
571                location: format!("unions.{name}.types"),
572            })?;
573
574            array
575                .iter()
576                .enumerate()
577                .map(|(i, type_val)| {
578                    type_val.as_str().ok_or_else(|| FraiseQLError::Parse {
579                        message:  format!("Type at index {i} in union {name} must be a string"),
580                        location: format!("unions.{name}.types[{i}]"),
581                    })
582                })
583                .collect::<Result<Vec<_>>>()?
584                .iter()
585                .map(|s| (*s).to_string())
586                .collect()
587        } else {
588            Vec::new()
589        };
590
591        Ok(IRUnion {
592            name,
593            types,
594            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
595        })
596    }
597
598    fn parse_input_types(&self, value: &Value) -> Result<Vec<IRInputType>> {
599        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
600            message:  "input_types must be an array".to_string(),
601            location: "input_types".to_string(),
602        })?;
603
604        array
605            .iter()
606            .enumerate()
607            .map(|(i, input_type_val)| self.parse_input_type(input_type_val, i))
608            .collect()
609    }
610
611    fn parse_input_type(&self, value: &Value, index: usize) -> Result<IRInputType> {
612        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
613            message:  format!("Input type at index {index} must be an object"),
614            location: format!("input_types[{index}]"),
615        })?;
616
617        let name = obj
618            .get("name")
619            .and_then(|v| v.as_str())
620            .ok_or_else(|| FraiseQLError::Parse {
621                message:  format!("Input type at index {index} missing 'name' field"),
622                location: format!("input_types[{index}].name"),
623            })?
624            .to_string();
625
626        let fields = if let Some(fields_val) = obj.get("fields") {
627            self.parse_input_fields(fields_val, &name)?
628        } else {
629            Vec::new()
630        };
631
632        Ok(IRInputType {
633            name,
634            fields,
635            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
636        })
637    }
638
639    fn parse_input_fields(&self, value: &Value, type_name: &str) -> Result<Vec<IRInputField>> {
640        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
641            message:  format!("fields for input type {type_name} must be an array"),
642            location: format!("{type_name}.fields"),
643        })?;
644
645        array
646            .iter()
647            .enumerate()
648            .map(|(i, field_val)| self.parse_input_field(field_val, type_name, i))
649            .collect()
650    }
651
652    fn parse_input_field(
653        &self,
654        value: &Value,
655        type_name: &str,
656        index: usize,
657    ) -> Result<IRInputField> {
658        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
659            message:  format!("Input field at index {index} in type {type_name} must be an object"),
660            location: format!("{type_name}.fields[{index}]"),
661        })?;
662
663        let name = obj
664            .get("name")
665            .and_then(|v| v.as_str())
666            .ok_or_else(|| FraiseQLError::Parse {
667                message:  format!(
668                    "Input field at index {index} in type {type_name} missing 'name'"
669                ),
670                location: format!("{type_name}.fields[{index}].name"),
671            })?
672            .to_string();
673
674        let field_type = obj
675            .get("type")
676            .and_then(|v| v.as_str())
677            .ok_or_else(|| FraiseQLError::Parse {
678                message:  format!("Input field '{name}' in type {type_name} missing 'type'"),
679                location: format!("{type_name}.fields.{name}.type"),
680            })?
681            .to_string();
682
683        let nullable = obj.get("nullable").and_then(|v| v.as_bool()).unwrap_or(true);
684
685        Ok(IRInputField {
686            name,
687            field_type,
688            nullable,
689            default_value: obj.get("default_value").map(GraphQLValue::from_json).transpose()?,
690            description: obj.get("description").and_then(|v| v.as_str()).map(String::from),
691        })
692    }
693
694    fn parse_scalars(&self, value: &Value) -> Result<Vec<IRScalar>> {
695        let array = value.as_array().ok_or_else(|| FraiseQLError::Parse {
696            message:  "scalars must be an array".to_string(),
697            location: "scalars".to_string(),
698        })?;
699
700        array
701            .iter()
702            .enumerate()
703            .map(|(i, scalar_val)| self.parse_scalar(scalar_val, i))
704            .collect()
705    }
706
707    fn parse_scalar(&self, value: &Value, index: usize) -> Result<IRScalar> {
708        let obj = value.as_object().ok_or_else(|| FraiseQLError::Parse {
709            message:  format!("Scalar at index {index} must be an object"),
710            location: format!("scalars[{index}]"),
711        })?;
712
713        let name = obj
714            .get("name")
715            .and_then(|v| v.as_str())
716            .ok_or_else(|| FraiseQLError::Parse {
717                message:  format!("Scalar at index {index} missing 'name' field"),
718                location: format!("scalars[{index}].name"),
719            })?
720            .to_string();
721
722        let description = obj.get("description").and_then(|v| v.as_str()).map(String::from);
723        let specified_by_url =
724            obj.get("specified_by_url").and_then(|v| v.as_str()).map(String::from);
725        let base_type = obj.get("base_type").and_then(|v| v.as_str()).map(String::from);
726
727        // Parse validation rules if present. A malformed block is a hard error:
728        // `unwrap_or_default()` turned a typo into an empty rule set, silently
729        // disabling that scalar's validation while every other field in this
730        // file gets strict `FraiseQLError::Parse` treatment (#720).
731        let validation_rules = match obj.get("validation_rules") {
732            Some(rules_val) => {
733                serde_json::from_value(rules_val.clone()).map_err(|e| FraiseQLError::Parse {
734                    message:  format!(
735                        "Scalar '{name}' has malformed 'validation_rules': {e}. Refusing to \
736                         compile a scalar whose validation would silently not run."
737                    ),
738                    location: format!("scalars[{index}].validation_rules"),
739                })?
740            },
741            None => Vec::new(),
742        };
743
744        Ok(IRScalar {
745            name,
746            description,
747            specified_by_url,
748            validation_rules,
749            base_type,
750        })
751    }
752}
753
754impl Default for SchemaParser {
755    fn default() -> Self {
756        Self::new()
757    }
758}