Skip to main content

sva_cli/
composition.rs

1// Concern: the directory a subcommand reads its composition from, and a target's refs inside it | Non-concern: loading it (sva-core), where a reading goes | IO: (cwd, target) -> a directory, a target
2
3use std::io::ErrorKind;
4use std::path::{Component, Path, PathBuf};
5
6use sva_ast::TokenKind;
7use sva_core::{CliError, cwd};
8
9pub fn composition() -> Result<PathBuf, CliError> {
10    let dir = cwd()?;
11    match std::fs::read_dir(&dir) {
12        Ok(_) => Ok(dir),
13        Err(e) if e.kind() == ErrorKind::NotFound => Err(CliError::NotFound(format!(
14            "no composition at `{}`",
15            dir.display()
16        ))),
17        Err(e) => Err(CliError::Io(format!(
18            "could not read the composition at `{}`: {e}",
19            dir.display()
20        ))),
21    }
22}
23
24/// `here` while every ref sits under it; else the nearest directory above the first ref
25/// outside it that holds `variables/`, else that ref's own. Each ref is rewritten under it.
26pub fn located(here: &Path, target: &str) -> Result<(PathBuf, String), CliError> {
27    let Ok(tokens) = sva_ast::tokenize(target) else {
28        return Ok((here.to_path_buf(), target.to_string()));
29    };
30    let refs: Vec<(sva_ast::ByteSpan, PathBuf)> = tokens
31        .iter()
32        .filter_map(|t| match &t.kind {
33            TokenKind::Ref(path) => Some((t.span, normal(&here.join(path)))),
34            _ => None,
35        })
36        .collect();
37    let root = match refs.iter().find(|(_, file)| !file.starts_with(here)) {
38        None => here.to_path_buf(),
39        Some((_, file)) => holding(file),
40    };
41    let mut out = String::with_capacity(target.len());
42    let mut at = 0;
43    for (span, file) in &refs {
44        let inside = file.strip_prefix(&root).map_err(|_| {
45            CliError::Usage(format!(
46                "`{target}` reads `{}` and `{}`, which no one composition holds; read one \
47                 composition's nodes at a time",
48                refs[0].1.display(),
49                file.display()
50            ))
51        })?;
52        out.push_str(&target[at..span.start]);
53        out.push('@');
54        out.push_str(&inside.to_string_lossy());
55        at = span.end;
56    }
57    out.push_str(&target[at..]);
58    Ok((root, out))
59}
60
61fn normal(path: &Path) -> PathBuf {
62    let mut out = PathBuf::new();
63    for part in path.components() {
64        match part {
65            Component::CurDir => {}
66            Component::ParentDir => {
67                out.pop();
68            }
69            other => out.push(other),
70        }
71    }
72    out
73}
74
75fn holding(file: &Path) -> PathBuf {
76    let own = file.parent().unwrap_or(Path::new("/")).to_path_buf();
77    own.ancestors()
78        .find(|dir| dir.join(sva_ast::VARIABLES).is_dir())
79        .map_or(own.clone(), Path::to_path_buf)
80}