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::error::{FraiseQLError, Result};
47
48pub struct SchemaParser {
52 }
54
55impl SchemaParser {
56 #[must_use]
58 pub fn new() -> Self {
59 Self {}
60 }
61
62 pub fn parse(&self, schema_json: &str) -> Result<AuthoringIR> {
87 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 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 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 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}