use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExecutionContext {
inner: Arc<ExecutionContextInner>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ExecutionContextInner {
name: Option<String>,
cwd: Option<PathBuf>,
env_overrides: BTreeMap<String, String>,
trusted: bool,
}
impl ExecutionContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_name(self, name: impl Into<String>) -> Self {
let mut inner = Arc::unwrap_or_clone(self.inner);
inner.name = Some(name.into());
Self {
inner: Arc::new(inner),
}
}
#[must_use]
pub fn with_cwd(self, cwd: impl Into<PathBuf>) -> Self {
let mut inner = Arc::unwrap_or_clone(self.inner);
inner.cwd = Some(cwd.into());
Self {
inner: Arc::new(inner),
}
}
#[must_use]
pub fn with_env(self, key: impl Into<String>, value: impl Into<String>) -> Self {
let mut inner = Arc::unwrap_or_clone(self.inner);
inner.env_overrides.insert(key.into(), value.into());
Self {
inner: Arc::new(inner),
}
}
#[must_use]
pub fn with_envs<K, V, I>(self, iter: I) -> Self
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
let mut inner = Arc::unwrap_or_clone(self.inner);
for (k, v) in iter {
inner.env_overrides.insert(k.into(), v.into());
}
Self {
inner: Arc::new(inner),
}
}
}
impl ExecutionContext {
#[must_use]
pub fn name(&self) -> Option<&str> {
self.inner.name.as_deref()
}
#[must_use]
pub fn cwd(&self) -> Option<&Path> {
self.inner.cwd.as_deref()
}
#[must_use]
pub fn env_overrides(&self) -> &BTreeMap<String, String> {
&self.inner.env_overrides
}
#[must_use]
pub fn is_trusted(&self) -> bool {
self.inner.trusted
}
}
impl ExecutionContext {
pub(crate) fn trusted_from_parts(
name: Option<String>,
cwd: Option<PathBuf>,
env_overrides: BTreeMap<String, String>,
) -> Self {
Self {
inner: Arc::new(ExecutionContextInner {
name,
cwd,
env_overrides,
trusted: true,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_untrusted_and_empty() {
let ctx = ExecutionContext::new();
assert!(ctx.name().is_none());
assert!(ctx.cwd().is_none());
assert!(ctx.env_overrides().is_empty());
assert!(!ctx.is_trusted());
}
#[test]
fn builder_methods_chain() {
let ctx = ExecutionContext::new()
.with_name("test")
.with_cwd("/tmp")
.with_env("FOO", "bar");
assert_eq!(ctx.name(), Some("test"));
assert_eq!(ctx.cwd(), Some(Path::new("/tmp")));
assert_eq!(
ctx.env_overrides().get("FOO").map(String::as_str),
Some("bar")
);
assert!(!ctx.is_trusted());
}
#[test]
fn with_envs_adds_multiple() {
let ctx = ExecutionContext::new().with_envs([("A", "1"), ("B", "2")]);
assert_eq!(ctx.env_overrides().len(), 2);
assert_eq!(ctx.env_overrides()["A"], "1");
assert_eq!(ctx.env_overrides()["B"], "2");
}
#[test]
fn trusted_from_parts_is_trusted() {
let ctx = ExecutionContext::trusted_from_parts(
Some("ops".to_owned()),
Some(PathBuf::from("/workspace")),
[("SECRET_KEY".to_owned(), "val".to_owned())]
.into_iter()
.collect(),
);
assert!(ctx.is_trusted());
assert_eq!(ctx.name(), Some("ops"));
}
#[test]
fn clone_shares_arc() {
let ctx = ExecutionContext::new().with_name("shared");
let cloned = ctx.clone();
assert_eq!(ctx, cloned);
assert!(Arc::ptr_eq(&ctx.inner, &cloned.inner));
}
#[test]
fn with_env_overwrites_existing() {
let ctx = ExecutionContext::new()
.with_env("KEY", "first")
.with_env("KEY", "second");
assert_eq!(ctx.env_overrides()["KEY"], "second");
}
}