use std::path::Path;
use crate::{AgentSpec, RunError, SwarmFile};
pub type ParseResult<T> = Result<T, ParseError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum ParseError {
#[error("IO error: {0}")]
Io(String),
#[error("YAML parse error: {0}")]
Yaml(String),
#[error("JSON parse error: {0}")]
Json(String),
#[error("Validation error: {0}")]
Validation(String),
}
impl From<ParseError> for RunError {
fn from(e: ParseError) -> Self {
RunError::InvalidConfig {
message: e.to_string(),
}
}
}
pub struct AgentSpecParser;
impl AgentSpecParser {
pub fn from_yaml_file(path: &Path) -> ParseResult<AgentSpec> {
let content = std::fs::read_to_string(path)
.map_err(|e| ParseError::Io(format!("Failed to read {}: {}", path.display(), e)))?;
Self::from_yaml_str(&content)
}
pub fn from_yaml_str(content: &str) -> ParseResult<AgentSpec> {
let spec: AgentSpec = serde_yaml::from_str(content)
.map_err(|e| ParseError::Yaml(format!("Failed to parse YAML: {}", e)))?;
spec.validate()
.map_err(|e| ParseError::Validation(e.to_string()))?;
Ok(spec)
}
pub fn from_json_str(content: &str) -> ParseResult<AgentSpec> {
let spec: AgentSpec = serde_json::from_str(content)
.map_err(|e| ParseError::Json(format!("Failed to parse JSON: {}", e)))?;
spec.validate()
.map_err(|e| ParseError::Validation(e.to_string()))?;
Ok(spec)
}
}
pub struct SwarmFileParser;
impl SwarmFileParser {
pub fn from_yaml_file(path: &Path) -> ParseResult<SwarmFile> {
let content = std::fs::read_to_string(path)
.map_err(|e| ParseError::Io(format!("Failed to read {}: {}", path.display(), e)))?;
let mut swarm: SwarmFile = serde_yaml::from_str(&content)
.map_err(|e| ParseError::Yaml(format!("Failed to parse YAML: {}", e)))?;
swarm
.validate()
.map_err(|e| ParseError::Validation(e.to_string()))?;
swarm.file_path = Some(path.to_path_buf());
Ok(swarm)
}
pub fn from_yaml_str(content: &str) -> ParseResult<SwarmFile> {
let swarm: SwarmFile = serde_yaml::from_str(content)
.map_err(|e| ParseError::Yaml(format!("Failed to parse YAML: {}", e)))?;
swarm
.validate()
.map_err(|e| ParseError::Validation(e.to_string()))?;
Ok(swarm)
}
pub fn from_json_str(content: &str) -> ParseResult<SwarmFile> {
let swarm: SwarmFile = serde_json::from_str(content)
.map_err(|e| ParseError::Json(format!("Failed to parse JSON: {}", e)))?;
swarm
.validate()
.map_err(|e| ParseError::Validation(e.to_string()))?;
Ok(swarm)
}
}
pub struct Validator;
impl Validator {
pub fn validate_agent_spec(spec: &AgentSpec) -> ParseResult<()> {
spec.validate()
.map_err(|e| ParseError::Validation(e.to_string()))
}
pub fn validate_swarmfile(swarm: &SwarmFile) -> ParseResult<()> {
swarm
.validate()
.map_err(|e| ParseError::Validation(e.to_string()))
}
pub fn validate_swarm_workers(swarm: &SwarmFile, base_path: &Path) -> ParseResult<()> {
for worker in &swarm.workers {
if worker.a2a.is_some() {
continue;
}
if let Some(spec) = &worker.spec {
let spec_path = base_path.join(spec);
if !spec_path.exists() {
return Err(ParseError::Validation(format!(
"Worker '{}' references non-existent spec: {}",
worker.name,
spec_path.display()
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FlowPattern, RuntimeKind};
#[test]
fn test_parse_agent_spec_yaml() {
let yaml = r#"
apiVersion: v1
id: test-agent
runtime:
kind: Local
"#;
let spec = AgentSpecParser::from_yaml_str(yaml).unwrap();
assert_eq!(spec.id.as_str(), "test-agent");
assert_eq!(spec.runtime.kind, RuntimeKind::Local);
}
#[test]
fn test_parse_agent_spec_json() {
let json = r#"{"apiVersion":"v1","id":"test","runtime":{"kind":"Local"}}"#;
let spec = AgentSpecParser::from_json_str(json).unwrap();
assert_eq!(spec.id.as_str(), "test");
}
#[test]
fn test_parse_agent_spec_invalid_yaml() {
let yaml = "invalid: yaml: :";
let result = AgentSpecParser::from_yaml_str(yaml);
assert!(result.is_err());
}
#[test]
fn test_parse_agent_spec_validation_error() {
let yaml = r#"
apiVersion: v1
id: ""
runtime:
kind: Local
"#;
let result = AgentSpecParser::from_yaml_str(yaml);
assert!(matches!(result, Err(ParseError::Validation(_))));
}
#[test]
fn test_parse_swarmfile_yaml() {
let yaml = r#"
apiVersion: bzzz.dev/v1
kind: swarm
id: test-swarm
workers: []
flow:
type: sequence
steps: []
"#;
let swarm = SwarmFileParser::from_yaml_str(yaml).unwrap();
assert_eq!(swarm.id.as_str(), "test-swarm");
}
#[test]
fn test_parse_swarmfile_with_workers() {
let yaml = r#"
apiVersion: bzzz.dev/v1
kind: swarm
id: my-swarm
workers:
- name: agent1
spec: agents/a.yaml
flow:
type: parallel
branches: ["agent1"]
"#;
let swarm = SwarmFileParser::from_yaml_str(yaml).unwrap();
assert_eq!(swarm.workers.len(), 1);
assert_eq!(swarm.workers[0].name, "agent1");
}
#[test]
fn test_validate_agent_spec() {
let spec = AgentSpec::new("test", RuntimeKind::Local);
assert!(Validator::validate_agent_spec(&spec).is_ok());
let invalid = AgentSpec::new("", RuntimeKind::Local);
assert!(Validator::validate_agent_spec(&invalid).is_err());
}
#[test]
fn test_validate_swarmfile() {
let swarm = SwarmFile::new("test", FlowPattern::Sequence { steps: vec![] });
assert!(Validator::validate_swarmfile(&swarm).is_ok());
let invalid = SwarmFile::new("", FlowPattern::Sequence { steps: vec![] });
assert!(Validator::validate_swarmfile(&invalid).is_err());
}
#[test]
fn test_parse_all_flow_patterns() {
let patterns = vec![
(r#"{"type":"sequence","steps":[]}"#, "sequence"),
(
r#"{"type":"parallel","branches":[],"fail_fast":false}"#,
"parallel",
),
(
r#"{"type":"conditional","condition":"x","then":"a"}"#,
"conditional",
),
(r#"{"type":"loop","over":"items","do_":"process"}"#, "loop"),
(r#"{"type":"delegate","swarm":"sub.yaml"}"#, "delegate"),
(
r#"{"type":"supervisor","workers":[],"restart_policy":"on_failure"}"#,
"supervisor",
),
(r#"{"type":"compete","workers":[]}"#, "compete"),
(
r#"{"type":"escalation","primary":"main","chain":[]}"#,
"escalation",
),
(
r#"{"type":"alongside","main":"main","side":[]}"#,
"alongside",
),
];
for (json, name) in patterns {
let yaml = format!(
r#"
apiVersion: bzzz.dev/v1
kind: swarm
id: test
workers:
- name: a
spec: a.yaml
- name: process
spec: process.yaml
- name: main
spec: main.yaml
flow: {}
"#,
json
);
let result = SwarmFileParser::from_yaml_str(&yaml);
assert!(result.is_ok(), "Failed to parse {} pattern", name);
}
}
}