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 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 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, };
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}