use crate::error::Result;
use crate::model::Project;
use std::collections::HashSet;
pub struct Validator {
strict: bool,
}
impl Validator {
pub fn new() -> Self {
Self { strict: false }
}
pub fn strict() -> Self {
Self { strict: true }
}
pub fn validate_project(&self, project: &Project) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
self.validate_metadata(project, &mut report);
self.validate_schemas(project, &mut report);
self.validate_queries(project, &mut report);
self.validate_endpoints(project, &mut report);
self.validate_tests(project, &mut report);
self.check_duplicates(project, &mut report);
self.check_references(project, &mut report);
if self.strict && !report.warnings.is_empty() {
for warning in &report.warnings {
report.errors.push(warning.clone());
}
report.warnings.clear();
}
Ok(report)
}
fn validate_metadata(&self, project: &Project, report: &mut ValidationReport) {
if project.metadata.name.is_empty() {
report.add_error("Project name cannot be empty");
}
if project.metadata.version.is_empty() {
report.add_warning("Project version is empty");
}
}
fn validate_schemas(&self, project: &Project, report: &mut ValidationReport) {
for (name, schema) in &project.schemas {
if schema.fields.is_empty() {
report.add_warning(&format!("Schema '{}' has no fields", name));
}
for field in &schema.fields {
if let Err(e) = field.validate() {
report.add_error(&format!(
"Field '{}' in schema '{}': {}",
field.name, name, e
));
}
}
}
}
fn validate_queries(&self, project: &Project, report: &mut ValidationReport) {
for (name, query) in &project.queries {
if query.source.is_empty() {
report.add_error(&format!("Query '{}' has empty source", name));
}
let mut param_names = HashSet::new();
for param in &query.params {
if !param_names.insert(¶m.name) {
report.add_error(&format!(
"Duplicate parameter '{}' in query '{}'",
param.name, name
));
}
}
}
}
fn validate_endpoints(&self, project: &Project, report: &mut ValidationReport) {
for endpoint in &project.endpoints {
if endpoint.path.is_empty() {
report.add_error("Endpoint has empty path");
}
if endpoint.method.is_empty() {
report.add_error(&format!("Endpoint '{}' has empty method", endpoint.path));
}
let valid_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
if !valid_methods.contains(&endpoint.method.as_str()) {
report.add_warning(&format!(
"Endpoint '{}' has unusual method '{}'",
endpoint.path, endpoint.method
));
}
}
}
fn validate_tests(&self, project: &Project, report: &mut ValidationReport) {
for test in &project.tests {
if test.steps.is_empty() {
report.add_warning(&format!("Test '{}' has no steps", test.name));
}
}
}
fn check_duplicates(&self, project: &Project, report: &mut ValidationReport) {
let schema_count = project.schemas.len();
let unique_schemas: HashSet<_> = project.schemas.keys().collect();
if schema_count != unique_schemas.len() {
report.add_error("Duplicate schema names detected");
}
let query_count = project.queries.len();
let unique_queries: HashSet<_> = project.queries.keys().collect();
if query_count != unique_queries.len() {
report.add_error("Duplicate query names detected");
}
let mut endpoint_paths = HashSet::new();
for endpoint in &project.endpoints {
let key = format!("{} {}", endpoint.method, endpoint.path);
if !endpoint_paths.insert(key.clone()) {
report.add_error(&format!("Duplicate endpoint: {}", key));
}
}
}
fn check_references(&self, project: &Project, report: &mut ValidationReport) {
for (name, query) in &project.queries {
let schema_names = self.extract_schema_names(&query.return_type);
for schema_name in schema_names {
if !is_primitive_type(&schema_name) && !project.schemas.contains_key(&schema_name) {
report.add_error(&format!(
"Query '{}' return type references undefined schema '{}'",
name, schema_name
));
}
}
}
for endpoint in &project.endpoints {
if let Some(query_name) = &endpoint.query {
if !project.queries.contains_key(query_name) {
report.add_error(&format!(
"Endpoint '{}' references undefined query '{}'",
endpoint.path, query_name
));
}
}
}
}
fn extract_schema_names(&self, typ: &crate::schema::Type) -> Vec<String> {
use crate::schema::Type;
match typ {
Type::Named(name) | Type::Custom(name) => vec![name.clone()],
Type::Option(inner) | Type::Vec(inner) => self.extract_schema_names(inner),
_ => vec![],
}
}
}
impl Default for Validator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ValidationReport {
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationReport {
pub fn new() -> Self {
Self {
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn add_error(&mut self, message: &str) {
self.errors.push(message.to_string());
}
pub fn add_warning(&mut self, message: &str) {
self.warnings.push(message.to_string());
}
pub fn total_issues(&self) -> usize {
self.errors.len() + self.warnings.len()
}
}
impl Default for ValidationReport {
fn default() -> Self {
Self::new()
}
}
fn is_primitive_type(type_name: &str) -> bool {
matches!(
type_name,
"String"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "f32"
| "f64"
| "bool"
| "char"
| "EntityId"
| "Timestamp"
| "Integer"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{DatabaseConfig, ProjectMetadata};
#[test]
fn test_validator_creation() {
let validator = Validator::new();
assert!(!validator.strict);
let strict_validator = Validator::strict();
assert!(strict_validator.strict);
}
#[test]
fn test_validation_report() {
let mut report = ValidationReport::new();
assert!(report.is_valid());
assert_eq!(report.total_issues(), 0);
report.add_error("test error");
assert!(!report.is_valid());
assert_eq!(report.errors.len(), 1);
report.add_warning("test warning");
assert_eq!(report.warnings.len(), 1);
assert_eq!(report.total_issues(), 2);
}
#[test]
fn test_empty_project_name() {
let validator = Validator::new();
let project = Project {
metadata: ProjectMetadata {
name: String::new(),
version: "0.1.0".to_string(),
authors: Vec::new(),
description: None,
},
database: DatabaseConfig::default(),
server: None,
scaffold: None,
schemas: Default::default(),
queries: Default::default(),
endpoints: Vec::new(),
tests: Vec::new(),
docs: None,
deployment: None,
dependencies: crate::model::dependency::DependencyManager::new(),
};
let report = validator.validate_project(&project).unwrap();
assert!(!report.is_valid());
assert!(
report
.errors
.iter()
.any(|e| e.contains("name cannot be empty"))
);
}
#[test]
fn test_is_primitive_type() {
assert!(is_primitive_type("String"));
assert!(is_primitive_type("i64"));
assert!(is_primitive_type("bool"));
assert!(is_primitive_type("EntityId"));
assert!(!is_primitive_type("User"));
assert!(!is_primitive_type("CustomType"));
}
}