1use 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
51pub struct SchemaParser {
55 }
57
58impl SchemaParser {
59 #[must_use]
61 pub const fn new() -> Self {
62 Self {}
63 }
64
65 pub fn parse(&self, schema_json: &str) -> Result<AuthoringIR> {
90 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 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 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)] 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 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}