Skip to main content

mongo_graphql/schema/
parser.rs

1use crate::error::GraphQLError;
2use crate::schema::definition::{FieldType, RelationKind, SchemaDefinition};
3use std::collections::{HashMap, HashSet};
4
5pub struct SchemaParser;
6
7impl SchemaParser {
8    pub fn from_str(json: &str) -> Result<SchemaDefinition, GraphQLError> {
9        let schema: SchemaDefinition =
10            serde_json::from_str(json).map_err(|e| GraphQLError::SchemaParse {
11                message: format!("Invalid JSON: {}", e),
12                location: "root".into(),
13            })?;
14
15        Self::validate_referenced_collections(&schema)?;
16        Self::validate_fields(&schema)?;
17        Self::validate_enum_names(&schema)?;
18        Self::validate_many_to_many(&schema)?;
19        Self::validate_relation_fields(&schema)?;
20
21        Ok(schema)
22    }
23
24    fn validate_referenced_collections(schema: &SchemaDefinition) -> Result<(), GraphQLError> {
25        let names: std::collections::HashSet<&str> =
26            schema.collections.iter().map(|coll_def| coll_def.collection.as_str()).collect();
27
28        for coll in &schema.collections {
29            for field in &coll.fields {
30                if let FieldType::Relation(rel) = &field.field_type {
31                    if !names.contains(rel.collection.as_str()) {
32                        return Err(GraphQLError::SchemaParse {
33                            message: format!(
34                                "Collection '{}' references unknown collection '{}' in field '{}'",
35                                coll.collection, rel.collection, field.name
36                            ),
37                            location: format!(
38                                "$.collections.{}.fields.{}",
39                                coll.collection, field.name
40                            ),
41                        });
42                    }
43
44                }
45            }
46        }
47
48        Ok(())
49    }
50
51    fn validate_fields(schema: &SchemaDefinition) -> Result<(), GraphQLError> {
52        for coll in &schema.collections {
53            // Check for duplicate field names within the collection.
54            let mut field_names = std::collections::HashSet::new();
55            let mut gql_names = std::collections::HashSet::new();
56            for field in &coll.fields {
57                if !field_names.insert(&field.name) {
58                    return Err(GraphQLError::SchemaParse {
59                        message: format!(
60                            "Duplicate field name '{}' in collection '{}'",
61                            field.name, coll.collection
62                        ),
63                        location: format!(
64                            "$.collections.{}.fields.{}",
65                            coll.collection, field.name
66                        ),
67                    });
68                }
69                let gql_name = field.graphql_name();
70                if !gql_names.insert(gql_name.clone()) {
71                    return Err(GraphQLError::SchemaParse {
72                        message: format!(
73                            "Duplicate GraphQL field name '{}' in collection '{}'",
74                            gql_name, coll.collection
75                        ),
76                        location: format!(
77                            "$.collections.{}.fields.{}",
78                            coll.collection, field.name
79                        ),
80                    });
81                }
82            }
83
84            for field in &coll.fields {
85                // V1: only a single level of nesting is supported for lists.
86                if let FieldType::List(inner) = &field.field_type {
87                    if matches!(**inner, FieldType::List(_)) {
88                        return Err(GraphQLError::SchemaParse {
89                            message: format!(
90                                "Nested lists are not supported in {}.{}",
91                                coll.collection, field.name
92                            ),
93                            location: format!(
94                                "$.collections.{}.fields.{}.type",
95                                coll.collection, field.name
96                            ),
97                        });
98                    }
99                }
100
101                if let Some(enum_def) = &field.r#enum {
102                    if enum_def.values.is_empty() {
103                        return Err(GraphQLError::SchemaParse {
104                            message: format!(
105                                "Enum '{}' in {}.{} has no values",
106                                enum_def.name, coll.collection, field.name
107                            ),
108                            location: format!(
109                                "$.collections.{}.fields.{}.enum.values",
110                                coll.collection, field.name
111                            ),
112                        });
113                    }
114
115                    let mut seen = std::collections::HashSet::new();
116                    for val in &enum_def.values {
117                        if !seen.insert(val) {
118                            return Err(GraphQLError::SchemaParse {
119                                message: format!(
120                                    "Enum '{}' has duplicate value '{}' in {}.{}",
121                                    enum_def.name, val, coll.collection, field.name
122                                ),
123                                location: format!(
124                                    "$.collections.{}.fields.{}.enum.values",
125                                    coll.collection, field.name
126                                ),
127                            });
128                        }
129                        if !is_valid_graphql_identifier(val) {
130                            return Err(GraphQLError::SchemaParse {
131                                message: format!(
132                                    "Enum '{}' has invalid GraphQL identifier '{}' in {}.{}",
133                                    enum_def.name, val, coll.collection, field.name
134                                ),
135                                location: format!(
136                                    "$.collections.{}.fields.{}.enum.values",
137                                    coll.collection, field.name
138                                ),
139                            });
140                        }
141                    }
142                }
143            }
144        }
145
146        Ok(())
147    }
148
149    fn validate_enum_names(schema: &SchemaDefinition) -> Result<(), GraphQLError> {
150        let type_names: std::collections::HashSet<String> = schema
151            .collections
152            .iter()
153            .map(|coll| coll.type_name())
154            .collect();
155
156        for coll_def in &schema.collections {
157            for field_def in &coll_def.fields {
158                if let Some(enum_def) = &field_def.r#enum {
159                    if type_names.contains(&enum_def.name) {
160                        return Err(GraphQLError::SchemaParse {
161                            message: format!(
162                                "Enum '{}' in {}.{} clashes with an auto-generated type name",
163                                enum_def.name, coll_def.collection, field_def.name
164                            ),
165                            location: format!(
166                                "$.collections.{}.fields.{}.enum.name",
167                                coll_def.collection, field_def.name
168                            ),
169                        });
170                    }
171                }
172            }
173        }
174
175        Ok(())
176    }
177
178    fn validate_many_to_many(schema: &SchemaDefinition) -> Result<(), GraphQLError> {
179        let mut junctions: HashMap<&str, Vec<&str>> = HashMap::new();
180
181        for coll in &schema.collections {
182            for field in &coll.fields {
183                if let FieldType::Relation(rel) = &field.field_type {
184                    if matches!(rel.kind, RelationKind::ManyToMany) {
185                        let junction = rel
186                            .junction
187                            .as_ref()
188                            .map(|j| j.collection.as_str())
189                            .unwrap_or("unknown");
190
191                        junctions
192                            .entry(junction)
193                            .or_default()
194                            .push(&coll.collection);
195                    }
196                }
197            }
198        }
199
200        for (junction, sides) in &junctions {
201            if sides.len() != 2 {
202                return Err(GraphQLError::SchemaParse {
203                    message: format!(
204                        "many_to_many junction '{}' has {} side(s) declared. \
205                         Exactly 2 required (one per participating collection). \
206                         Declared by: [{}]",
207                        junction,
208                        sides.len(),
209                        sides.join(", ")
210                    ),
211                    location: format!("junction: {}", junction),
212                });
213            }
214        }
215
216        Ok(())
217    }
218
219    fn validate_relation_fields(schema: &SchemaDefinition) -> Result<(), GraphQLError> {
220        for coll in &schema.collections {
221            for field in &coll.fields {
222                let rel = match &field.field_type {
223                    FieldType::Relation(rel) => rel,
224                    _ => continue,
225                };
226
227                let target = match schema.collection_by_name(&rel.collection) {
228                    Some(c) => c,
229                    None => continue, // already caught by validate_referenced_collections
230                };
231
232                let mut target_field_names: HashSet<&str> = target
233                    .fields
234                    .iter()
235                    .map(|f| f.name.as_str())
236                    .collect();
237                for f in &target.fields {
238                    if let Some(gql) = &f.graphql_name {
239                        target_field_names.insert(gql.as_str());
240                    }
241                }
242
243                if !target_field_names.contains(rel.reference_field.as_str()) {
244                    return Err(GraphQLError::SchemaParse {
245                        message: format!(
246                            "reference_field '{}' not found in collection '{}' (relation from {}.{})",
247                            rel.reference_field, rel.collection, coll.collection, field.name
248                        ),
249                        location: format!(
250                            "$.collections.{}.fields.{}",
251                            coll.collection, field.name
252                        ),
253                    });
254                }
255
256                if let Some(junction) = &rel.junction {
257                    let jct_coll = match schema.collection_by_name(&junction.collection) {
258                        Some(c) => c,
259                        None => continue,
260                    };
261
262                    let mut jct_field_names: HashSet<&str> = jct_coll
263                        .fields
264                        .iter()
265                        .map(|f| f.name.as_str())
266                        .collect();
267                    for f in &jct_coll.fields {
268                        if let Some(gql) = &f.graphql_name {
269                            jct_field_names.insert(gql.as_str());
270                        }
271                    }
272
273                    for jct_field in [&junction.local_field, &junction.foreign_field] {
274                        if !jct_field_names.contains(jct_field.as_str()) {
275                            return Err(GraphQLError::SchemaParse {
276                                message: format!(
277                                    "junction field '{}' not found in '{}' (relation from {}.{})",
278                                    jct_field, junction.collection, coll.collection, field.name
279                                ),
280                                location: format!(
281                                    "$.collections.{}.fields.{}",
282                                    coll.collection, field.name
283                                ),
284                            });
285                        }
286                    }
287                }
288            }
289        }
290
291        Ok(())
292    }
293}
294
295fn is_valid_graphql_identifier(s: &str) -> bool {
296    let mut chars = s.chars();
297    match chars.next() {
298        Some(first_char) if first_char.is_ascii_alphabetic() || first_char == '_' => {
299            chars.all(|rest_char| rest_char.is_ascii_alphanumeric() || rest_char == '_')
300        }
301        _ => false,
302    }
303}