use std::{fs, path::Path};
use sim_kernel::{Error, Expr, Result, Symbol};
use crate::{Graph, GraphTest, text::graph_from_text};
#[derive(Clone, Debug)]
pub struct TopologyPackage {
pub graph: Graph,
pub tests: Vec<GraphTest>,
pub metadata: Vec<(Symbol, Expr)>,
pub capabilities: Vec<Symbol>,
}
impl TopologyPackage {
pub fn name(&self) -> &Symbol {
&self.graph.name
}
}
pub fn parse_package(source: &str) -> Result<TopologyPackage> {
let sections = PackageSections::parse(source)?;
let mut graph_text = String::new();
append_lines(&mut graph_text, §ions.graph);
for line in §ions.metadata {
let trimmed = line.trim();
if trimmed.starts_with("meta ") {
graph_text.push_str(trimmed);
} else {
graph_text.push_str("meta ");
graph_text.push_str(trimmed);
}
graph_text.push('\n');
}
for line in §ions.tests {
let trimmed = line.trim();
if trimmed.starts_with("test ") {
graph_text.push_str(trimmed);
} else {
graph_text.push_str("test ");
graph_text.push_str(trimmed);
}
graph_text.push('\n');
}
let mut graph = graph_from_text(&graph_text)?;
graph.capabilities = sections
.capabilities
.iter()
.map(|line| symbol_from_text(line.trim()))
.collect();
Ok(TopologyPackage {
tests: graph.tests.clone(),
metadata: graph.metadata.clone(),
capabilities: graph.capabilities.clone(),
graph,
})
}
pub fn load_package_file(path: impl AsRef<Path>) -> Result<TopologyPackage> {
let path = path.as_ref();
let source = fs::read_to_string(path).map_err(|err| {
Error::HostError(format!(
"failed to read topology package {}: {err}",
path.display()
))
})?;
parse_package(&source)
}
#[derive(Clone, Copy)]
enum Section {
Graph,
Tests,
Metadata,
Capabilities,
}
#[derive(Default)]
struct PackageSections {
graph: Vec<String>,
tests: Vec<String>,
metadata: Vec<String>,
capabilities: Vec<String>,
}
impl PackageSections {
fn parse(source: &str) -> Result<Self> {
let mut sections = Self::default();
let mut current = None;
for (index, line) in source.lines().enumerate() {
let line_no = index + 1;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(section) = parse_section_header(trimmed, line_no)? {
current = Some(section);
continue;
}
let Some(section) = current else {
return Err(package_error(
line_no,
1,
"package content must appear inside a named section",
));
};
match section {
Section::Graph => sections.graph.push(line.to_owned()),
Section::Tests => sections.tests.push(trimmed.to_owned()),
Section::Metadata => sections.metadata.push(trimmed.to_owned()),
Section::Capabilities => sections.capabilities.push(trimmed.to_owned()),
}
}
if sections.graph.is_empty() {
return Err(Error::Eval(
"topology package parse error: missing graph section".to_owned(),
));
}
Ok(sections)
}
}
fn parse_section_header(trimmed: &str, line: usize) -> Result<Option<Section>> {
let Some(name) = trimmed.strip_suffix(':') else {
return Ok(None);
};
match name {
"graph" => Ok(Some(Section::Graph)),
"tests" => Ok(Some(Section::Tests)),
"metadata" => Ok(Some(Section::Metadata)),
"capabilities" => Ok(Some(Section::Capabilities)),
_ => Err(package_error(
line,
1,
format!("unknown package section {name}"),
)),
}
}
fn append_lines(output: &mut String, lines: &[String]) {
for line in lines {
output.push_str(line);
output.push('\n');
}
}
fn symbol_from_text(text: &str) -> Symbol {
if let Some((namespace, name)) = text.split_once('/')
&& !namespace.is_empty()
&& !name.is_empty()
{
return Symbol::qualified(namespace.to_owned(), name.to_owned());
}
if let Some((namespace, name)) = text.split_once(':')
&& !namespace.is_empty()
&& !name.is_empty()
{
return Symbol::qualified(namespace.to_owned(), name.to_owned());
}
Symbol::new(text.to_owned())
}
fn package_error(line: usize, column: usize, message: impl Into<String>) -> Error {
Error::Eval(format!(
"topology package parse error at line {line}, column {column}: {}",
message.into()
))
}