use std::fs;
use std::io::Read;
use std::path::Path;
use anyhow::Context;
use sha2::{Digest, Sha256};
pub fn read_cell_spec(path: &Path) -> anyhow::Result<String> {
if path.as_os_str() == std::ffi::OsStr::new("-") {
let mut raw = String::new();
std::io::stdin()
.read_to_string(&mut raw)
.context("read cell spec from stdin")?;
return Ok(raw);
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut opts = fs::OpenOptions::new();
opts.read(true);
opts.custom_flags(libc::O_RDONLY | libc::O_NOFOLLOW);
let mut file = opts
.open(path)
.with_context(|| format!("read cell spec {}", path.display()))?;
let mut raw = String::new();
file.read_to_string(&mut raw)
.with_context(|| format!("read cell spec {}", path.display()))?;
Ok(raw)
}
#[cfg(not(unix))]
{
fs::read_to_string(path).with_context(|| format!("read cell spec {}", path.display()))
}
}
pub fn spec_sha256(raw: &str) -> String {
let digest = Sha256::digest(raw.as_bytes());
digest.iter().map(|b| format!("{b:02x}")).collect()
}
pub const TENANT_ID_DEFAULT_TOKEN: &str = "_no_tenant_";
pub fn resolve_event_subject(spec_id: &str, run_id: &str, tenant_id: Option<&str>) -> String {
let tmpl = std::env::var("CELLOS_EVENTS_SUBJECT_TEMPLATE").unwrap_or_default();
if !tmpl.trim().is_empty() {
let tenant = tenant_id.unwrap_or(TENANT_ID_DEFAULT_TOKEN);
return tmpl
.replace("{spec_id}", spec_id)
.replace("{run_id}", run_id)
.replace("{tenantId}", tenant);
}
if let Ok(env_tenant) = std::env::var("CELLOS_TENANT_ID") {
let env_tenant = env_tenant.trim();
if !env_tenant.is_empty() {
return format!("cellos.events.{env_tenant}.{spec_id}.{run_id}");
}
}
std::env::var("CELL_OS_EVENTS_SUBJECT").unwrap_or_else(|_| "cellos.events.v1".into())
}
#[cfg(test)]
mod tests {
use super::resolve_event_subject;
static SUBJECT_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn resolve_subject_default_when_neither_var_set() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::remove_var("CELL_OS_EVENTS_SUBJECT");
assert_eq!(
resolve_event_subject("my-spec", "run-1", None),
"cellos.events.v1"
);
}
#[test]
fn resolve_subject_literal_from_env() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::set_var("CELL_OS_EVENTS_SUBJECT", "cellos.events.custom");
let result = resolve_event_subject("my-spec", "run-1", None);
std::env::remove_var("CELL_OS_EVENTS_SUBJECT");
assert_eq!(result, "cellos.events.custom");
}
#[test]
fn resolve_subject_template_substitutes_spec_id_and_run_id() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::set_var(
"CELLOS_EVENTS_SUBJECT_TEMPLATE",
"cellos.{spec_id}.{run_id}.v1",
);
let result = resolve_event_subject("demo-cell", "run-42", None);
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
assert_eq!(result, "cellos.demo-cell.run-42.v1");
}
#[test]
fn resolve_subject_template_takes_priority_over_literal_env() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::set_var("CELLOS_EVENTS_SUBJECT_TEMPLATE", "cellos.{spec_id}.v1");
std::env::set_var("CELL_OS_EVENTS_SUBJECT", "cellos.events.custom");
let result = resolve_event_subject("cell-abc", "run-1", None);
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::remove_var("CELL_OS_EVENTS_SUBJECT");
assert_eq!(result, "cellos.cell-abc.v1");
}
#[test]
fn resolve_subject_cellos_tenant_id_produces_namespaced_subject() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::remove_var("CELL_OS_EVENTS_SUBJECT");
std::env::set_var("CELLOS_TENANT_ID", "tenant-a");
let result = resolve_event_subject("demo-cell", "run-42", None);
std::env::remove_var("CELLOS_TENANT_ID");
assert_eq!(result, "cellos.events.tenant-a.demo-cell.run-42");
}
#[test]
fn resolve_subject_no_cellos_tenant_id_preserves_legacy_default() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::remove_var("CELL_OS_EVENTS_SUBJECT");
std::env::remove_var("CELLOS_TENANT_ID");
let result = resolve_event_subject("demo-cell", "run-42", None);
assert_eq!(result, "cellos.events.v1");
}
#[test]
fn resolve_subject_template_beats_cellos_tenant_id() {
let _g = SUBJECT_MUTEX.lock().unwrap();
std::env::set_var("CELLOS_EVENTS_SUBJECT_TEMPLATE", "cellos.{spec_id}.v1");
std::env::set_var("CELLOS_TENANT_ID", "tenant-a");
let result = resolve_event_subject("demo-cell", "run-42", None);
std::env::remove_var("CELLOS_EVENTS_SUBJECT_TEMPLATE");
std::env::remove_var("CELLOS_TENANT_ID");
assert_eq!(result, "cellos.demo-cell.v1");
}
}