Skip to main content

awaken_contract/state/
command.rs

1use std::ops::{Deref, DerefMut};
2
3use crate::StateError;
4use crate::model::{EffectSpec, ScheduledAction, ScheduledActionSpec, TypedEffect};
5
6use super::{MergeStrategy, MutationBatch};
7
8/// A command that carries state mutations, scheduled actions, and effects.
9///
10/// This is the primary mechanism for both plugin hooks and tools to express
11/// side-effects. Plugins return `StateCommand` from phase hooks; tools return
12/// it alongside `ToolResult` to declare their side-effects using the same
13/// machinery.
14#[derive(Debug)]
15pub struct StateCommand {
16    pub patch: MutationBatch,
17    pub scheduled_actions: Vec<ScheduledAction>,
18    pub effects: Vec<TypedEffect>,
19}
20
21impl StateCommand {
22    pub fn new() -> Self {
23        Self {
24            patch: MutationBatch::new(),
25            scheduled_actions: Vec::new(),
26            effects: Vec::new(),
27        }
28    }
29
30    pub fn with_base_revision(mut self, revision: u64) -> Self {
31        self.patch = self.patch.with_base_revision(revision);
32        self
33    }
34
35    pub fn is_empty(&self) -> bool {
36        self.patch.is_empty() && self.scheduled_actions.is_empty() && self.effects.is_empty()
37    }
38
39    /// Inspect scheduled actions (useful for testing).
40    pub fn scheduled_actions(&self) -> &[ScheduledAction] {
41        &self.scheduled_actions
42    }
43
44    pub fn emit<E: EffectSpec>(&mut self, payload: E::Payload) -> Result<(), StateError> {
45        self.effects.push(TypedEffect::from_spec::<E>(&payload)?);
46        Ok(())
47    }
48
49    pub fn schedule_action<A: ScheduledActionSpec>(
50        &mut self,
51        payload: A::Payload,
52    ) -> Result<(), StateError> {
53        self.scheduled_actions.push(ScheduledAction::new(
54            A::PHASE,
55            A::KEY,
56            A::encode_payload(&payload)?,
57        ));
58        Ok(())
59    }
60
61    pub fn extend(&mut self, mut other: Self) -> Result<(), StateError> {
62        self.patch.extend(other.patch)?;
63        self.scheduled_actions.append(&mut other.scheduled_actions);
64        self.effects.append(&mut other.effects);
65        Ok(())
66    }
67
68    /// Merge two commands from parallel execution using the given merge strategy.
69    pub fn merge_parallel<F>(self, other: Self, strategy: F) -> Result<Self, StateError>
70    where
71        F: Fn(&str) -> MergeStrategy,
72    {
73        let patch = self.patch.merge_parallel(other.patch, strategy)?;
74        let mut scheduled_actions = self.scheduled_actions;
75        scheduled_actions.extend(other.scheduled_actions);
76        let mut effects = self.effects;
77        effects.extend(other.effects);
78        Ok(Self {
79            patch,
80            scheduled_actions,
81            effects,
82        })
83    }
84}
85
86impl Default for StateCommand {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl Deref for StateCommand {
93    type Target = MutationBatch;
94
95    fn deref(&self) -> &Self::Target {
96        &self.patch
97    }
98}
99
100impl DerefMut for StateCommand {
101    fn deref_mut(&mut self) -> &mut Self::Target {
102        &mut self.patch
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use crate::model::{EffectSpec, Phase, ScheduledActionSpec};
109
110    use super::*;
111
112    struct TestAction;
113
114    impl ScheduledActionSpec for TestAction {
115        const KEY: &'static str = "test.action";
116        const PHASE: Phase = Phase::RunStart;
117        type Payload = String;
118    }
119
120    struct CustomEffect;
121
122    impl EffectSpec for CustomEffect {
123        const KEY: &'static str = "test.custom_effect";
124        type Payload = String;
125    }
126
127    #[test]
128    fn state_command_accumulates_actions_and_effects() {
129        let mut command = StateCommand::new();
130        command
131            .schedule_action::<TestAction>("go".into())
132            .expect("schedule should succeed");
133        command
134            .emit::<CustomEffect>("payload".into())
135            .expect("effect should encode");
136
137        assert!(!command.is_empty());
138        assert_eq!(command.scheduled_actions.len(), 1);
139        assert_eq!(command.effects.len(), 1);
140    }
141
142    #[test]
143    fn state_command_extend_merges_all() {
144        let mut left = StateCommand::new();
145        left.schedule_action::<TestAction>("left".into()).unwrap();
146
147        let mut right = StateCommand::new();
148        right.emit::<CustomEffect>("effect".into()).unwrap();
149
150        left.extend(right).unwrap();
151        assert_eq!(left.scheduled_actions.len(), 1);
152        assert_eq!(left.effects.len(), 1);
153    }
154
155    #[test]
156    fn state_command_new_is_empty() {
157        let cmd = StateCommand::new();
158        assert!(cmd.is_empty());
159    }
160
161    #[test]
162    fn state_command_merge_parallel_combines_all_fields() {
163        let mut left = StateCommand::new();
164        left.schedule_action::<TestAction>("left_action".into())
165            .unwrap();
166        left.emit::<CustomEffect>("left_effect".into()).unwrap();
167
168        let mut right = StateCommand::new();
169        right
170            .schedule_action::<TestAction>("right_action".into())
171            .unwrap();
172        right.emit::<CustomEffect>("right_effect".into()).unwrap();
173
174        let merged = left
175            .merge_parallel(right, |_| MergeStrategy::Commutative)
176            .unwrap();
177        assert_eq!(merged.scheduled_actions.len(), 2);
178        assert_eq!(merged.effects.len(), 2);
179    }
180}