greentic_dw_engine/
lib.rs1use greentic_dw_core::RuntimeOperation;
7use greentic_dw_types::TaskEnvelope;
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct EngineContext {
13 pub envelope: TaskEnvelope,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum EngineDecision {
19 Noop,
21 Operation(RuntimeOperation),
23 Batch(Vec<RuntimeOperation>),
25}
26
27#[derive(Debug, Error, PartialEq, Eq)]
28pub enum EngineError {
29 #[error("engine rejected empty operation batch")]
30 EmptyBatch,
31 #[error("engine returned invalid decision: {0}")]
32 InvalidDecision(String),
33}
34
35pub trait DwEngine {
37 fn decide(&self, context: &EngineContext) -> Result<EngineDecision, EngineError>;
38
39 fn decide_with_envelope(&self, envelope: &TaskEnvelope) -> Result<EngineDecision, EngineError> {
42 self.decide(&EngineContext {
43 envelope: envelope.clone(),
44 })
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct StaticEngine {
51 decision: EngineDecision,
52}
53
54impl StaticEngine {
55 pub fn new(decision: EngineDecision) -> Self {
56 Self { decision }
57 }
58}
59
60impl DwEngine for StaticEngine {
61 fn decide(&self, _context: &EngineContext) -> Result<EngineDecision, EngineError> {
62 Self::validate_decision(&self.decision)?;
63 Ok(self.decision.clone())
64 }
65
66 fn decide_with_envelope(
67 &self,
68 _envelope: &TaskEnvelope,
69 ) -> Result<EngineDecision, EngineError> {
70 Self::validate_decision(&self.decision)?;
71 Ok(self.decision.clone())
72 }
73}
74
75impl StaticEngine {
76 fn validate_decision(decision: &EngineDecision) -> Result<(), EngineError> {
77 if let EngineDecision::Batch(ops) = decision
78 && ops.is_empty()
79 {
80 return Err(EngineError::EmptyBatch);
81 }
82 Ok(())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use greentic_dw_types::{
90 LocaleContext, LocalePropagation, OutputLocaleGuidance, TaskLifecycleState, TenantScope,
91 WorkerLocalePolicy,
92 };
93
94 fn sample_envelope() -> TaskEnvelope {
95 TaskEnvelope {
96 task_id: "task-1".to_string(),
97 worker_id: "worker-1".to_string(),
98 state: TaskLifecycleState::Created,
99 scope: TenantScope {
100 tenant: "tenant-a".to_string(),
101 team: None,
102 },
103 locale: LocaleContext {
104 worker_default_locale: "en-US".to_string(),
105 requested_locale: None,
106 human_locale: None,
107 policy: WorkerLocalePolicy::WorkerDefault,
108 propagation: LocalePropagation::CurrentTaskOnly,
109 output: OutputLocaleGuidance::WorkerDefault,
110 },
111 }
112 }
113
114 #[test]
115 fn static_engine_returns_configured_operation() {
116 let engine = StaticEngine::new(EngineDecision::Operation(RuntimeOperation::Start));
117 let context = EngineContext {
118 envelope: sample_envelope(),
119 };
120
121 let decision = engine.decide(&context).expect("decision should succeed");
122 assert_eq!(decision, EngineDecision::Operation(RuntimeOperation::Start));
123 }
124
125 #[test]
126 fn static_engine_rejects_empty_batch() {
127 let engine = StaticEngine::new(EngineDecision::Batch(vec![]));
128 let context = EngineContext {
129 envelope: sample_envelope(),
130 };
131
132 let err = engine
133 .decide(&context)
134 .expect_err("empty batch should be rejected");
135 assert_eq!(err, EngineError::EmptyBatch);
136 }
137}