use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Result;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkIdentity {
pub id: String,
pub work_type: String,
}
impl WorkIdentity {
pub fn new(id: impl Into<String>, work_type: impl Into<String>) -> Self {
Self {
id: id.into(),
work_type: work_type.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputFingerprint {
pub scalar_hash: String,
pub file_hash: String,
}
impl InputFingerprint {
pub fn new(scalar_hash: impl Into<String>, file_hash: impl Into<String>) -> Self {
Self {
scalar_hash: scalar_hash.into(),
file_hash: file_hash.into(),
}
}
pub fn combined_hash(&self) -> String {
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
self.scalar_hash.hash(&mut hasher);
self.file_hash.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
}
#[derive(Debug, Clone)]
pub struct WorkOutput {
pub success: bool,
pub output_files: Vec<PathBuf>,
pub errors: Vec<String>,
pub duration_ms: u64,
}
impl WorkOutput {
pub fn success(output_files: Vec<PathBuf>, duration_ms: u64) -> Self {
Self {
success: true,
output_files,
errors: Vec::new(),
duration_ms,
}
}
pub fn failure(errors: Vec<String>, duration_ms: u64) -> Self {
Self {
success: false,
output_files: Vec::new(),
errors,
duration_ms,
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionContext {
pub base_directory: PathBuf,
pub workspace: PathBuf,
pub properties: HashMap<String, String>,
pub incremental: bool,
}
impl ExecutionContext {
pub fn new(base_directory: PathBuf, workspace: PathBuf) -> Self {
Self {
base_directory,
workspace,
properties: HashMap::new(),
incremental: false,
}
}
pub fn with_properties(mut self, properties: HashMap<String, String>) -> Self {
self.properties = properties;
self
}
pub fn with_incremental(mut self, incremental: bool) -> Self {
self.incremental = incremental;
self
}
}
pub trait UnitOfWork: Send + Sync {
fn identify(&self) -> WorkIdentity;
fn description(&self) -> String;
fn execute(&self, context: &ExecutionContext) -> Result<WorkOutput>;
fn visit_immutable_inputs(&self, visitor: &mut dyn InputVisitor) {
let _ = visitor;
}
fn visit_mutable_inputs(&self, visitor: &mut dyn InputVisitor) {
let _ = visitor;
}
fn visit_outputs(&self, visitor: &mut dyn OutputVisitor) {
let _ = visitor;
}
fn should_disable_caching(&self) -> Option<String> {
None
}
fn timeout(&self) -> Option<std::time::Duration> {
None
}
}
pub trait InputVisitor {
fn visit_property(&mut self, name: &str, value: &str);
fn visit_file(&mut self, name: &str, path: &PathBuf);
fn visit_directory(&mut self, name: &str, path: &PathBuf);
}
pub trait OutputVisitor {
fn visit_file(&mut self, name: &str, path: &PathBuf);
fn visit_directory(&mut self, name: &str, path: &PathBuf);
}
pub struct FingerprintingInputVisitor {
properties: HashMap<String, String>,
files: Vec<(String, PathBuf)>,
}
impl FingerprintingInputVisitor {
pub fn new() -> Self {
Self {
properties: HashMap::new(),
files: Vec::new(),
}
}
pub fn compute_fingerprint(&self) -> InputFingerprint {
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
let mut scalar_hasher = DefaultHasher::new();
let mut sorted_props: Vec<_> = self.properties.iter().collect();
sorted_props.sort_by_key(|(k, _)| *k);
for (k, v) in sorted_props {
k.hash(&mut scalar_hasher);
v.hash(&mut scalar_hasher);
}
let mut file_hasher = DefaultHasher::new();
let mut sorted_files: Vec<_> = self.files.iter().collect();
sorted_files.sort_by_key(|(k, _)| k.clone());
for (name, path) in sorted_files {
name.hash(&mut file_hasher);
path.hash(&mut file_hasher);
}
InputFingerprint::new(
format!("{:x}", scalar_hasher.finish()),
format!("{:x}", file_hasher.finish()),
)
}
}
impl Default for FingerprintingInputVisitor {
fn default() -> Self {
Self::new()
}
}
impl InputVisitor for FingerprintingInputVisitor {
fn visit_property(&mut self, name: &str, value: &str) {
self.properties.insert(name.to_string(), value.to_string());
}
fn visit_file(&mut self, name: &str, path: &PathBuf) {
self.files.push((name.to_string(), path.clone()));
}
fn visit_directory(&mut self, name: &str, path: &PathBuf) {
self.files.push((name.to_string(), path.clone()));
}
}
pub struct CollectingOutputVisitor {
pub files: Vec<(String, PathBuf)>,
pub directories: Vec<(String, PathBuf)>,
}
impl CollectingOutputVisitor {
pub fn new() -> Self {
Self {
files: Vec::new(),
directories: Vec::new(),
}
}
}
impl Default for CollectingOutputVisitor {
fn default() -> Self {
Self::new()
}
}
impl OutputVisitor for CollectingOutputVisitor {
fn visit_file(&mut self, name: &str, path: &PathBuf) {
self.files.push((name.to_string(), path.clone()));
}
fn visit_directory(&mut self, name: &str, path: &PathBuf) {
self.directories.push((name.to_string(), path.clone()));
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestWork {
id: String,
}
impl UnitOfWork for TestWork {
fn identify(&self) -> WorkIdentity {
WorkIdentity::new(&self.id, "test")
}
fn description(&self) -> String {
format!("Test work: {}", self.id)
}
fn execute(&self, _context: &ExecutionContext) -> Result<WorkOutput> {
Ok(WorkOutput::success(vec![], 100))
}
fn visit_immutable_inputs(&self, visitor: &mut dyn InputVisitor) {
visitor.visit_property("id", &self.id);
}
}
#[test]
fn test_work_identity() {
let identity = WorkIdentity::new("compile-main", "compile");
assert_eq!(identity.id, "compile-main");
assert_eq!(identity.work_type, "compile");
}
#[test]
fn test_input_fingerprint() {
let fp = InputFingerprint::new("abc123", "def456");
assert!(!fp.combined_hash().is_empty());
}
#[test]
fn test_work_output_success() {
let output = WorkOutput::success(vec![PathBuf::from("out.jar")], 500);
assert!(output.success);
assert_eq!(output.output_files.len(), 1);
assert!(output.errors.is_empty());
}
#[test]
fn test_work_output_failure() {
let output = WorkOutput::failure(vec!["Compilation failed".to_string()], 100);
assert!(!output.success);
assert!(output.output_files.is_empty());
assert_eq!(output.errors.len(), 1);
}
#[test]
fn test_execution_context() {
let ctx = ExecutionContext::new(
PathBuf::from("/project"),
PathBuf::from("/project/build"),
)
.with_incremental(true);
assert!(ctx.incremental);
assert_eq!(ctx.base_directory, PathBuf::from("/project"));
}
#[test]
fn test_unit_of_work_trait() {
let work = TestWork { id: "test-1".to_string() };
let identity = work.identify();
assert_eq!(identity.id, "test-1");
assert_eq!(identity.work_type, "test");
}
#[test]
fn test_fingerprinting_visitor() {
let mut visitor = FingerprintingInputVisitor::new();
visitor.visit_property("version", "1.0.0");
visitor.visit_file("source", &PathBuf::from("src/main.java"));
let fingerprint = visitor.compute_fingerprint();
assert!(!fingerprint.scalar_hash.is_empty());
assert!(!fingerprint.file_hash.is_empty());
}
#[test]
fn test_collecting_output_visitor() {
let mut visitor = CollectingOutputVisitor::new();
visitor.visit_file("jar", &PathBuf::from("build/app.jar"));
visitor.visit_directory("classes", &PathBuf::from("build/classes"));
assert_eq!(visitor.files.len(), 1);
assert_eq!(visitor.directories.len(), 1);
}
}