use std::{collections::HashMap, fmt};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::GitError,
hash::ObjectHash,
internal::object::{
ObjectTrait,
integrity::IntegrityHash,
types::{ActorRef, Header, ObjectType},
},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Environment {
pub os: String,
pub arch: String,
pub cwd: String,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl Environment {
pub fn capture() -> Self {
Self {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
cwd: std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|e| {
tracing::warn!("Failed to get current directory: {}", e);
"unknown".to_string()
}),
extra: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Run {
#[serde(flatten)]
header: Header,
task: Uuid,
#[serde(default, skip_serializing_if = "Option::is_none")]
plan: Option<Uuid>,
commit: IntegrityHash,
#[serde(default, skip_serializing_if = "Option::is_none")]
snapshot: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
environment: Option<Environment>,
}
impl Run {
pub fn new(created_by: ActorRef, task: Uuid, commit: impl AsRef<str>) -> Result<Self, String> {
let commit = commit.as_ref().parse()?;
Ok(Self {
header: Header::new(ObjectType::Run, created_by)?,
task,
plan: None,
commit,
snapshot: None,
environment: Some(Environment::capture()),
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn task(&self) -> Uuid {
self.task
}
pub fn plan(&self) -> Option<Uuid> {
self.plan
}
pub fn set_plan(&mut self, plan: Option<Uuid>) {
self.plan = plan;
}
pub fn commit(&self) -> &IntegrityHash {
&self.commit
}
pub fn snapshot(&self) -> Option<Uuid> {
self.snapshot
}
pub fn set_snapshot(&mut self, snapshot: Option<Uuid>) {
self.snapshot = snapshot;
}
pub fn environment(&self) -> Option<&Environment> {
self.environment.as_ref()
}
}
impl fmt::Display for Run {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Run: {}", self.header.object_id())
}
}
impl ObjectTrait for Run {
fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
where
Self: Sized,
{
serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
fn get_type(&self) -> ObjectType {
ObjectType::Run
}
fn get_size(&self) -> usize {
match serde_json::to_vec(self) {
Ok(v) => v.len(),
Err(e) => {
tracing::warn!("failed to compute Run size: {}", e);
0
}
}
}
fn to_data(&self) -> Result<Vec<u8>, GitError> {
serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_hash_hex() -> String {
IntegrityHash::compute(b"ai-process-test").to_hex()
}
#[test]
fn test_new_objects_creation() {
let actor = ActorRef::agent("test-agent").expect("actor");
let base_hash = test_hash_hex();
let run = Run::new(actor, Uuid::from_u128(0x1), &base_hash).expect("run");
let env = run.environment().expect("environment");
assert!(!env.os.is_empty());
assert!(!env.arch.is_empty());
assert!(!env.cwd.is_empty());
}
#[test]
fn test_run_plan_and_snapshot() {
let actor = ActorRef::agent("test-agent").expect("actor");
let base_hash = test_hash_hex();
let mut run = Run::new(actor, Uuid::from_u128(0x1), &base_hash).expect("run");
let plan_id = Uuid::from_u128(0x10);
let snapshot_id = Uuid::from_u128(0x20);
run.set_plan(Some(plan_id));
run.set_snapshot(Some(snapshot_id));
assert_eq!(run.plan(), Some(plan_id));
assert_eq!(run.snapshot(), Some(snapshot_id));
}
}