Skip to main content

fraiseql_core/federation/
selection_parser.rs

1//! GraphQL field selection parsing for federation queries.
2//!
3//! Extracts which fields are requested in a GraphQL query so federation
4//! can project only necessary fields from the database.
5
6use crate::error::Result;
7
8/// Represents requested fields in a GraphQL selection set.
9#[derive(Debug, Clone, Default)]
10pub struct FieldSelection {
11    /// Names of fields requested in the query
12    pub fields: Vec<String>,
13}
14
15impl FieldSelection {
16    /// Create a new field selection.
17    #[must_use]
18    pub fn new(fields: Vec<String>) -> Self {
19        Self { fields }
20    }
21
22    /// Check if a field is selected.
23    #[must_use]
24    pub fn contains(&self, field: &str) -> bool {
25        self.fields.contains(&field.to_string())
26    }
27
28    /// Add a field to the selection.
29    pub fn add_field(&mut self, field: String) {
30        if !self.fields.contains(&field) {
31            self.fields.push(field);
32        }
33    }
34}
35
36/// Parse GraphQL query to extract field selection for _entities.
37///
38/// Example query:
39/// ```graphql
40/// query {
41///   _entities(representations: [...]) {
42///     __typename
43///     id
44///     name
45///     email
46///   }
47/// }
48/// ```
49///
50/// Returns: FieldSelection { fields: ["__typename", "id", "name", "email"] }
51///
52/// # Errors
53///
54/// Returns error if query parsing fails.
55pub fn parse_field_selection(query: &str) -> Result<FieldSelection> {
56    let trimmed = query.trim();
57
58    // Extract fields between outermost { } for _entities
59    let fields = extract_fields_from_selection_set(trimmed)?;
60
61    Ok(FieldSelection::new(fields))
62}
63
64/// Extract field names from a GraphQL selection set.
65fn extract_fields_from_selection_set(query: &str) -> Result<Vec<String>> {
66    let mut fields = Vec::new();
67
68    // Simple regex-like extraction: look for patterns like "fieldName" within selection sets
69    // This is a simplified implementation that handles common cases
70    let mut in_selection = false;
71    let mut current_field = String::new();
72    let mut depth = 0;
73
74    for ch in query.chars() {
75        match ch {
76            '{' => {
77                depth += 1;
78                if depth == 2 {
79                    // Entering selection set for _entities
80                    in_selection = true;
81                }
82            },
83            '}' => {
84                depth -= 1;
85                if in_selection && !current_field.is_empty() {
86                    fields.push(current_field.trim().to_string());
87                    current_field.clear();
88                }
89                if depth == 1 {
90                    in_selection = false;
91                }
92            },
93            ' ' | '\n' | '\r' | '\t' if in_selection => {
94                // Whitespace is a field separator
95                if !current_field.is_empty() {
96                    fields.push(current_field.trim().to_string());
97                    current_field.clear();
98                }
99            },
100            _ if in_selection => {
101                current_field.push(ch);
102            },
103            _ => {},
104        }
105    }
106
107    // Filter out empty and invalid field names
108    let fields: Vec<String> = fields
109        .into_iter()
110        .filter(|f| !f.is_empty() && !f.contains('(') && !f.contains(':'))
111        .collect();
112
113    Ok(fields)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_parse_simple_field_selection() {
122        let query = r"
123            query {
124                _entities(representations: [...]) {
125                    __typename
126                    id
127                    name
128                }
129            }
130        ";
131
132        let selection = parse_field_selection(query).unwrap();
133        assert!(selection.contains("__typename"));
134        assert!(selection.contains("id"));
135        assert!(selection.contains("name"));
136    }
137
138    #[test]
139    fn test_field_selection_without_whitespace() {
140        let query = "{ _entities(representations: [...]) { id name email } }";
141
142        let selection = parse_field_selection(query).unwrap();
143        assert!(selection.contains("id"));
144        assert!(selection.contains("name"));
145        assert!(selection.contains("email"));
146    }
147
148    #[test]
149    fn test_field_selection_contains() {
150        let mut selection = FieldSelection::new(vec!["id".to_string(), "name".to_string()]);
151        assert!(selection.contains("id"));
152        assert!(!selection.contains("email"));
153
154        selection.add_field("email".to_string());
155        assert!(selection.contains("email"));
156    }
157
158    #[test]
159    fn test_field_selection_excludes_invalid_patterns() {
160        let query = r#"
161            query {
162                _entities(representations: [...]) {
163                    id
164                    user(id: "123") @include(if: true)
165                    name
166                }
167            }
168        "#;
169
170        let selection = parse_field_selection(query).unwrap();
171        assert!(selection.contains("id"));
172        assert!(selection.contains("name"));
173        // Should not include the function call or directive
174    }
175}