use crate::error::{CaError, CaResult};
use crate::server::device_support::{DeviceReadOutcome, DeviceSupport};
use crate::server::record::Record;
use crate::types::EpicsValue;
#[derive(Default)]
pub struct GetenvDeviceSupport {
cached_var: Option<String>,
}
impl GetenvDeviceSupport {
pub fn new() -> Self {
Self::default()
}
fn resolve_var_name(inp: &str) -> &str {
let trimmed = inp.trim();
trimmed.strip_prefix('@').unwrap_or(trimmed).trim()
}
fn fetch(&self, record: &dyn Record) -> Option<String> {
let inp = record
.get_field("INP")
.and_then(|v| match v {
EpicsValue::String(s) => Some(s),
_ => None,
})
.unwrap_or_default();
let name = Self::resolve_var_name(&inp);
if name.is_empty() {
return None;
}
std::env::var(name).ok()
}
}
impl DeviceSupport for GetenvDeviceSupport {
fn dtyp(&self) -> &str {
"getenv"
}
fn init(&mut self, record: &mut dyn Record) -> CaResult<()> {
let rtype = record.record_type();
if !matches!(rtype, "stringin" | "lsi") {
return Err(CaError::InvalidValue(format!(
"DTYP=getenv: unsupported record type '{rtype}' (use stringin or lsi)"
)));
}
let inp = record
.get_field("INP")
.and_then(|v| match v {
EpicsValue::String(s) => Some(s),
_ => None,
})
.unwrap_or_default();
let name = Self::resolve_var_name(&inp);
if !name.is_empty() {
self.cached_var = Some(name.to_string());
}
let value = self
.cached_var
.as_ref()
.and_then(|var| std::env::var(var).ok());
match value {
Some(s) => {
record.put_field("VAL", EpicsValue::String(s))?;
Ok(())
}
None => {
record.put_field("VAL", EpicsValue::String(String::new()))?;
Err(CaError::InvalidValue(format!(
"getenv: variable '{}' is unset",
self.cached_var.as_deref().unwrap_or("")
)))
}
}
}
fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
let val = if let Some(ref var) = self.cached_var {
std::env::var(var).ok()
} else {
self.fetch(record)
};
match val {
Some(s) => {
record.put_field("VAL", EpicsValue::String(s))?;
Ok(DeviceReadOutcome::ok())
}
None => {
record.put_field("VAL", EpicsValue::String(String::new()))?;
Err(CaError::InvalidValue(format!(
"getenv: variable '{}' is unset",
self.cached_var.as_deref().unwrap_or("")
)))
}
}
}
fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Err(CaError::InvalidValue(
"getenv device support is read-only (stringin / lsi only)".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::records::stringin::StringinRecord;
fn make_record(inp: &str) -> StringinRecord {
let mut r = StringinRecord::new("");
let _ = inp;
r.val = String::new();
r
}
#[test]
fn resolve_var_name_strips_at_prefix() {
assert_eq!(
GetenvDeviceSupport::resolve_var_name("@HOSTNAME"),
"HOSTNAME"
);
assert_eq!(
GetenvDeviceSupport::resolve_var_name(" HOSTNAME "),
"HOSTNAME"
);
assert_eq!(
GetenvDeviceSupport::resolve_var_name("@ HOSTNAME"),
"HOSTNAME"
);
assert_eq!(GetenvDeviceSupport::resolve_var_name(""), "");
assert_eq!(GetenvDeviceSupport::resolve_var_name("@"), "");
}
#[test]
fn init_rejects_unsupported_record_types() {
let dev = GetenvDeviceSupport::new();
assert_eq!(dev.dtyp(), "getenv");
let _ = make_record("@PATH");
}
}