1use anyhow::Result;
35use std::collections::HashMap;
36use tensorlogic_adapters::{DomainInfo, PredicateInfo, SymbolTable};
37use tensorlogic_ir::{TLExpr, Term};
38
39#[derive(Debug, Clone)]
41pub struct GraphQLType {
42 pub name: String,
43 pub fields: Vec<GraphQLField>,
44 pub interfaces: Vec<String>,
45 pub description: Option<String>,
46}
47
48#[derive(Debug, Clone)]
50pub struct GraphQLField {
51 pub name: String,
52 pub field_type: String,
53 pub is_required: bool,
54 pub is_list: bool,
55 pub arguments: Vec<GraphQLArgument>,
56 pub directives: Vec<GraphQLDirective>,
57 pub description: Option<String>,
58}
59
60#[derive(Debug, Clone)]
62pub struct GraphQLArgument {
63 pub name: String,
64 pub arg_type: String,
65 pub is_required: bool,
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct GraphQLDirective {
71 pub name: String,
72 pub arguments: HashMap<String, DirectiveValue>,
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub enum DirectiveValue {
78 String(String),
79 Int(i64),
80 Float(f64),
81 Boolean(bool),
82 List(Vec<DirectiveValue>),
83}
84
85pub struct GraphQLConverter {
87 types: HashMap<String, GraphQLType>,
88}
89
90impl GraphQLConverter {
91 pub fn new() -> Self {
92 GraphQLConverter {
93 types: HashMap::new(),
94 }
95 }
96
97 pub fn parse_schema(&mut self, schema: &str) -> Result<SymbolTable> {
102 self.types.clear();
103
104 let lines: Vec<&str> = schema.lines().map(|l| l.trim()).collect();
106 let mut i = 0;
107
108 while i < lines.len() {
109 let line = lines[i];
110
111 if line.is_empty() || line.starts_with('#') {
113 i += 1;
114 continue;
115 }
116
117 if line.starts_with("type ") {
119 let type_def = self.parse_type_definition(&lines, &mut i)?;
120 self.types.insert(type_def.name.clone(), type_def);
121 } else {
122 i += 1;
123 }
124 }
125
126 self.to_symbol_table()
127 }
128
129 fn parse_type_definition(&self, lines: &[&str], index: &mut usize) -> Result<GraphQLType> {
131 let line = lines[*index];
132
133 let type_line = line
135 .strip_prefix("type ")
136 .ok_or_else(|| anyhow::anyhow!("Invalid type definition"))?;
137
138 let mut parts = type_line.split('{');
139 let type_header = parts
140 .next()
141 .ok_or_else(|| anyhow::anyhow!("Missing type header"))?
142 .trim();
143
144 let (name, interfaces) = if type_header.contains("implements") {
145 let mut impl_parts = type_header.split("implements");
146 let name = impl_parts
147 .next()
148 .ok_or_else(|| anyhow::anyhow!("Missing type name"))?
149 .trim()
150 .to_string();
151 let interfaces_str = impl_parts
152 .next()
153 .ok_or_else(|| anyhow::anyhow!("Missing interfaces"))?
154 .trim();
155 let interfaces = interfaces_str
156 .split('&')
157 .map(|s| s.trim().to_string())
158 .collect();
159 (name, interfaces)
160 } else {
161 (type_header.to_string(), Vec::new())
162 };
163
164 let mut fields = Vec::new();
165 *index += 1;
166
167 while *index < lines.len() {
169 let field_line = lines[*index].trim();
170
171 if field_line == "}" {
172 *index += 1;
173 break;
174 }
175
176 if field_line.is_empty() || field_line.starts_with('#') {
177 *index += 1;
178 continue;
179 }
180
181 if let Some(field) = self.parse_field(field_line)? {
183 fields.push(field);
184 }
185
186 *index += 1;
187 }
188
189 Ok(GraphQLType {
190 name,
191 fields,
192 interfaces,
193 description: None,
194 })
195 }
196
197 fn parse_field(&self, line: &str) -> Result<Option<GraphQLField>> {
199 let field_line = if line.find(':').is_some() {
203 line
204 } else {
205 return Ok(None); };
207
208 let (field_part, type_part) = field_line.split_once(':').unwrap();
209
210 let field_part = field_part.trim();
211 let mut type_part = type_part.trim();
212
213 let directives = self.parse_directives(type_part);
215
216 if let Some(at_pos) = type_part.find('@') {
218 type_part = type_part[..at_pos].trim();
219 }
220
221 let (field_name, arguments) = if field_part.contains('(') {
223 let name_end = field_part.find('(').unwrap();
224 let field_name = field_part[..name_end].trim();
225 let args_str = field_part[name_end + 1..]
226 .strip_suffix(')')
227 .unwrap_or("")
228 .trim();
229
230 let arguments = if args_str.is_empty() {
231 Vec::new()
232 } else {
233 self.parse_arguments(args_str)?
234 };
235
236 (field_name, arguments)
237 } else {
238 (field_part, Vec::new())
239 };
240
241 let (field_type, is_required, is_list) = self.parse_type(type_part);
243
244 Ok(Some(GraphQLField {
245 name: field_name.to_string(),
246 field_type,
247 is_required,
248 is_list,
249 arguments,
250 directives,
251 description: None,
252 }))
253 }
254
255 fn parse_arguments(&self, args_str: &str) -> Result<Vec<GraphQLArgument>> {
257 let mut arguments = Vec::new();
258
259 for arg in args_str.split(',') {
260 let arg = arg.trim();
261 if arg.is_empty() {
262 continue;
263 }
264
265 if let Some((name, type_str)) = arg.split_once(':') {
266 let name = name.trim().to_string();
267 let (arg_type, is_required, _is_list) = self.parse_type(type_str.trim());
268
269 arguments.push(GraphQLArgument {
270 name,
271 arg_type,
272 is_required,
273 });
274 }
275 }
276
277 Ok(arguments)
278 }
279
280 fn parse_type(&self, type_str: &str) -> (String, bool, bool) {
282 let type_str = type_str.trim();
283 let is_required = type_str.ends_with('!');
284 let type_str = type_str.trim_end_matches('!');
285
286 let is_list = type_str.starts_with('[') && type_str.ends_with(']');
287 let type_str = if is_list {
288 type_str
289 .strip_prefix('[')
290 .unwrap()
291 .strip_suffix(']')
292 .unwrap()
293 .trim_end_matches('!')
294 } else {
295 type_str
296 };
297
298 (type_str.to_string(), is_required, is_list)
299 }
300
301 pub fn to_symbol_table(&self) -> Result<SymbolTable> {
303 let mut table = SymbolTable::new();
304
305 for scalar in &["String", "Int", "Float", "Boolean", "ID", "Value"] {
307 let domain = DomainInfo::new(*scalar, 1000); table.add_domain(domain)?;
309 }
310
311 for (type_name, type_def) in &self.types {
313 if matches!(type_name.as_str(), "Query" | "Mutation" | "Subscription") {
315 continue;
316 }
317
318 let mut domain = DomainInfo::new(type_name, 100); if let Some(desc) = &type_def.description {
321 domain = domain.with_description(desc);
322 }
323
324 table.add_domain(domain)?;
325 }
326
327 for type_def in self.types.values() {
329 if matches!(
331 type_def.name.as_str(),
332 "Query" | "Mutation" | "Subscription"
333 ) {
334 continue;
335 }
336 for field in &type_def.fields {
337 let predicate_name = format!("{}_{}", type_def.name, field.name);
338
339 let mut arg_domains = vec![type_def.name.clone()];
340
341 if self.types.contains_key(&field.field_type)
343 || self.is_scalar_type(&field.field_type)
344 {
345 arg_domains.push(field.field_type.clone());
346 } else {
347 arg_domains.push("Value".to_string()); }
349
350 let mut predicate = PredicateInfo::new(&predicate_name, arg_domains);
351
352 if let Some(desc) = &field.description {
353 predicate = predicate.with_description(desc);
354 }
355
356 table.add_predicate(predicate)?;
357 }
358 }
359
360 Ok(table)
361 }
362
363 fn is_scalar_type(&self, type_name: &str) -> bool {
365 matches!(
366 type_name,
367 "String" | "Int" | "Float" | "Boolean" | "ID" | "Value"
368 )
369 }
370
371 fn parse_directives(&self, line: &str) -> Vec<GraphQLDirective> {
374 let mut directives = Vec::new();
375 let mut current_pos = 0;
376
377 while let Some(at_pos) = line[current_pos..].find('@') {
378 let abs_pos = current_pos + at_pos;
379 let remaining = &line[abs_pos + 1..];
380
381 let name_end = remaining
383 .find(|c: char| c == '(' || c.is_whitespace() || c == '@')
384 .unwrap_or(remaining.len());
385 let directive_name = remaining[..name_end].trim().to_string();
386
387 let mut arguments = HashMap::new();
389 if remaining.len() > name_end && remaining.chars().nth(name_end) == Some('(') {
390 if let Some(close_paren) = remaining.find(')') {
391 let args_str = &remaining[name_end + 1..close_paren];
392 arguments = self.parse_directive_arguments(args_str);
393 current_pos = abs_pos + 1 + close_paren + 1;
394 } else {
395 current_pos = abs_pos + 1 + name_end;
396 }
397 } else {
398 current_pos = abs_pos + 1 + name_end;
399 }
400
401 directives.push(GraphQLDirective {
402 name: directive_name,
403 arguments,
404 });
405 }
406
407 directives
408 }
409
410 fn parse_directive_arguments(&self, args_str: &str) -> HashMap<String, DirectiveValue> {
412 let mut arguments = HashMap::new();
413
414 for arg in args_str.split(',') {
415 let arg = arg.trim();
416 if arg.is_empty() {
417 continue;
418 }
419
420 if let Some((name, value_str)) = arg.split_once(':') {
421 let name = name.trim().to_string();
422 let value_str = value_str.trim();
423
424 if let Some(value) = self.parse_directive_value(value_str) {
425 arguments.insert(name, value);
426 }
427 }
428 }
429
430 arguments
431 }
432
433 fn parse_directive_value(&self, value_str: &str) -> Option<DirectiveValue> {
435 Self::parse_directive_value_impl(value_str)
436 }
437
438 fn parse_directive_value_impl(value_str: &str) -> Option<DirectiveValue> {
440 let value_str = value_str.trim();
441
442 if value_str.starts_with('"') && value_str.ends_with('"') {
444 let s = value_str[1..value_str.len() - 1].to_string();
445 return Some(DirectiveValue::String(s));
446 }
447
448 if value_str == "true" {
450 return Some(DirectiveValue::Boolean(true));
451 }
452 if value_str == "false" {
453 return Some(DirectiveValue::Boolean(false));
454 }
455
456 if let Ok(i) = value_str.parse::<i64>() {
458 return Some(DirectiveValue::Int(i));
459 }
460
461 if let Ok(f) = value_str.parse::<f64>() {
463 return Some(DirectiveValue::Float(f));
464 }
465
466 if value_str.starts_with('[') && value_str.ends_with(']') {
468 let inner = &value_str[1..value_str.len() - 1];
469 let items: Vec<DirectiveValue> = inner
470 .split(',')
471 .filter_map(|s| Self::parse_directive_value_impl(s.trim()))
472 .collect();
473 return Some(DirectiveValue::List(items));
474 }
475
476 None
477 }
478
479 pub fn directives_to_constraints(&self, type_name: &str, field: &GraphQLField) -> Vec<TLExpr> {
481 let mut constraints = Vec::new();
482 let field_var = format!("{}_{}", type_name, field.name);
483
484 for directive in &field.directives {
485 match directive.name.as_str() {
486 "constraint" => {
487 constraints.extend(self.constraint_directive_to_expr(&field_var, directive));
488 }
489 "range" => {
490 constraints.extend(self.range_directive_to_expr(&field_var, directive));
491 }
492 "length" => {
493 constraints.extend(self.length_directive_to_expr(&field_var, directive));
494 }
495 "pattern" => {
496 constraints.extend(self.pattern_directive_to_expr(&field_var, directive));
497 }
498 "uniqueItems" => {
499 constraints.push(self.unique_items_directive_to_expr(&field_var));
500 }
501 _ => {} }
503 }
504
505 constraints
506 }
507
508 fn constraint_directive_to_expr(
510 &self,
511 field_var: &str,
512 directive: &GraphQLDirective,
513 ) -> Vec<TLExpr> {
514 let mut constraints = Vec::new();
515
516 if let Some(DirectiveValue::Int(min_len)) = directive.arguments.get("minLength") {
518 let expr = TLExpr::pred(
519 "minLength",
520 vec![Term::var(field_var), Term::constant(min_len.to_string())],
521 );
522 constraints.push(expr);
523 }
524
525 if let Some(DirectiveValue::Int(max_len)) = directive.arguments.get("maxLength") {
527 let expr = TLExpr::pred(
528 "maxLength",
529 vec![Term::var(field_var), Term::constant(max_len.to_string())],
530 );
531 constraints.push(expr);
532 }
533
534 if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
536 let expr = TLExpr::pred(
537 "greaterOrEqual",
538 vec![Term::var(field_var), Term::constant(min.to_string())],
539 );
540 constraints.push(expr);
541 }
542 if let Some(DirectiveValue::Float(min)) = directive.arguments.get("min") {
543 let expr = TLExpr::pred(
544 "greaterOrEqual",
545 vec![Term::var(field_var), Term::constant(min.to_string())],
546 );
547 constraints.push(expr);
548 }
549
550 if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
552 let expr = TLExpr::pred(
553 "lessOrEqual",
554 vec![Term::var(field_var), Term::constant(max.to_string())],
555 );
556 constraints.push(expr);
557 }
558 if let Some(DirectiveValue::Float(max)) = directive.arguments.get("max") {
559 let expr = TLExpr::pred(
560 "lessOrEqual",
561 vec![Term::var(field_var), Term::constant(max.to_string())],
562 );
563 constraints.push(expr);
564 }
565
566 if let Some(DirectiveValue::String(pattern)) = directive.arguments.get("pattern") {
568 let expr = TLExpr::pred(
569 "matches",
570 vec![Term::var(field_var), Term::constant(pattern)],
571 );
572 constraints.push(expr);
573 }
574
575 if let Some(DirectiveValue::String(format)) = directive.arguments.get("format") {
577 let expr = TLExpr::pred(
578 format!("isValid{}", capitalize_first(format)),
579 vec![Term::var(field_var)],
580 );
581 constraints.push(expr);
582 }
583
584 constraints
585 }
586
587 fn range_directive_to_expr(
589 &self,
590 field_var: &str,
591 directive: &GraphQLDirective,
592 ) -> Vec<TLExpr> {
593 let mut constraints = Vec::new();
594
595 if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
596 let expr = TLExpr::pred(
597 "greaterOrEqual",
598 vec![Term::var(field_var), Term::constant(min.to_string())],
599 );
600 constraints.push(expr);
601 }
602
603 if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
604 let expr = TLExpr::pred(
605 "lessOrEqual",
606 vec![Term::var(field_var), Term::constant(max.to_string())],
607 );
608 constraints.push(expr);
609 }
610
611 constraints
612 }
613
614 fn length_directive_to_expr(
616 &self,
617 field_var: &str,
618 directive: &GraphQLDirective,
619 ) -> Vec<TLExpr> {
620 let mut constraints = Vec::new();
621
622 if let Some(DirectiveValue::Int(min)) = directive.arguments.get("min") {
623 let expr = TLExpr::pred(
624 "minLength",
625 vec![Term::var(field_var), Term::constant(min.to_string())],
626 );
627 constraints.push(expr);
628 }
629
630 if let Some(DirectiveValue::Int(max)) = directive.arguments.get("max") {
631 let expr = TLExpr::pred(
632 "maxLength",
633 vec![Term::var(field_var), Term::constant(max.to_string())],
634 );
635 constraints.push(expr);
636 }
637
638 constraints
639 }
640
641 fn pattern_directive_to_expr(
643 &self,
644 field_var: &str,
645 directive: &GraphQLDirective,
646 ) -> Vec<TLExpr> {
647 if let Some(DirectiveValue::String(pattern)) = directive.arguments.get("value") {
648 vec![TLExpr::pred(
649 "matches",
650 vec![Term::var(field_var), Term::constant(pattern)],
651 )]
652 } else {
653 vec![]
654 }
655 }
656
657 fn unique_items_directive_to_expr(&self, field_var: &str) -> TLExpr {
659 TLExpr::pred("allUnique", vec![Term::var(field_var)])
660 }
661
662 pub fn get_constraints(&self, type_name: &str) -> Vec<TLExpr> {
664 let mut constraints = Vec::new();
665
666 if let Some(type_def) = self.types.get(type_name) {
667 for field in &type_def.fields {
668 constraints.extend(self.directives_to_constraints(type_name, field));
669 }
670 }
671
672 constraints
673 }
674
675 pub fn types(&self) -> &HashMap<String, GraphQLType> {
677 &self.types
678 }
679}
680
681fn capitalize_first(s: &str) -> String {
683 let mut chars = s.chars();
684 match chars.next() {
685 None => String::new(),
686 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
687 }
688}
689
690impl Default for GraphQLConverter {
691 fn default() -> Self {
692 Self::new()
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn test_graphql_converter_basic() {
702 let converter = GraphQLConverter::new();
703 assert!(converter.types().is_empty());
704 }
705
706 #[test]
707 fn test_parse_simple_type() {
708 let schema = r#"
709 type Person {
710 name: String!
711 age: Int
712 }
713 "#;
714
715 let mut converter = GraphQLConverter::new();
716 let result = converter.parse_schema(schema);
717 assert!(result.is_ok());
718
719 assert_eq!(converter.types().len(), 1);
720 assert!(converter.types().contains_key("Person"));
721 }
722
723 #[test]
724 fn test_parse_type_with_fields() {
725 let schema = r#"
726 type User {
727 id: ID!
728 name: String!
729 email: String
730 }
731 "#;
732
733 let mut converter = GraphQLConverter::new();
734 converter.parse_schema(schema).unwrap();
735
736 let user_type = converter.types().get("User").unwrap();
737 assert_eq!(user_type.fields.len(), 3);
738
739 let id_field = &user_type.fields[0];
740 assert_eq!(id_field.name, "id");
741 assert_eq!(id_field.field_type, "ID");
742 assert!(id_field.is_required);
743 }
744
745 #[test]
746 fn test_parse_type_with_list() {
747 let schema = r#"
748 type Post {
749 tags: [String!]
750 comments: [Comment]
751 }
752 "#;
753
754 let mut converter = GraphQLConverter::new();
755 converter.parse_schema(schema).unwrap();
756
757 let post_type = converter.types().get("Post").unwrap();
758 assert_eq!(post_type.fields.len(), 2);
759
760 let tags_field = &post_type.fields[0];
761 assert!(tags_field.is_list);
762 }
763
764 #[test]
765 fn test_to_symbol_table() {
766 let schema = r#"
767 type Person {
768 name: String!
769 age: Int
770 }
771 "#;
772
773 let mut converter = GraphQLConverter::new();
774 let table = converter.parse_schema(schema).unwrap();
775
776 assert!(table.domains.contains_key("Person"));
778
779 assert!(table.predicates.contains_key("Person_name"));
781 assert!(table.predicates.contains_key("Person_age"));
782 }
783
784 #[test]
785 fn test_parse_multiple_types() {
786 let schema = r#"
787 type Author {
788 name: String!
789 }
790
791 type Book {
792 title: String!
793 author: Author!
794 }
795 "#;
796
797 let mut converter = GraphQLConverter::new();
798 converter.parse_schema(schema).unwrap();
799
800 assert_eq!(converter.types().len(), 2);
801 assert!(converter.types().contains_key("Author"));
802 assert!(converter.types().contains_key("Book"));
803 }
804
805 #[test]
806 fn test_skip_special_types() {
807 let schema = r#"
808 type Query {
809 user(id: ID!): User
810 }
811
812 type User {
813 name: String!
814 }
815 "#;
816
817 let mut converter = GraphQLConverter::new();
818 let table = converter.parse_schema(schema).unwrap();
819
820 assert!(!table.domains.contains_key("Query"));
822
823 assert!(table.domains.contains_key("User"));
825 }
826
827 #[test]
830 fn test_parse_directive_simple() {
831 let converter = GraphQLConverter::new();
832 let directives = converter.parse_directives("name: String! @deprecated");
833
834 assert_eq!(directives.len(), 1);
835 assert_eq!(directives[0].name, "deprecated");
836 assert!(directives[0].arguments.is_empty());
837 }
838
839 #[test]
840 fn test_parse_directive_with_arguments() {
841 let converter = GraphQLConverter::new();
842 let directives = converter.parse_directives(r#"age: Int @constraint(min: 0, max: 120)"#);
843
844 assert_eq!(directives.len(), 1);
845 assert_eq!(directives[0].name, "constraint");
846 assert_eq!(directives[0].arguments.len(), 2);
847
848 assert_eq!(
849 directives[0].arguments.get("min"),
850 Some(&DirectiveValue::Int(0))
851 );
852 assert_eq!(
853 directives[0].arguments.get("max"),
854 Some(&DirectiveValue::Int(120))
855 );
856 }
857
858 #[test]
859 fn test_parse_directive_string_argument() {
860 let converter = GraphQLConverter::new();
861 let directives =
862 converter.parse_directives(r#"email: String @constraint(pattern: "^[a-z]+$")"#);
863
864 assert_eq!(directives.len(), 1);
865 assert_eq!(
866 directives[0].arguments.get("pattern"),
867 Some(&DirectiveValue::String("^[a-z]+$".to_string()))
868 );
869 }
870
871 #[test]
872 fn test_parse_multiple_directives() {
873 let converter = GraphQLConverter::new();
874 let directives = converter
875 .parse_directives(r#"name: String @length(min: 3, max: 50) @pattern(value: "[a-z]+")"#);
876
877 assert_eq!(directives.len(), 2);
878 assert_eq!(directives[0].name, "length");
879 assert_eq!(directives[1].name, "pattern");
880 }
881
882 #[test]
883 fn test_field_with_directive_parsing() {
884 let schema = r#"
885 type User {
886 age: Int @constraint(min: 0, max: 120)
887 }
888 "#;
889
890 let mut converter = GraphQLConverter::new();
891 converter.parse_schema(schema).unwrap();
892
893 let user_type = converter.types().get("User").unwrap();
894 assert_eq!(user_type.fields.len(), 1);
895
896 let age_field = &user_type.fields[0];
897 assert_eq!(age_field.directives.len(), 1);
898 assert_eq!(age_field.directives[0].name, "constraint");
899 }
900
901 #[test]
902 fn test_constraint_directive_to_expr() {
903 let converter = GraphQLConverter::new();
904
905 let mut directive_args = HashMap::new();
906 directive_args.insert("min".to_string(), DirectiveValue::Int(0));
907 directive_args.insert("max".to_string(), DirectiveValue::Int(120));
908
909 let directive = GraphQLDirective {
910 name: "constraint".to_string(),
911 arguments: directive_args,
912 };
913
914 let constraints = converter.constraint_directive_to_expr("User_age", &directive);
915
916 assert_eq!(constraints.len(), 2);
917 let expr_strs: Vec<String> = constraints.iter().map(|e| format!("{:?}", e)).collect();
919 assert!(expr_strs
920 .iter()
921 .any(|s| s.contains("greaterOrEqual") && s.contains("User_age")));
922 assert!(expr_strs
923 .iter()
924 .any(|s| s.contains("lessOrEqual") && s.contains("User_age")));
925 }
926
927 #[test]
928 fn test_length_directive_to_expr() {
929 let converter = GraphQLConverter::new();
930
931 let mut directive_args = HashMap::new();
932 directive_args.insert("min".to_string(), DirectiveValue::Int(3));
933 directive_args.insert("max".to_string(), DirectiveValue::Int(50));
934
935 let directive = GraphQLDirective {
936 name: "length".to_string(),
937 arguments: directive_args,
938 };
939
940 let constraints = converter.length_directive_to_expr("User_name", &directive);
941
942 assert_eq!(constraints.len(), 2);
943 let expr_strs: Vec<String> = constraints.iter().map(|e| format!("{:?}", e)).collect();
944 assert!(expr_strs
945 .iter()
946 .any(|s| s.contains("minLength") && s.contains("User_name")));
947 assert!(expr_strs
948 .iter()
949 .any(|s| s.contains("maxLength") && s.contains("User_name")));
950 }
951
952 #[test]
953 fn test_pattern_directive_to_expr() {
954 let converter = GraphQLConverter::new();
955
956 let mut directive_args = HashMap::new();
957 directive_args.insert(
958 "value".to_string(),
959 DirectiveValue::String("^[a-z]+$".to_string()),
960 );
961
962 let directive = GraphQLDirective {
963 name: "pattern".to_string(),
964 arguments: directive_args,
965 };
966
967 let constraints = converter.pattern_directive_to_expr("User_username", &directive);
968
969 assert_eq!(constraints.len(), 1);
970 let expr_str = format!("{:?}", constraints[0]);
971 assert!(expr_str.contains("matches"));
972 assert!(expr_str.contains("User_username"));
973 assert!(expr_str.contains("^[a-z]+$"));
974 }
975
976 #[test]
977 fn test_range_directive_to_expr() {
978 let converter = GraphQLConverter::new();
979
980 let mut directive_args = HashMap::new();
981 directive_args.insert("min".to_string(), DirectiveValue::Int(18));
982 directive_args.insert("max".to_string(), DirectiveValue::Int(65));
983
984 let directive = GraphQLDirective {
985 name: "range".to_string(),
986 arguments: directive_args,
987 };
988
989 let constraints = converter.range_directive_to_expr("User_age", &directive);
990
991 assert_eq!(constraints.len(), 2);
992 }
993
994 #[test]
995 fn test_unique_items_directive() {
996 let converter = GraphQLConverter::new();
997 let expr = converter.unique_items_directive_to_expr("User_tags");
998
999 let expr_str = format!("{:?}", expr);
1000 assert!(expr_str.contains("allUnique"));
1001 assert!(expr_str.contains("User_tags"));
1002 }
1003
1004 #[test]
1005 fn test_get_constraints_for_type() {
1006 let schema = r#"
1007 type Product {
1008 price: Float @constraint(min: 0.0, max: 10000.0)
1009 name: String @length(min: 1, max: 100)
1010 sku: String @pattern(value: "^[A-Z0-9]+$")
1011 }
1012 "#;
1013
1014 let mut converter = GraphQLConverter::new();
1015 converter.parse_schema(schema).unwrap();
1016
1017 let constraints = converter.get_constraints("Product");
1018
1019 assert!(constraints.len() >= 5); }
1022
1023 #[test]
1024 fn test_format_directive_to_expr() {
1025 let converter = GraphQLConverter::new();
1026
1027 let mut directive_args = HashMap::new();
1028 directive_args.insert(
1029 "format".to_string(),
1030 DirectiveValue::String("email".to_string()),
1031 );
1032
1033 let directive = GraphQLDirective {
1034 name: "constraint".to_string(),
1035 arguments: directive_args,
1036 };
1037
1038 let constraints = converter.constraint_directive_to_expr("User_email", &directive);
1039
1040 assert_eq!(constraints.len(), 1);
1041 let expr_str = format!("{:?}", constraints[0]);
1042 assert!(expr_str.contains("isValidEmail"));
1043 assert!(expr_str.contains("User_email"));
1044 }
1045
1046 #[test]
1047 fn test_directive_value_parsing_boolean() {
1048 let converter = GraphQLConverter::new();
1049
1050 assert_eq!(
1051 converter.parse_directive_value("true"),
1052 Some(DirectiveValue::Boolean(true))
1053 );
1054 assert_eq!(
1055 converter.parse_directive_value("false"),
1056 Some(DirectiveValue::Boolean(false))
1057 );
1058 }
1059
1060 #[test]
1061 fn test_directive_value_parsing_numbers() {
1062 let converter = GraphQLConverter::new();
1063
1064 assert_eq!(
1065 converter.parse_directive_value("42"),
1066 Some(DirectiveValue::Int(42))
1067 );
1068 assert_eq!(
1069 converter.parse_directive_value("2.5"),
1070 Some(DirectiveValue::Float(2.5))
1071 );
1072 }
1073
1074 #[test]
1075 fn test_directive_value_parsing_list() {
1076 let converter = GraphQLConverter::new();
1077
1078 if let Some(DirectiveValue::List(items)) = converter.parse_directive_value("[1, 2, 3]") {
1079 assert_eq!(items.len(), 3);
1080 assert_eq!(items[0], DirectiveValue::Int(1));
1081 assert_eq!(items[1], DirectiveValue::Int(2));
1082 assert_eq!(items[2], DirectiveValue::Int(3));
1083 } else {
1084 panic!("Expected List");
1085 }
1086 }
1087
1088 #[test]
1089 fn test_complex_directive_scenario() {
1090 let schema = r#"
1091 type User {
1092 username: String! @constraint(minLength: 3, maxLength: 20, pattern: "^[a-z0-9_]+$")
1093 age: Int! @range(min: 13, max: 120)
1094 email: String! @constraint(format: "email")
1095 tags: [String!] @uniqueItems
1096 }
1097 "#;
1098
1099 let mut converter = GraphQLConverter::new();
1100 converter.parse_schema(schema).unwrap();
1101
1102 let user_type = converter.types().get("User").unwrap();
1103
1104 let username_field = &user_type
1106 .fields
1107 .iter()
1108 .find(|f| f.name == "username")
1109 .unwrap();
1110 assert!(!username_field.directives.is_empty());
1111
1112 let constraints = converter.get_constraints("User");
1114 assert!(constraints.len() >= 7); }
1116
1117 #[test]
1118 fn test_capitalize_first() {
1119 assert_eq!(capitalize_first("email"), "Email");
1120 assert_eq!(capitalize_first("url"), "Url");
1121 assert_eq!(capitalize_first(""), "");
1122 assert_eq!(capitalize_first("A"), "A");
1123 }
1124}