evidentsource_core/domain/state_change.rs
1//! State change types.
2//!
3//! State changes are commands that produce events. They encapsulate
4//! business logic for validating and processing requests.
5
6use super::identifiers::StateChangeName;
7
8/// State change version number.
9pub type StateChangeVersion = u64;
10
11/// A summary of a state change definition.
12///
13/// This is returned when listing state changes registered with the database.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct StateChangeDefinitionSummary {
16 /// The name of the state change.
17 pub name: StateChangeName,
18 /// The version of the state change.
19 pub version: StateChangeVersion,
20}
21
22impl StateChangeDefinitionSummary {
23 /// Create a new state change definition summary.
24 pub fn new(name: StateChangeName, version: StateChangeVersion) -> Self {
25 Self { name, version }
26 }
27}
28
29/// A command received by a state change.
30///
31/// This mirrors the WIT Command type and can be used by domain crates
32/// to implement TryFrom conversions without depending on the SDK.
33///
34/// The SDK is format-agnostic: users implement `TryFrom<Command>` with their
35/// preferred deserializer (serde_json, prost, rmp-serde, etc.).
36///
37/// # Example
38///
39/// ```rust,ignore
40/// use evidentsource_core::domain::{Command, CommandParseError};
41///
42/// impl TryFrom<Command> for MyDomainCommand {
43/// type Error = CommandParseError;
44///
45/// fn try_from(cmd: Command) -> Result<Self, Self::Error> {
46/// let bytes = cmd.body.as_deref().ok_or(CommandParseError::NoBody)?;
47/// // Use your preferred deserializer
48/// serde_json::from_slice(bytes)
49/// .map_err(|e| CommandParseError::DeserializationFailed(e.to_string()))
50/// }
51/// }
52/// ```
53#[derive(Debug, Clone)]
54pub struct Command {
55 /// Optional request body as raw bytes.
56 pub body: Option<Vec<u8>>,
57 /// Content type of the body (e.g., "application/json").
58 pub content_type: String,
59 /// Optional content schema URI.
60 pub content_schema: Option<String>,
61}
62
63impl Command {
64 /// Create a new command with the given body and content type.
65 pub fn new(body: Option<Vec<u8>>, content_type: impl Into<String>) -> Self {
66 Command {
67 body,
68 content_type: content_type.into(),
69 content_schema: None,
70 }
71 }
72
73 /// Create a command with a JSON body.
74 pub fn json(body: Vec<u8>) -> Self {
75 Command {
76 body: Some(body),
77 content_type: "application/json".to_string(),
78 content_schema: None,
79 }
80 }
81
82 /// Set the content schema.
83 pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
84 self.content_schema = Some(schema.into());
85 self
86 }
87
88 /// Get a reference to the body bytes if present.
89 pub fn body(&self) -> Option<&[u8]> {
90 self.body.as_deref()
91 }
92
93 /// Get the content type.
94 pub fn content_type(&self) -> &str {
95 &self.content_type
96 }
97
98 /// Get the content schema if present.
99 pub fn content_schema(&self) -> Option<&str> {
100 self.content_schema.as_deref()
101 }
102
103 /// Check if the content type is JSON.
104 pub fn is_json(&self) -> bool {
105 self.content_type == "application/json" || self.content_type.ends_with("+json")
106 }
107}
108
109/// Error when parsing a command body.
110#[derive(Debug, Clone, thiserror::Error)]
111pub enum CommandParseError {
112 /// Command body is required but was not provided.
113 #[error("command body is required")]
114 NoBody,
115 /// Failed to deserialize the command body.
116 #[error("deserialization failed: {0}")]
117 DeserializationFailed(String),
118}
119
120/// Error when creating a command request.
121#[derive(Debug, Clone, thiserror::Error)]
122pub enum CommandRequestError {
123 /// Failed to serialize the request body.
124 #[error("serialization failed: {0}")]
125 SerializationFailed(String),
126}
127
128/// A command request to execute a state change.
129#[derive(Debug, Clone)]
130pub struct CommandRequest {
131 /// HTTP-style headers for the request.
132 pub headers: Vec<(String, String)>,
133 /// Optional request body.
134 pub body: Option<Vec<u8>>,
135 /// Content type of the body (e.g., "application/json").
136 pub content_type: Option<String>,
137 /// Optional content schema URL for validation.
138 pub content_schema: Option<String>,
139}
140
141impl CommandRequest {
142 /// Create a new empty command request.
143 pub fn new() -> Self {
144 CommandRequest {
145 headers: Vec::new(),
146 body: None,
147 content_type: None,
148 content_schema: None,
149 }
150 }
151
152 /// Create a command request with a body.
153 pub fn with_body(body: Vec<u8>) -> Self {
154 CommandRequest {
155 headers: Vec::new(),
156 body: Some(body),
157 content_type: None,
158 content_schema: None,
159 }
160 }
161
162 /// Create a command request with a JSON-serialized body.
163 ///
164 /// This serializes the value to JSON and sets the appropriate Content-Type header.
165 ///
166 /// # Example
167 ///
168 /// ```
169 /// use evidentsource_core::domain::CommandRequest;
170 /// use serde::Serialize;
171 ///
172 /// #[derive(Serialize)]
173 /// struct CreateAccount {
174 /// account_id: String,
175 /// initial_balance: u64,
176 /// }
177 ///
178 /// let cmd = CreateAccount {
179 /// account_id: "acc-123".to_string(),
180 /// initial_balance: 1000,
181 /// };
182 ///
183 /// let request = CommandRequest::json(&cmd).unwrap();
184 /// assert_eq!(request.get_header("Content-Type"), Some("application/json"));
185 /// ```
186 pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, CommandRequestError> {
187 let body = serde_json::to_vec(value)
188 .map_err(|e| CommandRequestError::SerializationFailed(e.to_string()))?;
189 Ok(CommandRequest {
190 headers: vec![("Content-Type".to_string(), "application/json".to_string())],
191 body: Some(body),
192 content_type: Some("application/json".to_string()),
193 content_schema: None,
194 })
195 }
196
197 /// Add a header to the request.
198 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
199 self.headers.push((name.into(), value.into()));
200 self
201 }
202
203 /// Set the request body.
204 pub fn body(mut self, body: Vec<u8>) -> Self {
205 self.body = Some(body);
206 self
207 }
208
209 /// Set the content type.
210 ///
211 /// # Example
212 ///
213 /// ```
214 /// use evidentsource_core::domain::CommandRequest;
215 ///
216 /// let request = CommandRequest::new()
217 /// .body(b"<xml>data</xml>".to_vec())
218 /// .content_type("application/xml");
219 /// ```
220 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
221 self.content_type = Some(content_type.into());
222 self
223 }
224
225 /// Set the content schema URL.
226 ///
227 /// # Example
228 ///
229 /// ```
230 /// use evidentsource_core::domain::CommandRequest;
231 ///
232 /// let request = CommandRequest::new()
233 /// .body(b"{}".to_vec())
234 /// .content_type("application/json")
235 /// .content_schema("https://example.com/schemas/command.json");
236 /// ```
237 pub fn content_schema(mut self, schema_url: impl Into<String>) -> Self {
238 self.content_schema = Some(schema_url.into());
239 self
240 }
241
242 /// Get a header value by name.
243 pub fn get_header(&self, name: &str) -> Option<&str> {
244 self.headers
245 .iter()
246 .find(|(k, _)| k.eq_ignore_ascii_case(name))
247 .map(|(_, v)| v.as_str())
248 }
249}
250
251impl Default for CommandRequest {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use serde::Serialize;
261
262 #[test]
263 fn test_command_request_builder() {
264 let request = CommandRequest::new()
265 .header("Content-Type", "application/json")
266 .body(b"{}".to_vec());
267
268 assert_eq!(request.get_header("content-type"), Some("application/json"));
269 assert_eq!(request.body, Some(b"{}".to_vec()));
270 }
271
272 #[test]
273 fn test_command_request_json() {
274 #[derive(Serialize)]
275 struct TestCmd {
276 name: String,
277 value: i32,
278 }
279
280 let cmd = TestCmd {
281 name: "test".to_string(),
282 value: 42,
283 };
284
285 let request = CommandRequest::json(&cmd).unwrap();
286
287 assert_eq!(request.get_header("Content-Type"), Some("application/json"));
288 assert!(request.body.is_some());
289
290 // Verify the JSON is valid
291 let body = request.body.unwrap();
292 let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
293 assert_eq!(parsed["name"], "test");
294 assert_eq!(parsed["value"], 42);
295 }
296
297 #[test]
298 fn test_state_change_definition_summary() {
299 let name = StateChangeName::new("my-state-change").unwrap();
300 let summary = StateChangeDefinitionSummary::new(name.clone(), 1);
301
302 assert_eq!(summary.name, name);
303 assert_eq!(summary.version, 1);
304 }
305}