1use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use serde_json::Value;
12use uuid::Uuid;
13
14use crate::error::{ExecuteErrorCode, RegisterErrorCode};
15
16pub type MessageId = Uuid;
21
22#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
26pub struct CommandDef {
27 pub id: String,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub description: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub schema: Option<CommandSchema>,
32}
33
34#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
41pub struct CommandSchema {
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub request: Option<Value>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub response: Option<Value>,
46}
47
48impl CommandSchema {
49 pub fn empty() -> Self {
53 Self {
54 request: None,
55 response: None,
56 }
57 }
58
59 pub fn permissive() -> Self {
68 Self {
69 request: Some(serde_json::json!({
70 "type": "object",
71 "additionalProperties": true,
72 })),
73 response: Some(serde_json::json!({
74 "type": "object",
75 "additionalProperties": true,
76 })),
77 }
78 }
79
80 pub fn with_request(mut self, schema: Value) -> Self {
82 self.request = Some(schema);
83 self
84 }
85
86 pub fn with_response(mut self, schema: Value) -> Self {
88 self.response = Some(schema);
89 self
90 }
91}
92
93#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
95pub struct ExecuteError {
96 pub code: ExecuteErrorCode,
97 pub message: String,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub struct True;
107
108impl Serialize for True {
109 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
110 s.serialize_bool(true)
111 }
112}
113
114impl<'de> Deserialize<'de> for True {
115 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
116 match bool::deserialize(d)? {
117 true => Ok(True),
118 false => Err(serde::de::Error::custom("expected literal `true`")),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub struct False;
126
127impl Serialize for False {
128 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
129 s.serialize_bool(false)
130 }
131}
132
133impl<'de> Deserialize<'de> for False {
134 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
135 match bool::deserialize(d)? {
136 false => Ok(False),
137 true => Err(serde::de::Error::custom("expected literal `false`")),
138 }
139 }
140}
141
142#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
144#[serde(untagged)]
145pub enum RegisterResult {
146 Ok { ok: True },
147 Err { ok: False, error: RegisterErrorCode },
148}
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
152#[serde(untagged)]
153pub enum ExecuteResult {
154 Ok {
155 ok: True,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 result: Option<Value>,
158 },
159 Err {
160 ok: False,
161 error: ExecuteError,
162 },
163}
164
165#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
170#[serde(tag = "type")]
171pub enum Message {
172 #[serde(rename = "register.command.request")]
173 RegisterCommandRequest { id: MessageId, command: CommandDef },
174
175 #[serde(rename = "register.command.response")]
176 RegisterCommandResponse {
177 id: MessageId,
178 thid: MessageId,
179 response: RegisterResult,
180 },
181
182 #[serde(rename = "list.commands.request")]
183 ListCommandsRequest { id: MessageId },
184
185 #[serde(rename = "list.commands.response")]
186 ListCommandsResponse {
187 id: MessageId,
188 thid: MessageId,
189 commands: Vec<CommandDef>,
190 },
191
192 #[serde(rename = "execute.command.request")]
193 ExecuteCommandRequest {
194 id: MessageId,
195 #[serde(rename = "commandId")]
196 command_id: String,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 request: Option<Value>,
199 },
200
201 #[serde(rename = "execute.command.response")]
202 ExecuteCommandResponse {
203 id: MessageId,
204 thid: MessageId,
205 response: ExecuteResult,
206 },
207
208 #[serde(rename = "event")]
209 Event {
210 id: MessageId,
211 #[serde(rename = "eventId")]
212 event_id: String,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 payload: Option<Value>,
215 },
216}
217
218impl Message {
219 pub fn id(&self) -> MessageId {
221 match self {
222 Self::RegisterCommandRequest { id, .. }
223 | Self::RegisterCommandResponse { id, .. }
224 | Self::ListCommandsRequest { id, .. }
225 | Self::ListCommandsResponse { id, .. }
226 | Self::ExecuteCommandRequest { id, .. }
227 | Self::ExecuteCommandResponse { id, .. }
228 | Self::Event { id, .. } => *id,
229 }
230 }
231
232 pub fn thid(&self) -> Option<MessageId> {
234 match self {
235 Self::RegisterCommandResponse { thid, .. }
236 | Self::ListCommandsResponse { thid, .. }
237 | Self::ExecuteCommandResponse { thid, .. } => Some(*thid),
238 _ => None,
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 fn uid(s: &str) -> Uuid {
249 Uuid::parse_str(s).unwrap()
250 }
251
252 fn check_roundtrip(msg: &Message, expected: Value) {
256 let serialized = serde_json::to_value(msg).unwrap();
257 assert_eq!(
258 serialized, expected,
259 "serialized form does not match fixture"
260 );
261 let parsed: Message = serde_json::from_value(expected.clone()).unwrap();
262 assert_eq!(&parsed, msg, "fixture did not deserialize back to input");
263 }
264
265 #[test]
266 fn register_command_request_roundtrip() {
267 let msg = Message::RegisterCommandRequest {
268 id: uid("11111111-1111-1111-1111-111111111111"),
269 command: CommandDef {
270 id: "math.add".to_string(),
271 description: Some("Adds two numbers".to_string()),
272 schema: Some(CommandSchema {
273 request: Some(json!({
274 "type": "object",
275 "properties": { "a": { "type": "number" }, "b": { "type": "number" } },
276 "required": ["a", "b"]
277 })),
278 response: Some(json!({ "type": "number" })),
279 }),
280 },
281 };
282 check_roundtrip(
283 &msg,
284 json!({
285 "type": "register.command.request",
286 "id": "11111111-1111-1111-1111-111111111111",
287 "command": {
288 "id": "math.add",
289 "description": "Adds two numbers",
290 "schema": {
291 "request": {
292 "type": "object",
293 "properties": { "a": { "type": "number" }, "b": { "type": "number" } },
294 "required": ["a", "b"]
295 },
296 "response": { "type": "number" }
297 }
298 }
299 }),
300 );
301 }
302
303 #[test]
304 fn register_command_response_ok_roundtrip() {
305 let msg = Message::RegisterCommandResponse {
306 id: uid("22222222-2222-2222-2222-222222222222"),
307 thid: uid("11111111-1111-1111-1111-111111111111"),
308 response: RegisterResult::Ok { ok: True },
309 };
310 check_roundtrip(
311 &msg,
312 json!({
313 "type": "register.command.response",
314 "id": "22222222-2222-2222-2222-222222222222",
315 "thid": "11111111-1111-1111-1111-111111111111",
316 "response": { "ok": true }
317 }),
318 );
319 }
320
321 #[test]
322 fn register_command_response_err_roundtrip() {
323 let msg = Message::RegisterCommandResponse {
324 id: uid("22222222-2222-2222-2222-222222222222"),
325 thid: uid("11111111-1111-1111-1111-111111111111"),
326 response: RegisterResult::Err {
327 ok: False,
328 error: RegisterErrorCode::DuplicateCommand,
329 },
330 };
331 check_roundtrip(
332 &msg,
333 json!({
334 "type": "register.command.response",
335 "id": "22222222-2222-2222-2222-222222222222",
336 "thid": "11111111-1111-1111-1111-111111111111",
337 "response": { "ok": false, "error": "duplicate_command" }
338 }),
339 );
340 }
341
342 #[test]
343 fn list_commands_request_roundtrip() {
344 let msg = Message::ListCommandsRequest {
345 id: uid("33333333-3333-3333-3333-333333333333"),
346 };
347 check_roundtrip(
348 &msg,
349 json!({
350 "type": "list.commands.request",
351 "id": "33333333-3333-3333-3333-333333333333"
352 }),
353 );
354 }
355
356 #[test]
357 fn list_commands_response_roundtrip() {
358 let msg = Message::ListCommandsResponse {
359 id: uid("44444444-4444-4444-4444-444444444444"),
360 thid: uid("33333333-3333-3333-3333-333333333333"),
361 commands: vec![CommandDef {
362 id: "user.create".to_string(),
363 description: None,
364 schema: None,
365 }],
366 };
367 check_roundtrip(
368 &msg,
369 json!({
370 "type": "list.commands.response",
371 "id": "44444444-4444-4444-4444-444444444444",
372 "thid": "33333333-3333-3333-3333-333333333333",
373 "commands": [{ "id": "user.create" }]
374 }),
375 );
376 }
377
378 #[test]
379 fn execute_command_request_roundtrip() {
380 let msg = Message::ExecuteCommandRequest {
381 id: uid("55555555-5555-5555-5555-555555555555"),
382 command_id: "math.add".to_string(),
383 request: Some(json!({ "a": 1, "b": 2 })),
384 };
385 check_roundtrip(
386 &msg,
387 json!({
388 "type": "execute.command.request",
389 "id": "55555555-5555-5555-5555-555555555555",
390 "commandId": "math.add",
391 "request": { "a": 1, "b": 2 }
392 }),
393 );
394 }
395
396 #[test]
397 fn execute_command_request_no_payload() {
398 let msg = Message::ExecuteCommandRequest {
399 id: uid("55555555-5555-5555-5555-555555555555"),
400 command_id: "system.ping".to_string(),
401 request: None,
402 };
403 check_roundtrip(
404 &msg,
405 json!({
406 "type": "execute.command.request",
407 "id": "55555555-5555-5555-5555-555555555555",
408 "commandId": "system.ping"
409 }),
410 );
411 }
412
413 #[test]
414 fn execute_command_response_ok_roundtrip() {
415 let msg = Message::ExecuteCommandResponse {
416 id: uid("66666666-6666-6666-6666-666666666666"),
417 thid: uid("55555555-5555-5555-5555-555555555555"),
418 response: ExecuteResult::Ok {
419 ok: True,
420 result: Some(json!(3)),
421 },
422 };
423 check_roundtrip(
424 &msg,
425 json!({
426 "type": "execute.command.response",
427 "id": "66666666-6666-6666-6666-666666666666",
428 "thid": "55555555-5555-5555-5555-555555555555",
429 "response": { "ok": true, "result": 3 }
430 }),
431 );
432 }
433
434 #[test]
435 fn execute_command_response_err_roundtrip() {
436 let msg = Message::ExecuteCommandResponse {
437 id: uid("66666666-6666-6666-6666-666666666666"),
438 thid: uid("55555555-5555-5555-5555-555555555555"),
439 response: ExecuteResult::Err {
440 ok: False,
441 error: ExecuteError {
442 code: ExecuteErrorCode::NotFound,
443 message: "no such command".to_string(),
444 },
445 },
446 };
447 check_roundtrip(
448 &msg,
449 json!({
450 "type": "execute.command.response",
451 "id": "66666666-6666-6666-6666-666666666666",
452 "thid": "55555555-5555-5555-5555-555555555555",
453 "response": {
454 "ok": false,
455 "error": { "code": "not_found", "message": "no such command" }
456 }
457 }),
458 );
459 }
460
461 #[test]
462 fn event_roundtrip() {
463 let msg = Message::Event {
464 id: uid("77777777-7777-7777-7777-777777777777"),
465 event_id: "user.created".to_string(),
466 payload: Some(json!({ "userId": "u1" })),
467 };
468 check_roundtrip(
469 &msg,
470 json!({
471 "type": "event",
472 "id": "77777777-7777-7777-7777-777777777777",
473 "eventId": "user.created",
474 "payload": { "userId": "u1" }
475 }),
476 );
477 }
478
479 #[test]
480 fn event_private_prefix_preserved() {
481 let msg = Message::Event {
484 id: uid("77777777-7777-7777-7777-777777777777"),
485 event_id: "_internal.tick".to_string(),
486 payload: None,
487 };
488 check_roundtrip(
489 &msg,
490 json!({
491 "type": "event",
492 "id": "77777777-7777-7777-7777-777777777777",
493 "eventId": "_internal.tick"
494 }),
495 );
496 }
497
498 #[test]
499 fn unknown_type_rejected() {
500 let bad =
501 json!({ "type": "not.a.real.type", "id": "00000000-0000-0000-0000-000000000000" });
502 assert!(serde_json::from_value::<Message>(bad).is_err());
503 }
504
505 #[test]
506 fn register_result_err_requires_ok_false() {
507 let bad = json!({ "ok": true, "error": "duplicate_command" });
509 let parsed: RegisterResult = serde_json::from_value(bad).unwrap();
510 assert!(matches!(parsed, RegisterResult::Ok { .. }));
511 }
512}