protograph_core/
validation.rs1use crate::ast::*;
2use thiserror::Error;
3
4#[derive(Error, Debug, Clone)]
5pub enum ValidationError {
6 #[error("Field '{field}' on type '{parent_type}' references '{target_type}' which is not an @entity. Add @entity directive to '{target_type}'.")]
7 NonEntityRelationship {
8 parent_type: String,
9 field: String,
10 target_type: String,
11 },
12
13 #[error("Field '{field}' on type '{parent_type}' references unknown type '{target_type}'.")]
14 UnknownType {
15 parent_type: String,
16 field: String,
17 target_type: String,
18 },
19
20 #[error("@{directive} on '{parent_type}::{field}' references foreign key '{foreign_key}' which doesn't exist on '{check_type}'.")]
21 MissingForeignKey {
22 parent_type: String,
23 field: String,
24 directive: String,
25 foreign_key: String,
26 check_type: String,
27 },
28
29 #[error("@manyToMany on '{parent_type}::{field}' references junction table '{junction_table}' which doesn't exist.")]
30 MissingJunctionTable {
31 parent_type: String,
32 field: String,
33 junction_table: String,
34 },
35
36 #[error("Junction table '{junction_table}' must have @entity directive.")]
37 JunctionTableNotEntity { junction_table: String },
38}
39
40pub fn validate_schema(schema: &ProtographSchema) -> Result<(), Vec<ValidationError>> {
41 let mut errors = Vec::new();
42
43 for (type_name, entity) in &schema.types {
44 for field in &entity.fields {
45 if let Some(relationship) = &field.relationship {
46 validate_relationship(field, entity, relationship, schema, &mut errors);
47 }
48 }
49 }
50
51 validate_junction_tables(schema, &mut errors);
52
53 if errors.is_empty() {
54 Ok(())
55 } else {
56 Err(errors)
57 }
58}
59
60fn validate_relationship(
61 field: &Field,
62 parent: &EntityType,
63 relationship: &Relationship,
64 schema: &ProtographSchema,
65 errors: &mut Vec<ValidationError>,
66) {
67 let target_type_name = field.field_type.base_type();
68
69 let target_entity = match schema.types.get(target_type_name) {
70 Some(e) => e,
71 None => {
72 errors.push(ValidationError::UnknownType {
73 parent_type: parent.name.clone(),
74 field: field.name.clone(),
75 target_type: target_type_name.to_string(),
76 });
77 return;
78 }
79 };
80
81 if !target_entity.is_entity {
82 errors.push(ValidationError::NonEntityRelationship {
83 parent_type: parent.name.clone(),
84 field: field.name.clone(),
85 target_type: target_type_name.to_string(),
86 });
87 }
88
89 match relationship {
90 Relationship::BelongsTo { foreign_key } => {
91 if !parent.fields.iter().any(|f| &f.name == foreign_key) {
92 errors.push(ValidationError::MissingForeignKey {
93 parent_type: parent.name.clone(),
94 field: field.name.clone(),
95 directive: "belongsTo".to_string(),
96 foreign_key: foreign_key.clone(),
97 check_type: parent.name.clone(),
98 });
99 }
100 }
101 Relationship::HasMany { foreign_key } => {
102 if !target_entity.fields.iter().any(|f| &f.name == foreign_key) {
103 errors.push(ValidationError::MissingForeignKey {
104 parent_type: parent.name.clone(),
105 field: field.name.clone(),
106 directive: "hasMany".to_string(),
107 foreign_key: foreign_key.clone(),
108 check_type: target_type_name.to_string(),
109 });
110 }
111 }
112 Relationship::ManyToMany {
113 junction_table,
114 foreign_key,
115 } => {
116 if !schema.types.contains_key(junction_table) {
117 errors.push(ValidationError::MissingJunctionTable {
118 parent_type: parent.name.clone(),
119 field: field.name.clone(),
120 junction_table: junction_table.clone(),
121 });
122 }
123 }
124 }
125}
126
127fn validate_junction_tables(schema: &ProtographSchema, errors: &mut Vec<ValidationError>) {
128 for (_, entity) in &schema.types {
129 for field in &entity.fields {
130 if let Some(Relationship::ManyToMany { junction_table, .. }) = &field.relationship {
131 if let Some(junction) = schema.types.get(junction_table) {
132 if !junction.is_entity {
133 errors.push(ValidationError::JunctionTableNotEntity {
134 junction_table: junction_table.clone(),
135 });
136 }
137 }
138 }
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::parser::parse_schema_file;
147
148 #[test]
149 fn test_valid_schema() {
150 let schema = r#"
151 type User @entity {
152 id: ID!
153 name: String!
154 posts: [Post!]! @hasMany(field: "authorId")
155 }
156
157 type Post @entity {
158 id: ID!
159 title: String!
160 author: User! @belongsTo(field: "authorId")
161 authorId: ID!
162 }
163 "#;
164
165 let parsed = parse_schema_file(schema).unwrap();
166 let result = validate_schema(&parsed);
167 assert!(result.is_ok());
168 }
169
170 #[test]
171 fn test_non_entity_relationship() {
172 let schema = r#"
173 type User {
174 id: ID!
175 name: String!
176 }
177
178 type Post @entity {
179 id: ID!
180 author: User! @belongsTo(field: "authorId")
181 authorId: ID!
182 }
183 "#;
184
185 let parsed = parse_schema_file(schema).unwrap();
186 let result = validate_schema(&parsed);
187 assert!(result.is_err());
188
189 let errors = result.unwrap_err();
190 assert!(errors.iter().any(|e| matches!(e, ValidationError::NonEntityRelationship { .. })));
191 }
192
193 #[test]
194 fn test_missing_foreign_key() {
195 let schema = r#"
196 type User @entity {
197 id: ID!
198 name: String!
199 }
200
201 type Post @entity {
202 id: ID!
203 author: User! @belongsTo(field: "userId")
204 }
205 "#;
206
207 let parsed = parse_schema_file(schema).unwrap();
208 let result = validate_schema(&parsed);
209 assert!(result.is_err());
210
211 let errors = result.unwrap_err();
212 assert!(errors.iter().any(|e| matches!(e, ValidationError::MissingForeignKey { .. })));
213 }
214}