1use std::{
4 fs,
5 path::{Path, PathBuf},
6};
7
8use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol, Value};
9
10use crate::{
11 Graph, GraphTest,
12 capability::{capability_name_from_text, capability_symbol},
13 text::graph_from_text,
14};
15
16#[derive(Clone, Debug)]
18pub enum TopologyPackageSource {
19 HostFile {
21 path: PathBuf,
23 },
24 TableEntry {
26 table: Value,
28 key: Symbol,
30 },
31}
32
33impl TopologyPackageSource {
34 pub fn host_file(path: impl Into<PathBuf>) -> Self {
36 Self::HostFile { path: path.into() }
37 }
38
39 pub fn table_entry(table: Value, key: Symbol) -> Self {
41 Self::TableEntry { table, key }
42 }
43
44 pub(crate) fn read_to_string(&self, cx: &mut Cx) -> Result<String> {
45 match self {
46 Self::HostFile { path } => read_host_file(path),
47 Self::TableEntry { table, key } => read_table_entry(cx, table, key),
48 }
49 }
50
51 pub(crate) fn requires_topology_file(&self) -> bool {
52 matches!(self, Self::HostFile { .. })
53 }
54}
55
56#[derive(Clone, Debug)]
58pub struct TopologyPackage {
59 pub graph: Graph,
61 pub tests: Vec<GraphTest>,
63 pub metadata: Vec<(Symbol, Expr)>,
65 pub capabilities: Vec<CapabilityName>,
67}
68
69impl TopologyPackage {
70 pub fn name(&self) -> &Symbol {
72 &self.graph.name
73 }
74}
75
76pub fn parse_package(source: &str) -> Result<TopologyPackage> {
83 let sections = PackageSections::parse(source)?;
84 let mut graph_text = String::new();
85
86 append_lines(&mut graph_text, §ions.graph);
87 for line in §ions.metadata {
88 let trimmed = line.trim();
89 if trimmed.starts_with("meta ") {
90 graph_text.push_str(trimmed);
91 } else {
92 graph_text.push_str("meta ");
93 graph_text.push_str(trimmed);
94 }
95 graph_text.push('\n');
96 }
97 for line in §ions.tests {
98 let trimmed = line.trim();
99 if trimmed.starts_with("test ") {
100 graph_text.push_str(trimmed);
101 } else {
102 graph_text.push_str("test ");
103 graph_text.push_str(trimmed);
104 }
105 graph_text.push('\n');
106 }
107
108 let capabilities = sections
109 .capabilities
110 .iter()
111 .map(|line| parse_capability_line(line.line_no, &line.text))
112 .collect::<Result<Vec<_>>>()?;
113
114 let mut graph = graph_from_text(&graph_text)?;
115 graph.capabilities = capabilities.iter().map(capability_symbol).collect();
116
117 Ok(TopologyPackage {
118 tests: graph.tests.clone(),
119 metadata: graph.metadata.clone(),
120 capabilities,
121 graph,
122 })
123}
124
125pub fn load_package_file(path: impl Into<PathBuf>) -> Result<TopologyPackage> {
127 let path = path.into();
128 let source = read_host_file(&path)?;
129 parse_package(&source)
130}
131
132pub fn load_package_source(cx: &mut Cx, source: &TopologyPackageSource) -> Result<TopologyPackage> {
134 parse_package(&source.read_to_string(cx)?)
135}
136
137fn read_host_file(path: &Path) -> Result<String> {
138 let bytes = fs::read(path).map_err(|err| {
139 Error::HostError(format!(
140 "failed to read topology package {}: {err}",
141 path.display()
142 ))
143 })?;
144 String::from_utf8(bytes).map_err(|err| {
145 Error::HostError(format!(
146 "failed to decode topology package {} as utf-8: {err}",
147 path.display()
148 ))
149 })
150}
151
152fn read_table_entry(cx: &mut Cx, table: &Value, key: &Symbol) -> Result<String> {
153 let table = table.object().as_table_impl().ok_or(Error::TypeMismatch {
154 expected: "table",
155 found: "non-table",
156 })?;
157 let value = table.get(cx, key.clone())?;
158 match value.object().as_expr(cx)? {
159 Expr::String(source) => Ok(source),
160 Expr::Bytes(bytes) => String::from_utf8(bytes).map_err(|err| {
161 Error::Eval(format!(
162 "topology package table entry {key} is not utf-8: {err}"
163 ))
164 }),
165 Expr::Nil => Err(Error::Eval(format!(
166 "topology package table entry {key} is missing"
167 ))),
168 other => Err(Error::TypeMismatch {
169 expected: "string or bytes package source",
170 found: expr_type(&other),
171 }),
172 }
173}
174
175#[derive(Clone, Copy)]
176enum Section {
177 Graph,
178 Tests,
179 Metadata,
180 Capabilities,
181}
182
183#[derive(Default)]
184struct PackageSections {
185 graph: Vec<String>,
186 tests: Vec<String>,
187 metadata: Vec<String>,
188 capabilities: Vec<PackageLine>,
189}
190
191impl PackageSections {
192 fn parse(source: &str) -> Result<Self> {
193 let mut sections = Self::default();
194 let mut current = None;
195
196 for (index, line) in source.lines().enumerate() {
197 let line_no = index + 1;
198 let trimmed = line.trim();
199 if trimmed.is_empty() || trimmed.starts_with('#') {
200 continue;
201 }
202 if let Some(section) = parse_section_header(trimmed, line_no)? {
203 current = Some(section);
204 continue;
205 }
206 let Some(section) = current else {
207 return Err(package_error(
208 line_no,
209 1,
210 "package content must appear inside a named section",
211 ));
212 };
213 match section {
214 Section::Graph => sections.graph.push(line.to_owned()),
215 Section::Tests => sections.tests.push(trimmed.to_owned()),
216 Section::Metadata => sections.metadata.push(trimmed.to_owned()),
217 Section::Capabilities => sections.capabilities.push(PackageLine {
218 line_no,
219 text: trimmed.to_owned(),
220 }),
221 }
222 }
223
224 if sections.graph.is_empty() {
225 return Err(Error::Eval(
226 "topology package parse error: missing graph section".to_owned(),
227 ));
228 }
229
230 Ok(sections)
231 }
232}
233
234struct PackageLine {
235 line_no: usize,
236 text: String,
237}
238
239fn parse_section_header(trimmed: &str, line: usize) -> Result<Option<Section>> {
240 let Some(name) = trimmed.strip_suffix(':') else {
241 return Ok(None);
242 };
243 match name {
244 "graph" => Ok(Some(Section::Graph)),
245 "tests" => Ok(Some(Section::Tests)),
246 "metadata" => Ok(Some(Section::Metadata)),
247 "capabilities" => Ok(Some(Section::Capabilities)),
248 _ => Err(package_error(
249 line,
250 1,
251 format!("unknown package section {name}"),
252 )),
253 }
254}
255
256fn append_lines(output: &mut String, lines: &[String]) {
257 for line in lines {
258 output.push_str(line);
259 output.push('\n');
260 }
261}
262
263fn parse_capability_line(line_no: usize, text: &str) -> Result<CapabilityName> {
264 capability_name_from_text(text).map_err(|err| package_error(line_no, 1, err.to_string()))
265}
266
267fn expr_type(expr: &Expr) -> &'static str {
268 match expr {
269 Expr::Nil => "nil",
270 Expr::Bool(_) => "bool",
271 Expr::Number(_) => "number",
272 Expr::Symbol(_) => "symbol",
273 Expr::Local(_) => "local",
274 Expr::String(_) => "string",
275 Expr::Bytes(_) => "bytes",
276 Expr::List(_) => "list",
277 Expr::Vector(_) => "vector",
278 Expr::Map(_) => "map",
279 Expr::Set(_) => "set",
280 Expr::Call { .. } => "call",
281 Expr::Infix { .. } => "infix",
282 Expr::Prefix { .. } => "prefix",
283 Expr::Postfix { .. } => "postfix",
284 Expr::Block(_) => "block",
285 Expr::Quote { .. } => "quote",
286 Expr::Annotated { .. } => "annotated",
287 Expr::Extension { .. } => "extension",
288 }
289}
290
291fn package_error(line: usize, column: usize, message: impl Into<String>) -> Error {
292 Error::Eval(format!(
293 "topology package parse error at line {line}, column {column}: {}",
294 message.into()
295 ))
296}