use greentic_dw_core::RuntimeOperation;
use greentic_dw_types::TaskEnvelope;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EngineContext {
pub envelope: TaskEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EngineDecision {
Noop,
Operation(RuntimeOperation),
Batch(Vec<RuntimeOperation>),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum EngineError {
#[error("engine rejected empty operation batch")]
EmptyBatch,
#[error("engine returned invalid decision: {0}")]
InvalidDecision(String),
}
pub trait DwEngine {
fn decide(&self, context: &EngineContext) -> Result<EngineDecision, EngineError>;
fn decide_with_envelope(&self, envelope: &TaskEnvelope) -> Result<EngineDecision, EngineError> {
self.decide(&EngineContext {
envelope: envelope.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct StaticEngine {
decision: EngineDecision,
}
impl StaticEngine {
pub fn new(decision: EngineDecision) -> Self {
Self { decision }
}
}
impl DwEngine for StaticEngine {
fn decide(&self, _context: &EngineContext) -> Result<EngineDecision, EngineError> {
Self::validate_decision(&self.decision)?;
Ok(self.decision.clone())
}
fn decide_with_envelope(
&self,
_envelope: &TaskEnvelope,
) -> Result<EngineDecision, EngineError> {
Self::validate_decision(&self.decision)?;
Ok(self.decision.clone())
}
}
impl StaticEngine {
fn validate_decision(decision: &EngineDecision) -> Result<(), EngineError> {
if let EngineDecision::Batch(ops) = decision
&& ops.is_empty()
{
return Err(EngineError::EmptyBatch);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use greentic_dw_types::{
LocaleContext, LocalePropagation, OutputLocaleGuidance, TaskLifecycleState, TenantScope,
WorkerLocalePolicy,
};
fn sample_envelope() -> TaskEnvelope {
TaskEnvelope {
task_id: "task-1".to_string(),
worker_id: "worker-1".to_string(),
state: TaskLifecycleState::Created,
scope: TenantScope {
tenant: "tenant-a".to_string(),
team: None,
},
locale: LocaleContext {
worker_default_locale: "en-US".to_string(),
requested_locale: None,
human_locale: None,
policy: WorkerLocalePolicy::WorkerDefault,
propagation: LocalePropagation::CurrentTaskOnly,
output: OutputLocaleGuidance::WorkerDefault,
},
}
}
#[test]
fn static_engine_returns_configured_operation() {
let engine = StaticEngine::new(EngineDecision::Operation(RuntimeOperation::Start));
let context = EngineContext {
envelope: sample_envelope(),
};
let decision = engine.decide(&context).expect("decision should succeed");
assert_eq!(decision, EngineDecision::Operation(RuntimeOperation::Start));
}
#[test]
fn static_engine_rejects_empty_batch() {
let engine = StaticEngine::new(EngineDecision::Batch(vec![]));
let context = EngineContext {
envelope: sample_envelope(),
};
let err = engine
.decide(&context)
.expect_err("empty batch should be rejected");
assert_eq!(err, EngineError::EmptyBatch);
}
}