fraiseql_core/federation/
selection_parser.rs1use crate::error::Result;
7
8#[derive(Debug, Clone, Default)]
10pub struct FieldSelection {
11 pub fields: Vec<String>,
13}
14
15impl FieldSelection {
16 #[must_use]
18 pub fn new(fields: Vec<String>) -> Self {
19 Self { fields }
20 }
21
22 #[must_use]
24 pub fn contains(&self, field: &str) -> bool {
25 self.fields.contains(&field.to_string())
26 }
27
28 pub fn add_field(&mut self, field: String) {
30 if !self.fields.contains(&field) {
31 self.fields.push(field);
32 }
33 }
34}
35
36pub fn parse_field_selection(query: &str) -> Result<FieldSelection> {
56 let trimmed = query.trim();
57
58 let fields = extract_fields_from_selection_set(trimmed)?;
60
61 Ok(FieldSelection::new(fields))
62}
63
64fn extract_fields_from_selection_set(query: &str) -> Result<Vec<String>> {
66 let mut fields = Vec::new();
67
68 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 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 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 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 }
175}