Skip to main content

vantage_cmd/
cmd.rs

1//! The [`Cmd`] datasource — a locked command, declared env, and a
2//! registry of per-table Rhai scripts.
3
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use indexmap::IndexMap;
8use vantage_core::{Result, error};
9
10/// Per-table configuration registered on a [`Cmd`]: the Rhai script that
11/// builds the argv and parses the output, plus optional command / env
12/// overrides that win over the datasource-level defaults.
13#[derive(Clone, Debug)]
14pub struct CmdSpec {
15    pub script: Arc<str>,
16    pub command: Option<String>,
17    pub env: IndexMap<String, String>,
18}
19
20impl CmdSpec {
21    pub fn new(script: impl Into<Arc<str>>) -> Self {
22        Self {
23            script: script.into(),
24            command: None,
25            env: IndexMap::new(),
26        }
27    }
28
29    /// Override the locked command for this table only.
30    pub fn with_command(mut self, command: impl Into<String>) -> Self {
31        self.command = Some(command.into());
32        self
33    }
34
35    /// Declare an env var for this table only (merged over, and winning
36    /// against, the datasource-level env).
37    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
38        self.env.insert(key.into(), value.into());
39        self
40    }
41}
42
43/// A command-execution datasource.
44///
45/// Cheap to clone (everything is `Arc`-backed). The `command` and `env`
46/// here are the locked defaults; individual tables can be registered with
47/// their own [`CmdSpec`] overrides via [`Cmd::with_table`].
48#[derive(Clone, Debug)]
49pub struct Cmd {
50    command: Arc<str>,
51    env: Arc<IndexMap<String, String>>,
52    pass_path: bool,
53    base_dir: Option<Arc<Path>>,
54    scripts: Arc<IndexMap<String, CmdSpec>>,
55}
56
57impl Cmd {
58    /// Build a datasource locked to `command` (e.g. `"aws"`).
59    pub fn new(command: impl Into<Arc<str>>) -> Self {
60        Self {
61            command: command.into(),
62            env: Arc::new(IndexMap::new()),
63            pass_path: true,
64            base_dir: None,
65            scripts: Arc::new(IndexMap::new()),
66        }
67    }
68
69    /// Declare a datasource-level env var, passed to every table's child
70    /// process unless a table overrides it.
71    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
72        Arc::make_mut(&mut self.env).insert(key.into(), value.into());
73        self
74    }
75
76    /// Whether to forward `PATH`/`HOME` from the current process so the
77    /// command can be located. Defaults to `true`; set `false` to require
78    /// an absolute command path and a fully-declared environment.
79    pub fn with_pass_path(mut self, pass_path: bool) -> Self {
80        self.pass_path = pass_path;
81        self
82    }
83
84    /// Set the base directory used to resolve a relative `command` *path*
85    /// and as the child process's working directory.
86    ///
87    /// A `command` that contains a path separator but isn't absolute (e.g.
88    /// `./scripts/gh-stats.py`) is resolved against this directory; bare
89    /// names (e.g. `gh`) are left untouched for `PATH` lookup, and absolute
90    /// paths pass through. When set, every table's child process also runs
91    /// with this directory as its working directory, so a script can resolve
92    /// sibling files relative to it.
93    pub fn with_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
94        self.base_dir = Some(Arc::from(base_dir.into()));
95        self
96    }
97
98    /// Register a script under `name` with no overrides.
99    pub fn with_script(self, name: impl Into<String>, script: impl Into<Arc<str>>) -> Self {
100        self.with_table(name, CmdSpec::new(script))
101    }
102
103    /// Register a fully-specified [`CmdSpec`] under `name`.
104    pub fn with_table(mut self, name: impl Into<String>, spec: CmdSpec) -> Self {
105        Arc::make_mut(&mut self.scripts).insert(name.into(), spec);
106        self
107    }
108
109    /// The locked default command.
110    pub fn command(&self) -> &str {
111        &self.command
112    }
113
114    pub(crate) fn pass_path(&self) -> bool {
115        self.pass_path
116    }
117
118    pub(crate) fn base_dir(&self) -> Option<Arc<Path>> {
119        self.base_dir.clone()
120    }
121
122    pub(crate) fn spec_for(&self, name: &str) -> Result<&CmdSpec> {
123        self.scripts.get(name).ok_or_else(|| {
124            error!(
125                "no command script registered for table",
126                table = name.to_string()
127            )
128        })
129    }
130
131    /// Effective command for a table: the spec override, else the locked default.
132    pub(crate) fn effective_command(&self, spec: &CmdSpec) -> String {
133        spec.command
134            .clone()
135            .unwrap_or_else(|| self.command.to_string())
136    }
137
138    /// Effective env for a table: datasource env, with the spec's env
139    /// merged on top (spec wins on key clash).
140    pub(crate) fn effective_env(&self, spec: &CmdSpec) -> IndexMap<String, String> {
141        let mut env = (*self.env).clone();
142        for (k, v) in &spec.env {
143            env.insert(k.clone(), v.clone());
144        }
145        env
146    }
147
148    /// A Vista factory bound to this datasource.
149    pub fn vista_factory(&self) -> crate::vista::factory::CmdVistaFactory {
150        crate::vista::factory::CmdVistaFactory::new(self.clone())
151    }
152}