use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::services::ConfigService;
pub fn validate_config(path: Option<&Path>) -> Result<PathBuf> {
if let Some(p) = path {
if is_model_catalog_file(p) {
mecha10_schema_registry::validate_model_catalog_file(p)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", p.display(), e))?;
return Ok(p.to_path_buf());
}
if is_behavior_config_file(p) {
let content = std::fs::read_to_string(p)
.map_err(|e| anyhow::anyhow!("Failed to read behavior config file {}: {}", p.display(), e))?;
let value: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from {}: {}", p.display(), e))?;
mecha10_behavior_runtime::config::validate_behavior_composition_json(&value)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", p.display(), e))?;
return Ok(p.to_path_buf());
}
}
ConfigService::resolve_and_validate_with_node_schema_resolver(
path,
Some(&mecha10_schema_registry::node_schema_resolver),
)
}
fn is_model_catalog_file(path: &Path) -> bool {
path.file_name().and_then(|n| n.to_str()) == Some("model_catalog.toml")
}
fn is_behavior_config_file(path: &Path) -> bool {
if path.file_name().and_then(|n| n.to_str()) != Some("config.json") {
return false;
}
path.components().any(|c| c.as_os_str().to_str() == Some("behaviors"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn validate_config_validates_a_valid_model_catalog_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(
&path,
r#"
[[models]]
name = "yolov8n"
description = "YOLOv8 Nano"
task = "object-detection"
repo = "some/repo"
filename = "model.onnx"
"#,
)
.unwrap();
let result = validate_config(Some(&path));
assert!(result.is_ok(), "{:?}", result.err());
assert_eq!(result.unwrap(), path);
}
#[test]
fn validate_config_rejects_an_invalid_model_catalog_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(&path, "[[models]]\nname = \"bad\"\ndescription = \"d\"\n").unwrap();
assert!(validate_config(Some(&path)).is_err());
}
#[test]
fn validate_config_rejects_duplicate_model_names() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(
&path,
r#"
[[models]]
name = "dup"
description = "d1"
task = "object-detection"
repo = "r"
filename = "f.onnx"
[[models]]
name = "dup"
description = "d2"
task = "object-detection"
repo = "r"
filename = "f2.onnx"
"#,
)
.unwrap();
let err = validate_config(Some(&path)).unwrap_err();
assert!(err.to_string().to_lowercase().contains("duplicate"));
}
#[test]
fn validate_config_dispatches_a_built_in_node_type_to_its_specific_schema() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("config")).unwrap();
fs::write(dir.path().join("config/behavior.json"), "{}").unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"nodes": {
"custom": [
{"name": "behavior_executor", "path": "./nodes/behavior", "config": "./config/behavior.json"}
]
}
})
.to_string(),
)
.unwrap();
let err = validate_config(Some(&project_path)).unwrap_err();
assert!(
err.to_string().to_lowercase().contains("behavior_name"),
"expected the specific BehaviorExecutorConfig schema error, got: {}",
err
);
}
#[test]
fn validate_config_rejects_legacy_top_level_logging_block() {
let dir = tempdir().unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"logging": { "disk_logging_enabled": false }
})
.to_string(),
)
.unwrap();
let err = validate_config(Some(&project_path)).unwrap_err();
assert!(
err.to_string().contains("environments.<env>.logging"),
"expected the error to point at the new per-environment shape, got: {}",
err
);
}
#[test]
fn validate_config_falls_back_to_generic_schema_for_unrecognized_node_type() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("config")).unwrap();
fs::write(
dir.path().join("config/custom.json"),
serde_json::json!({"id": "custom_node", "executable": "./bin/custom"}).to_string(),
)
.unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"nodes": {
"custom": [
{"name": "my_totally_custom_node", "path": "./nodes/custom", "config": "./config/custom.json"}
]
}
})
.to_string(),
)
.unwrap();
assert!(validate_config(Some(&project_path)).is_ok());
}
}