use std::path::Path;
use terraphim_config::{Haystack, KnowledgeGraph, Role, ServiceType};
use thiserror::Error;
#[derive(Debug, Error, Clone)]
pub enum ValidationError {
#[error("Field '{0}' cannot be empty")]
EmptyField(String),
#[error("Role must have at least one haystack")]
MissingHaystack,
#[error("Invalid haystack location: {0}")]
InvalidLocation(String),
#[error("Service {0} requires: {1}")]
ServiceRequirement(String, String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Invalid knowledge graph: {0}")]
InvalidKnowledgeGraph(String),
}
pub fn validate_role(role: &Role) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if role.name.to_string().trim().is_empty() {
errors.push(ValidationError::EmptyField("name".into()));
}
if role.haystacks.is_empty() {
errors.push(ValidationError::MissingHaystack);
}
for haystack in &role.haystacks {
if let Err(e) = validate_haystack(haystack) {
errors.push(e);
}
}
if let Some(ref kg) = role.kg {
if let Err(e) = validate_knowledge_graph(kg) {
errors.push(e);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_haystack(haystack: &Haystack) -> Result<(), ValidationError> {
if haystack.location.trim().is_empty() {
return Err(ValidationError::EmptyField("location".into()));
}
match haystack.service {
ServiceType::Ripgrep => {
if haystack.location.starts_with("http://") || haystack.location.starts_with("https://")
{
return Err(ValidationError::InvalidLocation(
"Ripgrep requires a local path, not a URL".into(),
));
}
}
ServiceType::QueryRs => {
}
ServiceType::Quickwit => {
if !haystack.location.starts_with("http://")
&& !haystack.location.starts_with("https://")
{
return Err(ValidationError::ServiceRequirement(
"Quickwit".into(),
"URL (http:// or https://)".into(),
));
}
}
ServiceType::Atomic => {
if !haystack.location.starts_with("http://")
&& !haystack.location.starts_with("https://")
{
return Err(ValidationError::ServiceRequirement(
"Atomic".into(),
"URL (http:// or https://)".into(),
));
}
}
_ => {
}
}
Ok(())
}
pub fn validate_knowledge_graph(kg: &KnowledgeGraph) -> Result<(), ValidationError> {
let has_remote = kg.automata_path.is_some();
let has_local = kg.knowledge_graph_local.is_some();
if !has_remote && !has_local {
return Err(ValidationError::InvalidKnowledgeGraph(
"Must specify either remote automata URL or local knowledge graph path".into(),
));
}
if let Some(ref local) = kg.knowledge_graph_local {
if local.path.as_os_str().is_empty() {
return Err(ValidationError::InvalidKnowledgeGraph(
"Local knowledge graph path cannot be empty".into(),
));
}
}
Ok(())
}
pub fn path_exists(path: &str) -> bool {
let expanded = expand_tilde(path);
Path::new(&expanded).exists()
}
pub fn expand_tilde(path: &str) -> String {
if path.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
return path.replacen("~", home.to_string_lossy().as_ref(), 1);
}
} else if path == "~" {
if let Some(home) = dirs::home_dir() {
return home.to_string_lossy().to_string();
}
}
path.to_string()
}
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ValidationError::InvalidUrl(format!(
"URL must start with http:// or https://: {}",
url
)));
}
if url.len() < 10 {
return Err(ValidationError::InvalidUrl(format!(
"URL is too short: {}",
url
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use terraphim_types::RoleName;
fn create_test_role(name: &str) -> Role {
let mut role = Role::new(name);
role.haystacks = vec![Haystack {
location: "/some/path".to_string(),
service: ServiceType::Ripgrep,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
}];
role
}
#[test]
fn test_validate_role_valid() {
let role = create_test_role("Test Role");
assert!(validate_role(&role).is_ok());
}
#[test]
fn test_validate_role_empty_name() {
let mut role = create_test_role("");
role.name = RoleName::new(" ");
let result = validate_role(&role);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| matches!(e, ValidationError::EmptyField(_)))
);
}
#[test]
fn test_validate_role_missing_haystack() {
let mut role = create_test_role("Test Role");
role.haystacks.clear();
let result = validate_role(&role);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| matches!(e, ValidationError::MissingHaystack))
);
}
#[test]
fn test_validate_haystack_valid_ripgrep() {
let haystack = Haystack {
location: "/some/path".to_string(),
service: ServiceType::Ripgrep,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
};
assert!(validate_haystack(&haystack).is_ok());
}
#[test]
fn test_validate_haystack_ripgrep_rejects_url() {
let haystack = Haystack {
location: "https://example.com".to_string(),
service: ServiceType::Ripgrep,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
};
let result = validate_haystack(&haystack);
assert!(result.is_err());
}
#[test]
fn test_validate_haystack_quickwit_requires_url() {
let haystack = Haystack {
location: "/local/path".to_string(),
service: ServiceType::Quickwit,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
};
let result = validate_haystack(&haystack);
assert!(result.is_err());
let haystack_valid = Haystack {
location: "http://localhost:7280".to_string(),
service: ServiceType::Quickwit,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
};
assert!(validate_haystack(&haystack_valid).is_ok());
}
#[test]
fn test_validate_haystack_empty_location() {
let haystack = Haystack {
location: "".to_string(),
service: ServiceType::Ripgrep,
read_only: true,
fetch_content: false,
atomic_server_secret: None,
extra_parameters: Default::default(),
};
let result = validate_haystack(&haystack);
assert!(result.is_err());
}
#[test]
fn test_expand_tilde() {
let expanded = expand_tilde("~/Documents");
assert!(!expanded.starts_with("~") || dirs::home_dir().is_none());
}
#[test]
fn test_validate_url_valid() {
assert!(validate_url("https://example.com/api").is_ok());
assert!(validate_url("http://localhost:8080").is_ok());
}
#[test]
fn test_validate_url_invalid() {
assert!(validate_url("not-a-url").is_err());
assert!(validate_url("ftp://example.com").is_err());
assert!(validate_url("http://").is_err());
}
}