use crate::ast::*;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum ValidationError {
#[error("Field '{field}' on type '{parent_type}' references '{target_type}' which is not an @entity. Add @entity directive to '{target_type}'.")]
NonEntityRelationship {
parent_type: String,
field: String,
target_type: String,
},
#[error("Field '{field}' on type '{parent_type}' references unknown type '{target_type}'.")]
UnknownType {
parent_type: String,
field: String,
target_type: String,
},
#[error("@{directive} on '{parent_type}::{field}' references foreign key '{foreign_key}' which doesn't exist on '{check_type}'.")]
MissingForeignKey {
parent_type: String,
field: String,
directive: String,
foreign_key: String,
check_type: String,
},
#[error("@manyToMany on '{parent_type}::{field}' references junction table '{junction_table}' which doesn't exist.")]
MissingJunctionTable {
parent_type: String,
field: String,
junction_table: String,
},
#[error("Junction table '{junction_table}' must have @entity directive.")]
JunctionTableNotEntity { junction_table: String },
}
pub fn validate_schema(schema: &ProtographSchema) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
for (type_name, entity) in &schema.types {
for field in &entity.fields {
if let Some(relationship) = &field.relationship {
validate_relationship(field, entity, relationship, schema, &mut errors);
}
}
}
validate_junction_tables(schema, &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_relationship(
field: &Field,
parent: &EntityType,
relationship: &Relationship,
schema: &ProtographSchema,
errors: &mut Vec<ValidationError>,
) {
let target_type_name = field.field_type.base_type();
let target_entity = match schema.types.get(target_type_name) {
Some(e) => e,
None => {
errors.push(ValidationError::UnknownType {
parent_type: parent.name.clone(),
field: field.name.clone(),
target_type: target_type_name.to_string(),
});
return;
}
};
if !target_entity.is_entity {
errors.push(ValidationError::NonEntityRelationship {
parent_type: parent.name.clone(),
field: field.name.clone(),
target_type: target_type_name.to_string(),
});
}
match relationship {
Relationship::BelongsTo { foreign_key } => {
if !parent.fields.iter().any(|f| &f.name == foreign_key) {
errors.push(ValidationError::MissingForeignKey {
parent_type: parent.name.clone(),
field: field.name.clone(),
directive: "belongsTo".to_string(),
foreign_key: foreign_key.clone(),
check_type: parent.name.clone(),
});
}
}
Relationship::HasMany { foreign_key } => {
if !target_entity.fields.iter().any(|f| &f.name == foreign_key) {
errors.push(ValidationError::MissingForeignKey {
parent_type: parent.name.clone(),
field: field.name.clone(),
directive: "hasMany".to_string(),
foreign_key: foreign_key.clone(),
check_type: target_type_name.to_string(),
});
}
}
Relationship::ManyToMany {
junction_table,
foreign_key,
} => {
if !schema.types.contains_key(junction_table) {
errors.push(ValidationError::MissingJunctionTable {
parent_type: parent.name.clone(),
field: field.name.clone(),
junction_table: junction_table.clone(),
});
}
}
}
}
fn validate_junction_tables(schema: &ProtographSchema, errors: &mut Vec<ValidationError>) {
for (_, entity) in &schema.types {
for field in &entity.fields {
if let Some(Relationship::ManyToMany { junction_table, .. }) = &field.relationship {
if let Some(junction) = schema.types.get(junction_table) {
if !junction.is_entity {
errors.push(ValidationError::JunctionTableNotEntity {
junction_table: junction_table.clone(),
});
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_schema_file;
#[test]
fn test_valid_schema() {
let schema = r#"
type User @entity {
id: ID!
name: String!
posts: [Post!]! @hasMany(field: "authorId")
}
type Post @entity {
id: ID!
title: String!
author: User! @belongsTo(field: "authorId")
authorId: ID!
}
"#;
let parsed = parse_schema_file(schema).unwrap();
let result = validate_schema(&parsed);
assert!(result.is_ok());
}
#[test]
fn test_non_entity_relationship() {
let schema = r#"
type User {
id: ID!
name: String!
}
type Post @entity {
id: ID!
author: User! @belongsTo(field: "authorId")
authorId: ID!
}
"#;
let parsed = parse_schema_file(schema).unwrap();
let result = validate_schema(&parsed);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| matches!(e, ValidationError::NonEntityRelationship { .. })));
}
#[test]
fn test_missing_foreign_key() {
let schema = r#"
type User @entity {
id: ID!
name: String!
}
type Post @entity {
id: ID!
author: User! @belongsTo(field: "userId")
}
"#;
let parsed = parse_schema_file(schema).unwrap();
let result = validate_schema(&parsed);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| matches!(e, ValidationError::MissingForeignKey { .. })));
}
}