Skip to main content

tensorlogic_oxirs_bridge/graphql/
mod.rs

1//! GraphQL schema integration for TensorLogic
2//!
3//! This module provides functionality to parse GraphQL schemas and convert them
4//! to TensorLogic symbol tables and rules.
5//!
6//! ## Supported Features
7//!
8//! - **Type Definitions**: GraphQL types → TensorLogic domains
9//! - **Field Definitions**: GraphQL fields → TensorLogic predicates
10//! - **Directives**: GraphQL directives → constraint rules
11//! - **Interfaces**: GraphQL interfaces → domain hierarchies (future)
12//!
13//! ## Example
14//!
15//! ```ignore
16//! use tensorlogic_oxirs_bridge::GraphQLConverter;
17//!
18//! let schema = r#"
19//!     type Person {
20//!         name: String!
21//!         age: Int
22//!         friends: [Person!]
23//!     }
24//!
25//!     type Query {
26//!         person(id: ID!): Person
27//!     }
28//! "#;
29//!
30//! let converter = GraphQLConverter::new();
31//! let symbol_table = converter.parse_schema(schema)?;
32//! ```
33
34use anyhow::Result;
35use std::collections::HashMap;
36use tensorlogic_adapters::{DomainInfo, PredicateInfo, SymbolTable};
37use tensorlogic_ir::{TLExpr, Term};
38
39/// Represents a GraphQL type definition
40#[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/// Represents a GraphQL field definition
49#[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/// Represents a GraphQL field argument
61#[derive(Debug, Clone)]
62pub struct GraphQLArgument {
63    pub name: String,
64    pub arg_type: String,
65    pub is_required: bool,
66}
67
68/// Represents a GraphQL directive
69#[derive(Debug, Clone, PartialEq)]
70pub struct GraphQLDirective {
71    pub name: String,
72    pub arguments: HashMap<String, DirectiveValue>,
73}
74
75/// Directive argument value
76#[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
85/// GraphQL schema converter
86pub 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    /// Parse a GraphQL schema string (simplified implementation)
98    ///
99    /// Note: This is a basic implementation that handles simple type definitions.
100    /// For full GraphQL parsing, consider using a dedicated GraphQL parser crate.
101    pub fn parse_schema(&mut self, schema: &str) -> Result<SymbolTable> {
102        self.types.clear();
103
104        // Simple parsing logic - in production, use a proper GraphQL parser
105        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            // Skip empty lines and comments
112            if line.is_empty() || line.starts_with('#') {
113                i += 1;
114                continue;
115            }
116
117            // Parse type definitions
118            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    /// Parse a type definition from GraphQL schema
130    fn parse_type_definition(&self, lines: &[&str], index: &mut usize) -> Result<GraphQLType> {
131        let line = lines[*index];
132
133        // Extract type name from "type TypeName {" or "type TypeName implements Interface {"
134        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        // Parse fields until we hit closing brace
168        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            // Parse field: "fieldName: Type" or "fieldName(args): Type"
182            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    /// Parse a field definition
198    fn parse_field(&self, line: &str) -> Result<Option<GraphQLField>> {
199        // Handle field with arguments: "fieldName(arg: Type): ReturnType @directive"
200        // or simple field: "fieldName: Type @directive"
201
202        let field_line = if line.find(':').is_some() {
203            line
204        } else {
205            return Ok(None); // Not a valid field line
206        };
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        // Extract directives from type part
214        let directives = self.parse_directives(type_part);
215
216        // Remove directives from type part
217        if let Some(at_pos) = type_part.find('@') {
218            type_part = type_part[..at_pos].trim();
219        }
220
221        // Extract field name and arguments
222        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        // Parse field type (handle !, [], etc.)
242        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    /// Parse field arguments
256    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    /// Parse a GraphQL type string (handles !, [], etc.)
281    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    /// Convert GraphQL types to a TensorLogic symbol table
302    pub fn to_symbol_table(&self) -> Result<SymbolTable> {
303        let mut table = SymbolTable::new();
304
305        // First, add scalar types as domains
306        for scalar in &["String", "Int", "Float", "Boolean", "ID", "Value"] {
307            let domain = DomainInfo::new(*scalar, 1000); // Large cardinality for scalars
308            table.add_domain(domain)?;
309        }
310
311        // Convert types to domains (skip special GraphQL types)
312        for (type_name, type_def) in &self.types {
313            // Skip special types like Query, Mutation, Subscription
314            if matches!(type_name.as_str(), "Query" | "Mutation" | "Subscription") {
315                continue;
316            }
317
318            let mut domain = DomainInfo::new(type_name, 100); // Default cardinality
319
320            if let Some(desc) = &type_def.description {
321                domain = domain.with_description(desc);
322            }
323
324            table.add_domain(domain)?;
325        }
326
327        // Convert fields to predicates (excluding special types)
328        for type_def in self.types.values() {
329            // Skip special types - they're not domains
330            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                // Add field type as second argument if it's a known type
342                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()); // Default domain for unknown types
348                }
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    /// Check if a type is a GraphQL scalar type
364    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    /// Parse directives from a field line
372    /// Format: @directiveName(arg1: value1, arg2: value2)
373    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            // Extract directive name
382            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            // Parse arguments if present
388            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    /// Parse directive arguments
411    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    /// Parse a directive value
434    fn parse_directive_value(&self, value_str: &str) -> Option<DirectiveValue> {
435        Self::parse_directive_value_impl(value_str)
436    }
437
438    /// Parse directive value implementation (static to avoid recursion warning)
439    fn parse_directive_value_impl(value_str: &str) -> Option<DirectiveValue> {
440        let value_str = value_str.trim();
441
442        // String literal
443        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        // Boolean
449        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        // Integer
457        if let Ok(i) = value_str.parse::<i64>() {
458            return Some(DirectiveValue::Int(i));
459        }
460
461        // Float
462        if let Ok(f) = value_str.parse::<f64>() {
463            return Some(DirectiveValue::Float(f));
464        }
465
466        // List (simplified - handle basic cases)
467        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    /// Convert field directives to TensorLogic constraint expressions
480    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                _ => {} // Ignore unknown directives
502            }
503        }
504
505        constraints
506    }
507
508    /// Convert @constraint directive to TL expressions
509    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        // Handle minLength
517        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        // Handle maxLength
526        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        // Handle min (numeric)
535        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        // Handle max (numeric)
551        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        // Handle pattern (regex)
567        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        // Handle format (email, url, etc.)
576        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    /// Convert @range directive to TL expression
588    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    /// Convert @length directive to TL expression
615    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    /// Convert @pattern directive to TL expression
642    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    /// Convert @uniqueItems directive to TL expression
658    fn unique_items_directive_to_expr(&self, field_var: &str) -> TLExpr {
659        TLExpr::pred("allUnique", vec![Term::var(field_var)])
660    }
661
662    /// Get all constraints for a type
663    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    /// Get parsed types
676    pub fn types(&self) -> &HashMap<String, GraphQLType> {
677        &self.types
678    }
679}
680
681/// Capitalize first letter of a string
682fn 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        // Should have Person domain
777        assert!(table.domains.contains_key("Person"));
778
779        // Should have predicates for fields
780        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        // Query type should not be added as a domain
821        assert!(!table.domains.contains_key("Query"));
822
823        // But User should be added
824        assert!(table.domains.contains_key("User"));
825    }
826
827    // ====== Directive Tests ======
828
829    #[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        // Should have greaterOrEqual and lessOrEqual predicates
918        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        // Should have constraints from all three fields
1020        assert!(constraints.len() >= 5); // 2 from price, 2 from name, 1 from sku
1021    }
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        // Verify username field has constraint directive
1105        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        // Verify all constraints can be extracted
1113        let constraints = converter.get_constraints("User");
1114        assert!(constraints.len() >= 7); // Multiple constraints from multiple fields
1115    }
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}