orcs_event/request.rs
1//! Request/Response types for Component communication.
2//!
3//! This module defines the [`Request`] type used for synchronous
4//! queries between Components via the EventBus.
5//!
6//! # Request/Response Pattern
7//!
8//! ```text
9//! ┌─────────────┐ Request ┌─────────────┐
10//! │ Source │ ─────────► │ Target │
11//! │ Component │ │ Component │
12//! │ │ ◄───────── │ │
13//! └─────────────┘ Response └─────────────┘
14//! ```
15//!
16//! # Construction
17//!
18//! Always use [`Request::new`] to create requests. This ensures:
19//!
20//! - Unique [`RequestId`] is generated
21//! - Default timeout is applied
22//! - All required fields are provided
23//!
24//! ```
25//! use orcs_event::{Request, EventCategory};
26//! use orcs_types::{ComponentId, ChannelId};
27//! use serde_json::Value;
28//!
29//! let source = ComponentId::builtin("llm");
30//! let channel = ChannelId::new();
31//!
32//! let request = Request::new(EventCategory::Echo, "chat", source, channel, Value::Null)
33//! .with_timeout(5000);
34//! ```
35
36use crate::{EventCategory, EventError};
37use orcs_types::{ChannelId, ComponentId, RequestId, TryNew};
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40
41/// Default request timeout in milliseconds (30 seconds).
42///
43/// This value is applied automatically by [`Request::new`].
44/// Use [`Request::with_timeout`] to override.
45///
46/// # Value
47///
48/// 30,000 ms = 30 seconds
49pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
50
51/// A synchronous request message between Components.
52///
53/// Represents a query that expects exactly one `Response`.
54///
55/// # Required Fields
56///
57/// All fields except `target` are mandatory and have no sensible defaults:
58///
59/// | Field | Why No Default |
60/// |-------|----------------|
61/// | `id` | Must be unique per request (generated by [`Request::new`]) |
62/// | `operation` | Defines what action to perform |
63/// | `source` | Identifies the requesting Component |
64/// | `channel` | Execution context for permission/scope |
65/// | `payload` | Operation-specific data |
66/// | `timeout_ms` | Has default, but requires explicit construction |
67///
68/// # Why No `Default` Implementation
69///
70/// **DO NOT implement `Default` for this type.**
71///
72/// Reasons:
73///
74/// 1. **`id` must be unique**: Each request needs a fresh [`RequestId`].
75/// A `Default` impl would either reuse the same ID (broken) or
76/// generate new ones (hidden side effect in `Default`).
77///
78/// 2. **`source` is mandatory**: There is no "anonymous" request.
79/// Every request must have an identifiable source for routing
80/// and permission checking.
81///
82/// 3. **`channel` is mandatory**: Requests without a channel cannot
83/// be scoped for permission inheritance or signal targeting.
84///
85/// 4. **`operation` is mandatory**: An empty operation string is
86/// meaningless and would fail at runtime.
87///
88/// # Serde Deserialization
89///
90/// When deserializing, all fields must be present in the input.
91/// There are no `#[serde(default)]` attributes intentionally.
92/// Partial requests are rejected at deserialization time.
93///
94/// # Example
95///
96/// ```
97/// use orcs_event::{Request, EventCategory};
98/// use orcs_types::{ComponentId, ChannelId};
99/// use serde_json::json;
100///
101/// let request = Request::new(
102/// EventCategory::Echo,
103/// "read",
104/// ComponentId::builtin("tool"),
105/// ChannelId::new(),
106/// json!({ "path": "/tmp/file.txt" }),
107/// );
108///
109/// assert_eq!(request.operation, "read");
110/// assert!(request.target.is_none());
111/// ```
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Request {
114 /// Unique identifier for this request.
115 ///
116 /// Generated automatically by [`Request::new`].
117 /// Used to correlate with the corresponding `Response`.
118 pub id: RequestId,
119
120 /// Event category for subscription-based routing.
121 ///
122 /// The EventBus routes this request only to Components
123 /// that subscribe to this category.
124 pub category: EventCategory,
125
126 /// Operation name (e.g., "chat", "read", "approve").
127 ///
128 /// Determines how the target Component processes the request.
129 pub operation: String,
130
131 /// Source Component that initiated this request.
132 ///
133 /// Used for:
134 /// - Response routing
135 /// - Permission checking
136 /// - Audit logging
137 pub source: ComponentId,
138
139 /// Target Component (optional).
140 ///
141 /// - `Some(id)`: Direct request to specific Component
142 /// - `None`: Broadcast to all Components handling this operation
143 pub target: Option<ComponentId>,
144
145 /// Execution context channel.
146 ///
147 /// Used for:
148 /// - Permission scope inheritance
149 /// - Signal targeting (cancel, pause, etc.)
150 /// - Parallel execution isolation
151 pub channel: ChannelId,
152
153 /// Operation-specific payload.
154 ///
155 /// Structure depends on the `operation` type.
156 pub payload: Value,
157
158 /// Timeout in milliseconds.
159 ///
160 /// Default: [`DEFAULT_TIMEOUT_MS`] (30 seconds).
161 /// Override with [`Request::with_timeout`].
162 pub timeout_ms: u64,
163}
164
165/// Arguments for constructing a [`Request`] via [`TryNew`].
166///
167/// This struct provides validated request construction.
168///
169/// # Example
170///
171/// ```
172/// use orcs_event::{Request, RequestArgs, EventCategory};
173/// use orcs_types::{ComponentId, ChannelId, TryNew};
174/// use serde_json::json;
175///
176/// let args = RequestArgs {
177/// category: EventCategory::Echo,
178/// operation: "echo".to_string(),
179/// source: ComponentId::builtin("test"),
180/// channel: ChannelId::new(),
181/// payload: json!({ "message": "Hello" }),
182/// };
183///
184/// let request = Request::try_new(args).expect("valid request");
185/// ```
186pub struct RequestArgs {
187 /// Event category for routing.
188 pub category: EventCategory,
189 /// Operation name (must not be empty).
190 pub operation: String,
191 /// Source Component.
192 pub source: ComponentId,
193 /// Execution context channel.
194 pub channel: ChannelId,
195 /// Operation-specific payload.
196 pub payload: Value,
197}
198
199impl TryNew for Request {
200 type Error = EventError;
201 type Args = RequestArgs;
202
203 /// Attempts to create a new [`Request`] with validation.
204 ///
205 /// # Validation
206 ///
207 /// - `operation` must not be empty
208 /// - `operation` must not be whitespace-only
209 ///
210 /// # Errors
211 ///
212 /// Returns [`EventError::InvalidRequest`] if validation fails.
213 ///
214 /// # Example
215 ///
216 /// ```
217 /// use orcs_event::{Request, RequestArgs, EventError, EventCategory};
218 /// use orcs_types::{ComponentId, ChannelId, TryNew, ErrorCode};
219 /// use serde_json::Value;
220 ///
221 /// // Valid request
222 /// let args = RequestArgs {
223 /// category: EventCategory::Echo,
224 /// operation: "echo".to_string(),
225 /// source: ComponentId::builtin("echo"),
226 /// channel: ChannelId::new(),
227 /// payload: Value::Null,
228 /// };
229 /// assert!(Request::try_new(args).is_ok());
230 ///
231 /// // Invalid: empty operation
232 /// let args = RequestArgs {
233 /// category: EventCategory::Echo,
234 /// operation: String::new(),
235 /// source: ComponentId::builtin("echo"),
236 /// channel: ChannelId::new(),
237 /// payload: Value::Null,
238 /// };
239 /// let err = Request::try_new(args).unwrap_err();
240 /// assert_eq!(err.code(), "EVENT_INVALID_REQUEST");
241 /// ```
242 fn try_new(args: Self::Args) -> Result<Self, Self::Error> {
243 let operation = args.operation.trim();
244 if operation.is_empty() {
245 return Err(EventError::InvalidRequest(
246 "operation cannot be empty".to_string(),
247 ));
248 }
249
250 Ok(Self {
251 id: RequestId::new(),
252 category: args.category,
253 operation: operation.to_string(),
254 source: args.source,
255 target: None,
256 channel: args.channel,
257 payload: args.payload,
258 timeout_ms: DEFAULT_TIMEOUT_MS,
259 })
260 }
261}
262
263impl Request {
264 /// Creates a new [`Request`] with a unique ID and default timeout.
265 ///
266 /// **Note**: This method does not validate the operation string.
267 /// For validated construction, use [`Request::try_new`] via the
268 /// [`TryNew`] trait.
269 ///
270 /// # Arguments
271 ///
272 /// * `category` - Event category for routing
273 /// * `operation` - Operation name (e.g., "chat", "read")
274 /// * `source` - The Component initiating this request
275 /// * `channel` - Execution context for scoping
276 /// * `payload` - Operation-specific data
277 ///
278 /// # Example
279 ///
280 /// ```
281 /// use orcs_event::{Request, EventCategory};
282 /// use orcs_types::{ComponentId, ChannelId};
283 /// use serde_json::json;
284 ///
285 /// let req = Request::new(
286 /// EventCategory::Echo,
287 /// "echo",
288 /// ComponentId::builtin("test"),
289 /// ChannelId::new(),
290 /// json!({ "message": "Hello" }),
291 /// );
292 /// ```
293 #[must_use]
294 pub fn new(
295 category: EventCategory,
296 operation: impl Into<String>,
297 source: ComponentId,
298 channel: ChannelId,
299 payload: Value,
300 ) -> Self {
301 Self {
302 id: RequestId::new(),
303 category,
304 operation: operation.into(),
305 source,
306 target: None,
307 channel,
308 payload,
309 timeout_ms: DEFAULT_TIMEOUT_MS,
310 }
311 }
312
313 /// Sets the target Component for direct routing.
314 ///
315 /// By default, requests are broadcast to all Components that
316 /// handle the operation. Use this to send directly to one.
317 ///
318 /// # Example
319 ///
320 /// ```
321 /// use orcs_event::{Request, EventCategory};
322 /// use orcs_types::{ComponentId, ChannelId};
323 /// use serde_json::Value;
324 ///
325 /// let req = Request::new(
326 /// EventCategory::Hil,
327 /// "approve",
328 /// ComponentId::builtin("tool"),
329 /// ChannelId::new(),
330 /// Value::Null,
331 /// )
332 /// .with_target(ComponentId::builtin("hil"));
333 ///
334 /// assert!(req.target.is_some());
335 /// ```
336 #[must_use]
337 pub fn with_target(mut self, target: ComponentId) -> Self {
338 self.target = Some(target);
339 self
340 }
341
342 /// Sets a custom timeout in milliseconds.
343 ///
344 /// Default is [`DEFAULT_TIMEOUT_MS`] (30 seconds).
345 ///
346 /// # Example
347 ///
348 /// ```
349 /// use orcs_event::{Request, EventCategory};
350 /// use orcs_types::{ComponentId, ChannelId};
351 /// use serde_json::Value;
352 ///
353 /// // Short timeout for quick operations
354 /// let req = Request::new(
355 /// EventCategory::Lifecycle,
356 /// "ping",
357 /// ComponentId::builtin("monitor"),
358 /// ChannelId::new(),
359 /// Value::Null,
360 /// )
361 /// .with_timeout(1000); // 1 second
362 ///
363 /// assert_eq!(req.timeout_ms, 1000);
364 /// ```
365 #[must_use]
366 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
367 self.timeout_ms = timeout_ms;
368 self
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn request_creation() {
378 let source = ComponentId::builtin("test");
379 let channel = ChannelId::new();
380 let req = Request::new(
381 EventCategory::Echo,
382 "echo",
383 source.clone(),
384 channel,
385 Value::String("hello".into()),
386 );
387
388 assert_eq!(req.category, EventCategory::Echo);
389 assert_eq!(req.operation, "echo");
390 assert_eq!(req.source, source);
391 assert!(req.target.is_none());
392 assert_eq!(req.timeout_ms, DEFAULT_TIMEOUT_MS);
393 }
394
395 #[test]
396 fn request_with_target() {
397 let source = ComponentId::builtin("source");
398 let target = ComponentId::builtin("target");
399 let channel = ChannelId::new();
400 let req = Request::new(EventCategory::Hil, "submit", source, channel, Value::Null)
401 .with_target(target.clone());
402
403 assert_eq!(req.target, Some(target));
404 }
405
406 #[test]
407 fn request_unique_ids() {
408 let source = ComponentId::builtin("test");
409 let channel = ChannelId::new();
410 let req1 = Request::new(
411 EventCategory::Echo,
412 "op1",
413 source.clone(),
414 channel,
415 Value::Null,
416 );
417 let req2 = Request::new(EventCategory::Echo, "op2", source, channel, Value::Null);
418
419 assert_ne!(req1.id, req2.id);
420 }
421
422 #[test]
423 fn request_with_custom_timeout() {
424 let source = ComponentId::builtin("test");
425 let channel = ChannelId::new();
426 let req = Request::new(EventCategory::Echo, "op", source, channel, Value::Null)
427 .with_timeout(5000);
428
429 assert_eq!(req.timeout_ms, 5000);
430 }
431
432 #[test]
433 fn try_new_valid_request() {
434 let args = RequestArgs {
435 category: EventCategory::Echo,
436 operation: "echo".to_string(),
437 source: ComponentId::builtin("echo"),
438 channel: ChannelId::new(),
439 payload: Value::Null,
440 };
441
442 let result = Request::try_new(args);
443 assert!(result.is_ok());
444
445 let req = result.expect("try_new with valid 'echo' operation should succeed");
446 assert_eq!(req.category, EventCategory::Echo);
447 assert_eq!(req.operation, "echo");
448 }
449
450 #[test]
451 fn try_new_empty_operation_fails() {
452 use orcs_types::ErrorCode;
453
454 let args = RequestArgs {
455 category: EventCategory::Echo,
456 operation: String::new(),
457 source: ComponentId::builtin("echo"),
458 channel: ChannelId::new(),
459 payload: Value::Null,
460 };
461
462 let result = Request::try_new(args);
463 assert!(result.is_err());
464
465 let err = result.expect_err("try_new with empty operation should return error");
466 assert_eq!(err.code(), "EVENT_INVALID_REQUEST");
467 }
468
469 #[test]
470 fn try_new_whitespace_only_operation_fails() {
471 use orcs_types::ErrorCode;
472
473 let args = RequestArgs {
474 category: EventCategory::Echo,
475 operation: " ".to_string(),
476 source: ComponentId::builtin("echo"),
477 channel: ChannelId::new(),
478 payload: Value::Null,
479 };
480
481 let result = Request::try_new(args);
482 assert!(result.is_err());
483
484 let err = result.expect_err("try_new with whitespace-only operation should return error");
485 assert_eq!(err.code(), "EVENT_INVALID_REQUEST");
486 }
487
488 #[test]
489 fn try_new_trims_operation() {
490 let args = RequestArgs {
491 category: EventCategory::Echo,
492 operation: " echo ".to_string(),
493 source: ComponentId::builtin("echo"),
494 channel: ChannelId::new(),
495 payload: Value::Null,
496 };
497
498 let result = Request::try_new(args);
499 assert!(result.is_ok());
500
501 let req = result.expect(
502 "try_new with whitespace-padded 'echo' operation should succeed after trimming",
503 );
504 assert_eq!(req.operation, "echo");
505 }
506
507 #[test]
508 fn request_category_routing() {
509 let source = ComponentId::builtin("test");
510 let channel = ChannelId::new();
511
512 let hil_req = Request::new(
513 EventCategory::Hil,
514 "submit",
515 source.clone(),
516 channel,
517 Value::Null,
518 );
519 let echo_req = Request::new(EventCategory::Echo, "echo", source, channel, Value::Null);
520
521 assert!(hil_req.category.is_hil());
522 assert!(!echo_req.category.is_hil());
523 }
524}