use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensorData {
pub timestamp: String,
pub cwd: PathBuf,
pub os_arch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<GitSensor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docker: Option<DockerSensor>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitSensor {
pub branch: String,
pub status: String,
pub dirty: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerSensor {
pub in_container: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub container_id: Option<String>,
}
impl SensorData {
pub fn collect() -> Self {
Self {
timestamp: Self::collect_timestamp(),
cwd: Self::collect_cwd(),
os_arch: Self::collect_os_arch(),
git: Self::collect_git(),
docker: Self::collect_docker(),
}
}
fn collect_timestamp() -> String {
chrono::Utc::now().to_rfc3339()
}
fn collect_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn collect_os_arch() -> String {
format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
}
fn collect_git() -> Option<GitSensor> {
let branch = std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
})?;
let status_output = std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok()?;
let dirty = !status_output.stdout.is_empty();
let status = if dirty { "dirty" } else { "clean" };
Some(GitSensor {
branch,
status: status.to_string(),
dirty,
})
}
fn collect_docker() -> Option<DockerSensor> {
let in_container = std::path::Path::new("/.dockerenv").exists();
if !in_container {
return None;
}
let container_id = std::fs::read_to_string("/proc/self/cgroup")
.ok()
.and_then(|content| {
content
.lines()
.find(|line| line.contains("docker"))
.and_then(|line| line.split('/').next_back())
.map(|id| id.to_string())
});
Some(DockerSensor {
in_container,
container_id,
})
}
}
#[derive(Debug, Clone)]
pub struct Sensors(pub SensorData);
impl Sensors {
pub fn data(&self) -> &SensorData {
&self.0
}
pub fn data_mut(&mut self) -> &mut SensorData {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sensor_data_collect() {
let data = SensorData::collect();
assert!(!data.timestamp.is_empty());
assert!(data.cwd.exists() || data.cwd == std::path::Path::new("."));
assert!(data.os_arch.contains('-'));
}
#[test]
fn test_os_arch_format() {
let os_arch = SensorData::collect_os_arch();
assert!(os_arch.contains('-'));
let parts: Vec<&str> = os_arch.split('-').collect();
assert!(parts.len() >= 2, "OS-arch should be in format 'os-arch'");
}
#[test]
fn test_timestamp_format() {
let timestamp = SensorData::collect_timestamp();
assert!(chrono::DateTime::parse_from_rfc3339(×tamp).is_ok());
}
#[test]
fn test_cwd_exists() {
let cwd = SensorData::collect_cwd();
assert!(cwd.exists() || cwd == std::path::Path::new("."));
}
}