Skip to main content

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}