Skip to main content

oxirs_arq/
graphql_translator.rs

1// GraphQL to SPARQL Translation Module
2// Provides comprehensive translation of GraphQL queries to SPARQL algebra
3
4use crate::algebra::{Algebra, Term, TriplePattern, Variable};
5use oxirs_core::model::NamedNode;
6use scirs2_core::metrics::MetricsRegistry;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10use thiserror::Error;
11
12#[allow(dead_code)]
13/// Errors that can occur during GraphQL to SPARQL translation
14#[derive(Error, Debug)]
15pub enum TranslationError {
16    #[error("Unsupported GraphQL operation: {0}")]
17    UnsupportedOperation(String),
18
19    #[error("Invalid field mapping: {0}")]
20    InvalidFieldMapping(String),
21
22    #[error("Unknown GraphQL type: {0}")]
23    UnknownType(String),
24
25    #[error("Fragment not found: {0}")]
26    FragmentNotFound(String),
27
28    #[error("Invalid argument: {0}")]
29    InvalidArgument(String),
30
31    #[error("Schema mapping error: {0}")]
32    SchemaMappingError(String),
33
34    #[error("Variable resolution error: {0}")]
35    VariableResolutionError(String),
36
37    #[error("Directive processing error: {0}")]
38    DirectiveError(String),
39
40    #[error("Nested query too deep: {0}")]
41    QueryTooDeep(usize),
42
43    #[error("Translation failed: {0}")]
44    TranslationFailed(String),
45}
46
47/// Result type for GraphQL translation operations
48pub type TranslationResult<T> = std::result::Result<T, TranslationError>;
49
50/// GraphQL operation types
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52pub enum GraphQLOperationType {
53    Query,
54    Mutation,
55    Subscription,
56}
57
58/// Simplified GraphQL field representation
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct GraphQLField {
61    pub name: String,
62    pub alias: Option<String>,
63    pub arguments: HashMap<String, GraphQLValue>,
64    pub directives: Vec<GraphQLDirective>,
65    pub selection_set: Vec<GraphQLSelection>,
66}
67
68/// GraphQL selection types
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub enum GraphQLSelection {
71    Field(GraphQLField),
72    FragmentSpread {
73        name: String,
74        directives: Vec<GraphQLDirective>,
75    },
76    InlineFragment {
77        type_condition: Option<String>,
78        directives: Vec<GraphQLDirective>,
79        selection_set: Vec<GraphQLSelection>,
80    },
81}
82
83/// GraphQL directive
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct GraphQLDirective {
86    pub name: String,
87    pub arguments: HashMap<String, GraphQLValue>,
88}
89
90/// GraphQL value types
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum GraphQLValue {
93    Null,
94    Int(i64),
95    Float(f64),
96    String(String),
97    Boolean(bool),
98    Enum(String),
99    List(Vec<GraphQLValue>),
100    Object(HashMap<String, GraphQLValue>),
101    Variable(String),
102}
103
104/// GraphQL operation (query/mutation/subscription)
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct GraphQLOperation {
107    pub operation_type: GraphQLOperationType,
108    pub name: Option<String>,
109    pub variables: HashMap<String, GraphQLVariableDefinition>,
110    pub directives: Vec<GraphQLDirective>,
111    pub selection_set: Vec<GraphQLSelection>,
112}
113
114/// GraphQL variable definition
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct GraphQLVariableDefinition {
117    pub var_type: String,
118    pub default_value: Option<GraphQLValue>,
119}
120
121/// GraphQL fragment definition
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct GraphQLFragment {
124    pub name: String,
125    pub type_condition: String,
126    pub directives: Vec<GraphQLDirective>,
127    pub selection_set: Vec<GraphQLSelection>,
128}
129
130/// GraphQL document containing operations and fragments
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct GraphQLDocument {
133    pub operations: Vec<GraphQLOperation>,
134    pub fragments: HashMap<String, GraphQLFragment>,
135}
136
137/// Schema mapping configuration for GraphQL to RDF translation
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SchemaMapping {
140    /// Map GraphQL type names to RDF class URIs
141    pub type_to_class: HashMap<String, String>,
142
143    /// Map GraphQL field names to RDF property URIs
144    pub field_to_property: HashMap<String, String>,
145
146    /// RDF namespace prefixes
147    pub prefixes: HashMap<String, String>,
148
149    /// Root type for queries (default: "Query")
150    pub query_root_type: String,
151
152    /// Root type for mutations (default: "Mutation")
153    pub mutation_root_type: String,
154
155    /// Default RDF type property (default: rdf:type)
156    pub rdf_type_property: String,
157
158    /// Enable automatic camelCase to snake_case conversion
159    pub auto_case_conversion: bool,
160}
161
162impl Default for SchemaMapping {
163    fn default() -> Self {
164        let mut prefixes = HashMap::new();
165        prefixes.insert(
166            "rdf".to_string(),
167            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
168        );
169        prefixes.insert(
170            "rdfs".to_string(),
171            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
172        );
173        prefixes.insert(
174            "xsd".to_string(),
175            "http://www.w3.org/2001/XMLSchema#".to_string(),
176        );
177
178        Self {
179            type_to_class: HashMap::new(),
180            field_to_property: HashMap::new(),
181            prefixes,
182            query_root_type: "Query".to_string(),
183            mutation_root_type: "Mutation".to_string(),
184            rdf_type_property: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
185            auto_case_conversion: true,
186        }
187    }
188}
189
190/// Configuration for GraphQL to SPARQL translation
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct TranslatorConfig {
193    /// Schema mapping configuration
194    pub schema_mapping: SchemaMapping,
195
196    /// Maximum query depth to prevent overly complex translations
197    pub max_query_depth: usize,
198
199    /// Enable query optimization after translation
200    pub enable_optimization: bool,
201
202    /// Generate SPARQL comments with GraphQL source
203    pub generate_comments: bool,
204
205    /// Handle @include and @skip directives
206    pub process_directives: bool,
207}
208
209impl Default for TranslatorConfig {
210    fn default() -> Self {
211        Self {
212            schema_mapping: SchemaMapping::default(),
213            max_query_depth: 10,
214            enable_optimization: true,
215            generate_comments: true,
216            process_directives: true,
217        }
218    }
219}
220
221/// Translation context for tracking state during translation
222#[derive(Debug, Clone)]
223struct TranslationContext {
224    /// Current query depth
225    depth: usize,
226
227    /// Variable counter for generating unique SPARQL variables
228    var_counter: usize,
229
230    /// Fragment definitions available for expansion
231    fragments: HashMap<String, GraphQLFragment>,
232
233    /// GraphQL variable values
234    variables: HashMap<String, GraphQLValue>,
235
236    /// Current subject variable
237    current_subject: Option<Variable>,
238
239    /// Collected SPARQL variables
240    sparql_variables: HashSet<Variable>,
241
242    /// Collected triple patterns (as algebra) - reserved for future optimization
243    #[allow(dead_code)]
244    patterns: Vec<Algebra>,
245}
246
247impl TranslationContext {
248    fn new(
249        fragments: HashMap<String, GraphQLFragment>,
250        variables: HashMap<String, GraphQLValue>,
251    ) -> Self {
252        Self {
253            depth: 0,
254            var_counter: 0,
255            fragments,
256            variables,
257            current_subject: None,
258            sparql_variables: HashSet::new(),
259            patterns: Vec::new(),
260        }
261    }
262
263    fn next_var(&mut self, prefix: &str) -> Variable {
264        self.var_counter += 1;
265        let var = Variable::new(format!("{}{}", prefix, self.var_counter))
266            .expect("Variable name should be valid");
267        self.sparql_variables.insert(var.clone());
268        var
269    }
270
271    fn enter_scope(&mut self) -> TranslationResult<()> {
272        self.depth += 1;
273        Ok(())
274    }
275
276    fn exit_scope(&mut self) {
277        self.depth = self.depth.saturating_sub(1);
278    }
279}
280
281/// Statistics for translation operations
282#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283pub struct TranslationStats {
284    pub queries_translated: usize,
285    pub mutations_translated: usize,
286    pub fields_translated: usize,
287    pub fragments_expanded: usize,
288    pub directives_processed: usize,
289    pub average_query_depth: f64,
290    pub translation_errors: usize,
291}
292
293/// Main GraphQL to SPARQL translator
294pub struct GraphQLTranslator {
295    config: TranslatorConfig,
296    stats: TranslationStats,
297    /// Metrics registry - reserved for future instrumentation
298    #[allow(dead_code)]
299    metrics: Arc<MetricsRegistry>,
300}
301
302impl GraphQLTranslator {
303    /// Create a new GraphQL translator with default configuration
304    pub fn new() -> Self {
305        Self {
306            config: TranslatorConfig::default(),
307            stats: TranslationStats::default(),
308            metrics: Arc::new(MetricsRegistry::new()),
309        }
310    }
311
312    /// Create a new GraphQL translator with custom configuration
313    pub fn with_config(config: TranslatorConfig) -> Self {
314        Self {
315            config,
316            stats: TranslationStats::default(),
317            metrics: Arc::new(MetricsRegistry::new()),
318        }
319    }
320
321    /// Translate a GraphQL document to SPARQL algebra
322    pub fn translate_document(
323        &mut self,
324        document: GraphQLDocument,
325    ) -> TranslationResult<Vec<Algebra>> {
326        let mut algebras = Vec::new();
327
328        for operation in document.operations {
329            let algebra = self.translate_operation(&operation, &document.fragments)?;
330            algebras.push(algebra);
331        }
332
333        Ok(algebras)
334    }
335
336    /// Translate a single GraphQL operation to SPARQL algebra
337    pub fn translate_operation(
338        &mut self,
339        operation: &GraphQLOperation,
340        fragments: &HashMap<String, GraphQLFragment>,
341    ) -> TranslationResult<Algebra> {
342        let mut context = TranslationContext::new(fragments.clone(), HashMap::new());
343
344        match operation.operation_type {
345            GraphQLOperationType::Query => {
346                self.stats.queries_translated += 1;
347                self.translate_query(operation, &mut context)
348            }
349            GraphQLOperationType::Mutation => {
350                self.stats.mutations_translated += 1;
351                self.translate_mutation(operation, &mut context)
352            }
353            GraphQLOperationType::Subscription => Err(TranslationError::UnsupportedOperation(
354                "Subscriptions are not yet supported".to_string(),
355            )),
356        }
357    }
358
359    /// Translate a GraphQL query to SPARQL SELECT algebra
360    fn translate_query(
361        &mut self,
362        operation: &GraphQLOperation,
363        context: &mut TranslationContext,
364    ) -> TranslationResult<Algebra> {
365        context.enter_scope()?;
366
367        // Process selection set
368        let patterns = self.translate_selection_set(&operation.selection_set, context)?;
369
370        // Combine all patterns into a single BGP
371        let bgp = if patterns.is_empty() {
372            Algebra::Bgp(vec![])
373        } else if patterns.len() == 1 {
374            patterns
375                .into_iter()
376                .next()
377                .expect("collection validated to be non-empty")
378        } else {
379            // Join all patterns
380            patterns
381                .into_iter()
382                .reduce(|acc, pattern| Algebra::Join {
383                    left: Box::new(acc),
384                    right: Box::new(pattern),
385                })
386                .expect("collection validated to be non-empty")
387        };
388
389        // Project the variables collected during translation
390        let variables: Vec<Variable> = context.sparql_variables.iter().cloned().collect();
391        let result = Algebra::Project {
392            pattern: Box::new(bgp),
393            variables,
394        };
395
396        context.exit_scope();
397        Ok(result)
398    }
399
400    /// Translate a GraphQL mutation to SPARQL UPDATE algebra
401    fn translate_mutation(
402        &mut self,
403        _operation: &GraphQLOperation,
404        _context: &mut TranslationContext,
405    ) -> TranslationResult<Algebra> {
406        // For now, return a basic pattern
407        // Full mutation support would require UPDATE algebra extensions
408        Err(TranslationError::UnsupportedOperation(
409            "Mutations require SPARQL UPDATE support".to_string(),
410        ))
411    }
412
413    /// Translate a GraphQL selection set to SPARQL patterns
414    fn translate_selection_set(
415        &mut self,
416        selections: &[GraphQLSelection],
417        context: &mut TranslationContext,
418    ) -> TranslationResult<Vec<Algebra>> {
419        if context.depth > self.config.max_query_depth {
420            return Err(TranslationError::QueryTooDeep(context.depth));
421        }
422
423        let mut patterns = Vec::new();
424
425        for selection in selections {
426            match selection {
427                GraphQLSelection::Field(field) => {
428                    let pattern = self.translate_field(field, context)?;
429                    patterns.push(pattern);
430                    self.stats.fields_translated += 1;
431                }
432                GraphQLSelection::FragmentSpread { name, directives } => {
433                    if self.config.process_directives
434                        && self.should_skip_by_directives(directives, context)?
435                    {
436                        continue;
437                    }
438
439                    let fragment = context
440                        .fragments
441                        .get(name)
442                        .ok_or_else(|| TranslationError::FragmentNotFound(name.clone()))?;
443
444                    // Clone the selection set to avoid borrow checker issues
445                    let selection_set = fragment.selection_set.clone();
446                    let fragment_patterns =
447                        self.translate_selection_set(&selection_set, context)?;
448                    patterns.extend(fragment_patterns);
449                    self.stats.fragments_expanded += 1;
450                }
451                GraphQLSelection::InlineFragment {
452                    type_condition,
453                    directives,
454                    selection_set,
455                } => {
456                    if self.config.process_directives
457                        && self.should_skip_by_directives(directives, context)?
458                    {
459                        continue;
460                    }
461
462                    // Add type filter if type condition is present
463                    if let Some(type_name) = type_condition {
464                        if let Some(class_uri) =
465                            self.config.schema_mapping.type_to_class.get(type_name)
466                        {
467                            // Add rdf:type filter pattern
468                            let subject = context
469                                .current_subject
470                                .clone()
471                                .unwrap_or_else(|| context.next_var("subject"));
472
473                            let type_property =
474                                NamedNode::new(&self.config.schema_mapping.rdf_type_property)
475                                    .expect("Invalid RDF type property URI");
476                            let class_node = NamedNode::new(class_uri).expect("Invalid class URI");
477
478                            let type_pattern = Algebra::Bgp(vec![TriplePattern::new(
479                                Term::Variable(subject.clone()),
480                                Term::Iri(type_property),
481                                Term::Iri(class_node),
482                            )]);
483                            patterns.push(type_pattern);
484                        }
485                    }
486
487                    let inline_patterns = self.translate_selection_set(selection_set, context)?;
488                    patterns.extend(inline_patterns);
489                }
490            }
491        }
492
493        Ok(patterns)
494    }
495
496    /// Translate a GraphQL field to SPARQL pattern
497    fn translate_field(
498        &mut self,
499        field: &GraphQLField,
500        context: &mut TranslationContext,
501    ) -> TranslationResult<Algebra> {
502        // Check directives
503        if self.config.process_directives
504            && self.should_skip_by_directives(&field.directives, context)?
505        {
506            return Ok(Algebra::Bgp(vec![]));
507        }
508
509        // Get or create subject variable
510        let subject = context
511            .current_subject
512            .clone()
513            .unwrap_or_else(|| context.next_var("subject"));
514
515        // Map field name to RDF property
516        let property_uri = self.map_field_to_property(&field.name)?;
517
518        // Create object variable for this field
519        let object_var_name = field.alias.as_ref().unwrap_or(&field.name);
520        let object = context.next_var(object_var_name);
521
522        // Create basic triple pattern
523        let property_node = NamedNode::new(&property_uri).expect("Invalid property URI");
524
525        let triple_pattern = Algebra::Bgp(vec![TriplePattern::new(
526            Term::Variable(subject.clone()),
527            Term::Iri(property_node),
528            Term::Variable(object.clone()),
529        )]);
530
531        // Handle nested selections
532        if !field.selection_set.is_empty() {
533            let old_subject = context.current_subject.replace(object.clone());
534            context.enter_scope()?;
535
536            let nested_patterns = self.translate_selection_set(&field.selection_set, context)?;
537
538            context.exit_scope();
539            context.current_subject = old_subject;
540
541            // Join the triple pattern with nested patterns
542            if nested_patterns.is_empty() {
543                return Ok(triple_pattern);
544            }
545
546            let nested_algebra = nested_patterns
547                .into_iter()
548                .reduce(|acc, pattern| Algebra::Join {
549                    left: Box::new(acc),
550                    right: Box::new(pattern),
551                })
552                .expect("nested_patterns validated to be non-empty");
553
554            return Ok(Algebra::Join {
555                left: Box::new(triple_pattern),
556                right: Box::new(nested_algebra),
557            });
558        }
559
560        // Handle arguments as filters
561        if !field.arguments.is_empty() {
562            let filters = self.translate_arguments(&field.arguments, &object, context)?;
563            if !filters.is_empty() {
564                // Combine filters with AND logic
565                let combined_filter = filters
566                    .into_iter()
567                    .reduce(|acc, filter| {
568                        // For now, just join the filters
569                        // Full implementation would create proper FILTER expressions
570                        Algebra::Join {
571                            left: Box::new(acc),
572                            right: Box::new(filter),
573                        }
574                    })
575                    .expect("filters validated to be non-empty");
576
577                return Ok(Algebra::Join {
578                    left: Box::new(triple_pattern),
579                    right: Box::new(combined_filter),
580                });
581            }
582        }
583
584        Ok(triple_pattern)
585    }
586
587    /// Translate GraphQL arguments to SPARQL filters
588    fn translate_arguments(
589        &mut self,
590        arguments: &HashMap<String, GraphQLValue>,
591        _object: &Variable,
592        _context: &mut TranslationContext,
593    ) -> TranslationResult<Vec<Algebra>> {
594        let filters = Vec::new();
595
596        for (arg_name, arg_value) in arguments {
597            // For now, create placeholder filters
598            // Full implementation would create proper SPARQL FILTER expressions
599            let _filter_expr = format!("FILTER for {} = {:?}", arg_name, arg_value);
600            // filters.push(...) - would add actual filter algebra here
601        }
602
603        Ok(filters)
604    }
605
606    /// Check if a field should be skipped based on @include/@skip directives
607    fn should_skip_by_directives(
608        &mut self,
609        directives: &[GraphQLDirective],
610        context: &mut TranslationContext,
611    ) -> TranslationResult<bool> {
612        for directive in directives {
613            self.stats.directives_processed += 1;
614
615            match directive.name.as_str() {
616                "skip" => {
617                    if let Some(GraphQLValue::Boolean(should_skip)) = directive.arguments.get("if")
618                    {
619                        if *should_skip {
620                            return Ok(true);
621                        }
622                    } else if let Some(GraphQLValue::Variable(var_name)) =
623                        directive.arguments.get("if")
624                    {
625                        if let Some(GraphQLValue::Boolean(should_skip)) =
626                            context.variables.get(var_name)
627                        {
628                            if *should_skip {
629                                return Ok(true);
630                            }
631                        }
632                    }
633                }
634                "include" => {
635                    if let Some(GraphQLValue::Boolean(should_include)) =
636                        directive.arguments.get("if")
637                    {
638                        if !*should_include {
639                            return Ok(true);
640                        }
641                    } else if let Some(GraphQLValue::Variable(var_name)) =
642                        directive.arguments.get("if")
643                    {
644                        if let Some(GraphQLValue::Boolean(should_include)) =
645                            context.variables.get(var_name)
646                        {
647                            if !*should_include {
648                                return Ok(true);
649                            }
650                        }
651                    }
652                }
653                _ => {
654                    // Unknown directive - ignore for now
655                }
656            }
657        }
658
659        Ok(false)
660    }
661
662    /// Map a GraphQL field name to an RDF property URI
663    fn map_field_to_property(&self, field_name: &str) -> TranslationResult<String> {
664        // First check explicit mapping
665        if let Some(property_uri) = self.config.schema_mapping.field_to_property.get(field_name) {
666            return Ok(property_uri.clone());
667        }
668
669        // Try case conversion if enabled
670        if self.config.schema_mapping.auto_case_conversion {
671            let snake_case = self.camel_to_snake_case(field_name);
672            if let Some(property_uri) = self
673                .config
674                .schema_mapping
675                .field_to_property
676                .get(&snake_case)
677            {
678                return Ok(property_uri.clone());
679            }
680        }
681
682        // Generate a default property URI based on field name
683        Ok(format!("http://example.org/property/{}", field_name))
684    }
685
686    /// Convert camelCase to snake_case
687    /// Handles consecutive uppercase letters by treating them as separate words
688    fn camel_to_snake_case(&self, s: &str) -> String {
689        let mut result = String::new();
690        let chars: Vec<char> = s.chars().collect();
691
692        for (i, &c) in chars.iter().enumerate() {
693            if c.is_uppercase() {
694                // Add underscore if:
695                // 1. Not at the beginning
696                // 2. Previous char was lowercase
697                // 3. OR next char exists and is lowercase (for consecutive uppercase)
698                let should_add_underscore = i > 0
699                    && (chars[i - 1].is_lowercase()
700                        || (i + 1 < chars.len() && chars[i + 1].is_lowercase()));
701
702                if should_add_underscore && !result.is_empty() && !result.ends_with('_') {
703                    result.push('_');
704                }
705                result.push(c.to_ascii_lowercase());
706            } else {
707                result.push(c);
708            }
709        }
710
711        result
712    }
713
714    /// Get translation statistics
715    pub fn get_stats(&self) -> &TranslationStats {
716        &self.stats
717    }
718
719    /// Reset translation statistics
720    pub fn reset_stats(&mut self) {
721        self.stats = TranslationStats::default();
722    }
723
724    /// Update schema mapping configuration
725    pub fn update_schema_mapping(&mut self, mapping: SchemaMapping) {
726        self.config.schema_mapping = mapping;
727    }
728
729    /// Add a type mapping (GraphQL type -> RDF class)
730    pub fn add_type_mapping(&mut self, graphql_type: String, rdf_class: String) {
731        self.config
732            .schema_mapping
733            .type_to_class
734            .insert(graphql_type, rdf_class);
735    }
736
737    /// Add a field mapping (GraphQL field -> RDF property)
738    pub fn add_field_mapping(&mut self, graphql_field: String, rdf_property: String) {
739        self.config
740            .schema_mapping
741            .field_to_property
742            .insert(graphql_field, rdf_property);
743    }
744
745    /// Add a namespace prefix
746    pub fn add_prefix(&mut self, prefix: String, namespace: String) {
747        self.config
748            .schema_mapping
749            .prefixes
750            .insert(prefix, namespace);
751    }
752}
753
754impl Default for GraphQLTranslator {
755    fn default() -> Self {
756        Self::new()
757    }
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763
764    #[test]
765    fn test_translator_creation() {
766        let translator = GraphQLTranslator::new();
767        assert_eq!(translator.stats.queries_translated, 0);
768    }
769
770    #[test]
771    fn test_camel_to_snake_case() {
772        let translator = GraphQLTranslator::new();
773        assert_eq!(translator.camel_to_snake_case("firstName"), "first_name");
774        assert_eq!(translator.camel_to_snake_case("userName"), "user_name");
775        assert_eq!(translator.camel_to_snake_case("id"), "id");
776        assert_eq!(
777            translator.camel_to_snake_case("HTTPResponse"),
778            "http_response"
779        );
780        assert_eq!(translator.camel_to_snake_case("XMLParser"), "xml_parser");
781        assert_eq!(translator.camel_to_snake_case("IOError"), "io_error");
782    }
783
784    #[test]
785    fn test_schema_mapping_default() {
786        let mapping = SchemaMapping::default();
787        assert_eq!(mapping.query_root_type, "Query");
788        assert_eq!(mapping.mutation_root_type, "Mutation");
789        assert!(mapping.prefixes.contains_key("rdf"));
790        assert!(mapping.prefixes.contains_key("rdfs"));
791        assert!(mapping.prefixes.contains_key("xsd"));
792    }
793
794    #[test]
795    fn test_add_type_mapping() {
796        let mut translator = GraphQLTranslator::new();
797        translator.add_type_mapping("User".to_string(), "http://example.org/User".to_string());
798        assert_eq!(
799            translator.config.schema_mapping.type_to_class.get("User"),
800            Some(&"http://example.org/User".to_string())
801        );
802    }
803
804    #[test]
805    fn test_add_field_mapping() {
806        let mut translator = GraphQLTranslator::new();
807        translator.add_field_mapping(
808            "name".to_string(),
809            "http://xmlns.com/foaf/0.1/name".to_string(),
810        );
811        assert_eq!(
812            translator
813                .config
814                .schema_mapping
815                .field_to_property
816                .get("name"),
817            Some(&"http://xmlns.com/foaf/0.1/name".to_string())
818        );
819    }
820
821    #[test]
822    fn test_map_field_to_property_explicit() {
823        let mut translator = GraphQLTranslator::new();
824        translator.add_field_mapping(
825            "email".to_string(),
826            "http://xmlns.com/foaf/0.1/mbox".to_string(),
827        );
828
829        let property = translator.map_field_to_property("email").unwrap();
830        assert_eq!(property, "http://xmlns.com/foaf/0.1/mbox");
831    }
832
833    #[test]
834    fn test_map_field_to_property_default() {
835        let translator = GraphQLTranslator::new();
836        let property = translator.map_field_to_property("unknownField").unwrap();
837        assert_eq!(property, "http://example.org/property/unknownField");
838    }
839
840    #[test]
841    fn test_translation_context_variable_generation() {
842        let mut context = TranslationContext::new(HashMap::new(), HashMap::new());
843        let var1 = context.next_var("test");
844        let var2 = context.next_var("test");
845
846        assert_ne!(var1.name(), var2.name());
847        assert_eq!(context.sparql_variables.len(), 2);
848    }
849
850    #[test]
851    fn test_simple_query_translation() {
852        let mut translator = GraphQLTranslator::new();
853
854        let operation = GraphQLOperation {
855            operation_type: GraphQLOperationType::Query,
856            name: Some("GetUsers".to_string()),
857            variables: HashMap::new(),
858            directives: vec![],
859            selection_set: vec![GraphQLSelection::Field(GraphQLField {
860                name: "users".to_string(),
861                alias: None,
862                arguments: HashMap::new(),
863                directives: vec![],
864                selection_set: vec![
865                    GraphQLSelection::Field(GraphQLField {
866                        name: "id".to_string(),
867                        alias: None,
868                        arguments: HashMap::new(),
869                        directives: vec![],
870                        selection_set: vec![],
871                    }),
872                    GraphQLSelection::Field(GraphQLField {
873                        name: "name".to_string(),
874                        alias: None,
875                        arguments: HashMap::new(),
876                        directives: vec![],
877                        selection_set: vec![],
878                    }),
879                ],
880            })],
881        };
882
883        let fragments = HashMap::new();
884        let result = translator.translate_operation(&operation, &fragments);
885        assert!(result.is_ok());
886        assert_eq!(translator.stats.queries_translated, 1);
887        assert_eq!(translator.stats.fields_translated, 3); // users, id, name
888    }
889
890    #[test]
891    fn test_skip_directive() {
892        let mut translator = GraphQLTranslator::new();
893        let mut context = TranslationContext::new(HashMap::new(), HashMap::new());
894
895        let mut skip_args = HashMap::new();
896        skip_args.insert("if".to_string(), GraphQLValue::Boolean(true));
897
898        let directives = vec![GraphQLDirective {
899            name: "skip".to_string(),
900            arguments: skip_args,
901        }];
902
903        let should_skip = translator
904            .should_skip_by_directives(&directives, &mut context)
905            .unwrap();
906        assert!(should_skip);
907    }
908
909    #[test]
910    fn test_include_directive() {
911        let mut translator = GraphQLTranslator::new();
912        let mut context = TranslationContext::new(HashMap::new(), HashMap::new());
913
914        let mut include_args = HashMap::new();
915        include_args.insert("if".to_string(), GraphQLValue::Boolean(false));
916
917        let directives = vec![GraphQLDirective {
918            name: "include".to_string(),
919            arguments: include_args,
920        }];
921
922        let should_skip = translator
923            .should_skip_by_directives(&directives, &mut context)
924            .unwrap();
925        assert!(should_skip);
926    }
927
928    #[test]
929    fn test_unsupported_subscription() {
930        let mut translator = GraphQLTranslator::new();
931
932        let operation = GraphQLOperation {
933            operation_type: GraphQLOperationType::Subscription,
934            name: Some("OnUserCreated".to_string()),
935            variables: HashMap::new(),
936            directives: vec![],
937            selection_set: vec![],
938        };
939
940        let fragments = HashMap::new();
941        let result = translator.translate_operation(&operation, &fragments);
942        assert!(result.is_err());
943        assert!(matches!(
944            result.unwrap_err(),
945            TranslationError::UnsupportedOperation(_)
946        ));
947    }
948
949    #[test]
950    fn test_stats_tracking() {
951        let mut translator = GraphQLTranslator::new();
952
953        // Simulate some translations
954        translator.stats.queries_translated = 5;
955        translator.stats.mutations_translated = 3;
956        translator.stats.fields_translated = 42;
957
958        let stats = translator.get_stats();
959        assert_eq!(stats.queries_translated, 5);
960        assert_eq!(stats.mutations_translated, 3);
961        assert_eq!(stats.fields_translated, 42);
962
963        translator.reset_stats();
964        assert_eq!(translator.stats.queries_translated, 0);
965    }
966}