oxur_repl/protocol/
messages.rs

1// Protocol message types for Oxur REPL communication
2//
3// Based on ODD-0018: Oxur Remote REPL Protocol Design
4//
5// This module defines the core message types for bidirectional
6// communication between REPL client and server.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11use crate::metadata::SystemMetadata;
12use crate::metrics::{ServerMetricsSnapshot, SessionStatsSnapshot, SubprocessMetricsSnapshot};
13
14/// Universally unique identifier for REPL sessions
15///
16/// Format: UUID v4 as string (36 characters with hyphens)
17/// Example: "550e8400-e29b-41d4-a716-446655440000"
18///
19/// # Examples
20///
21/// ```
22/// use oxur_repl::protocol::SessionId;
23///
24/// let id = SessionId::new("550e8400-e29b-41d4-a716-446655440000");
25/// assert_eq!(id.as_str(), "550e8400-e29b-41d4-a716-446655440000");
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct SessionId(String);
30
31impl SessionId {
32    /// Creates a new SessionId from any string-like value.
33    ///
34    /// # Arguments
35    ///
36    /// * `id` - The session identifier (typically a UUID)
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use oxur_repl::protocol::SessionId;
42    ///
43    /// let id1 = SessionId::new("my-session");
44    /// let id2 = SessionId::new(String::from("my-session"));
45    /// assert_eq!(id1, id2);
46    /// ```
47    pub fn new(id: impl Into<String>) -> Self {
48        Self(id.into())
49    }
50
51    /// Returns the session ID as a string slice.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use oxur_repl::protocol::SessionId;
57    ///
58    /// let id = SessionId::new("test-session");
59    /// assert_eq!(id.as_str(), "test-session");
60    /// ```
61    pub fn as_str(&self) -> &str {
62        &self.0
63    }
64
65    /// Consumes the SessionId and returns the inner String.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use oxur_repl::protocol::SessionId;
71    ///
72    /// let id = SessionId::new("test-session");
73    /// let inner: String = id.into_inner();
74    /// assert_eq!(inner, "test-session");
75    /// ```
76    pub fn into_inner(self) -> String {
77        self.0
78    }
79}
80
81impl fmt::Display for SessionId {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(f, "{}", self.0)
84    }
85}
86
87impl AsRef<str> for SessionId {
88    fn as_ref(&self) -> &str {
89        &self.0
90    }
91}
92
93impl From<String> for SessionId {
94    fn from(s: String) -> Self {
95        Self(s)
96    }
97}
98
99impl From<&str> for SessionId {
100    fn from(s: &str) -> Self {
101        Self(s.to_string())
102    }
103}
104
105/// Monotonic message correlation identifier
106///
107/// Used to match responses to requests. Must be unique per session.
108/// Typically starts at 1 and increments with each request.
109///
110/// # Examples
111///
112/// ```
113/// use oxur_repl::protocol::MessageId;
114///
115/// let id = MessageId::new(1);
116/// assert_eq!(id.as_u64(), 1);
117/// ```
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
119#[serde(transparent)]
120pub struct MessageId(u64);
121
122impl MessageId {
123    /// Creates a new MessageId from a u64 value.
124    ///
125    /// # Arguments
126    ///
127    /// * `id` - The message identifier (monotonically increasing)
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use oxur_repl::protocol::MessageId;
133    ///
134    /// let id = MessageId::new(42);
135    /// assert_eq!(id.as_u64(), 42);
136    /// ```
137    pub fn new(id: u64) -> Self {
138        Self(id)
139    }
140
141    /// Returns the message ID as a u64.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use oxur_repl::protocol::MessageId;
147    ///
148    /// let id = MessageId::new(100);
149    /// assert_eq!(id.as_u64(), 100);
150    /// ```
151    pub fn as_u64(&self) -> u64 {
152        self.0
153    }
154
155    /// Increments the message ID and returns the new value.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use oxur_repl::protocol::MessageId;
161    ///
162    /// let mut id = MessageId::new(1);
163    /// id.increment();
164    /// assert_eq!(id.as_u64(), 2);
165    /// ```
166    pub fn increment(&mut self) {
167        self.0 += 1;
168    }
169
170    /// Returns the next message ID without modifying this one.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use oxur_repl::protocol::MessageId;
176    ///
177    /// let id = MessageId::new(5);
178    /// let next = id.next();
179    /// assert_eq!(id.as_u64(), 5);
180    /// assert_eq!(next.as_u64(), 6);
181    /// ```
182    pub fn next(&self) -> Self {
183        Self(self.0 + 1)
184    }
185}
186
187impl fmt::Display for MessageId {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{}", self.0)
190    }
191}
192
193impl From<u64> for MessageId {
194    fn from(n: u64) -> Self {
195        Self(n)
196    }
197}
198
199/// Source location in Oxur code
200///
201/// Tracks byte offsets and line/column positions for error reporting.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203pub struct SourceLocation {
204    /// Byte offset from start of input
205    pub offset: usize,
206    /// Line number (1-based)
207    pub line: usize,
208    /// Column number (1-based)
209    pub column: usize,
210}
211
212impl fmt::Display for SourceLocation {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        write!(f, "line {}, column {}", self.line, self.column)
215    }
216}
217
218/// Client request to REPL server
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub struct Request {
221    /// Unique identifier for this request
222    pub id: MessageId,
223    /// Session this request belongs to
224    pub session_id: SessionId,
225    /// The operation to perform
226    pub operation: Operation,
227}
228
229/// Server response to client request
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub struct Response {
232    /// ID of request this responds to
233    pub request_id: MessageId,
234    /// Session this response belongs to
235    pub session_id: SessionId,
236    /// The result of the operation
237    pub result: OperationResult,
238}
239
240/// Operations the REPL server can perform
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[non_exhaustive]
243pub enum Operation {
244    /// Create a new session
245    CreateSession {
246        /// Evaluation mode (Lisp or Sexpr)
247        mode: ReplMode,
248    },
249
250    /// Clone an existing session
251    Clone {
252        /// ID of session to clone from
253        source_session_id: SessionId,
254    },
255
256    /// Evaluate code in the session
257    Eval {
258        /// Source code to evaluate
259        code: String,
260        /// Evaluation mode (Lisp or Sexpr)
261        mode: ReplMode,
262    },
263
264    /// Load and evaluate code from a file
265    LoadFile {
266        /// Path to file (relative to server's working directory)
267        path: String,
268        /// Evaluation mode
269        mode: ReplMode,
270    },
271
272    /// Interrupt running evaluation
273    Interrupt,
274
275    /// Close the session
276    Close,
277
278    /// List all active sessions
279    LsSessions,
280
281    /// Describe a symbol or value
282    Describe {
283        /// Symbol name to describe
284        symbol: String,
285    },
286
287    /// Get evaluation history
288    History {
289        /// Maximum number of entries to return
290        limit: Option<usize>,
291    },
292
293    /// Clear output buffer
294    ClearOutput,
295
296    /// Get server-wide statistics
297    GetServerStats,
298
299    /// Get session statistics
300    GetSessionStats,
301
302    /// Get subprocess statistics for a session
303    GetSubprocessStats,
304
305    /// Get system metadata (versions, platform info, etc.)
306    GetSystemInfo,
307}
308
309/// Result of an operation
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311#[non_exhaustive]
312pub enum OperationResult {
313    /// Operation succeeded
314    Success {
315        /// Status information
316        status: Status,
317        /// Optional result value
318        value: Option<String>,
319        /// Standard output
320        stdout: Option<String>,
321        /// Standard error
322        stderr: Option<String>,
323    },
324
325    /// Operation failed
326    Error {
327        /// Error information
328        error: ErrorInfo,
329        /// Partial output before error
330        stdout: Option<String>,
331        /// Error output
332        stderr: Option<String>,
333    },
334
335    /// Session list response
336    Sessions {
337        /// List of active sessions
338        sessions: Vec<SessionInfo>,
339    },
340
341    /// History response
342    HistoryEntries {
343        /// History entries (most recent first)
344        entries: Vec<HistoryEntry>,
345    },
346
347    /// Server statistics response
348    ServerStats {
349        /// Server metrics snapshot
350        snapshot: ServerMetricsSnapshot,
351    },
352
353    /// Session statistics response
354    SessionStats {
355        /// Session metrics snapshot
356        snapshot: SessionStatsSnapshot,
357    },
358
359    /// Subprocess statistics response
360    SubprocessStats {
361        /// Subprocess metrics snapshot
362        snapshot: SubprocessMetricsSnapshot,
363    },
364
365    /// System metadata response
366    SystemInfo {
367        /// System metadata captured at startup
368        metadata: SystemMetadata,
369    },
370}
371
372/// Evaluation mode for the REPL
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[non_exhaustive]
375pub enum ReplMode {
376    /// Lisp syntax with syntactic sugar
377    Lisp,
378    /// Raw S-expressions (canonical form)
379    Sexpr,
380}
381
382impl fmt::Display for ReplMode {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        match self {
385            ReplMode::Lisp => write!(f, "Lisp"),
386            ReplMode::Sexpr => write!(f, "Sexpr"),
387        }
388    }
389}
390
391/// Status information after successful operation
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393pub struct Status {
394    /// Execution tier used (1 = Calculator, 2 = Cached Compilation)
395    pub tier: u8,
396    /// Whether result was cached
397    pub cached: bool,
398    /// Execution time in milliseconds
399    pub duration_ms: u64,
400}
401
402/// Session information
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct SessionInfo {
405    /// Session ID
406    pub id: SessionId,
407    /// Optional session name
408    pub name: Option<String>,
409    /// Evaluation mode (Lisp or Sexpr)
410    pub mode: ReplMode,
411    /// Number of evaluations performed
412    pub eval_count: u64,
413    /// Creation timestamp (milliseconds since epoch)
414    pub created_at: u64,
415    /// Last active timestamp (milliseconds since epoch)
416    pub last_active_at: u64,
417    /// Session timeout in milliseconds
418    pub timeout_ms: u64,
419}
420
421/// Detailed error information
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423pub struct ErrorInfo {
424    /// Error classification
425    pub kind: ErrorKind,
426    /// Human-readable error message
427    pub message: String,
428    /// Source location where error occurred (if applicable)
429    pub location: Option<SourceLocation>,
430    /// Detailed backtrace or context
431    pub details: Option<String>,
432}
433
434impl fmt::Display for ErrorInfo {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        write!(f, "{}: {}", self.kind, self.message)?;
437        if let Some(loc) = &self.location {
438            write!(f, " at {}", loc)?;
439        }
440        Ok(())
441    }
442}
443
444/// Error classification for structured error handling
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
446#[non_exhaustive]
447pub enum ErrorKind {
448    /// Syntax error in source code
449    SyntaxError,
450    /// Type error during compilation
451    TypeError,
452    /// Runtime evaluation error
453    RuntimeError,
454    /// Compilation failed
455    CompilationError,
456    /// Session not found
457    SessionNotFound,
458    /// Session already exists
459    SessionAlreadyExists,
460    /// File I/O error
461    IoError,
462    /// Operation interrupted
463    Interrupted,
464    /// Invalid request
465    InvalidRequest,
466    /// Internal server error
467    InternalError,
468}
469
470impl fmt::Display for ErrorKind {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        match self {
473            ErrorKind::SyntaxError => write!(f, "Syntax Error"),
474            ErrorKind::TypeError => write!(f, "Type Error"),
475            ErrorKind::RuntimeError => write!(f, "Runtime Error"),
476            ErrorKind::CompilationError => write!(f, "Compilation Error"),
477            ErrorKind::SessionNotFound => write!(f, "Session Not Found"),
478            ErrorKind::SessionAlreadyExists => write!(f, "Session Already Exists"),
479            ErrorKind::IoError => write!(f, "I/O Error"),
480            ErrorKind::Interrupted => write!(f, "Interrupted"),
481            ErrorKind::InvalidRequest => write!(f, "Invalid Request"),
482            ErrorKind::InternalError => write!(f, "Internal Error"),
483        }
484    }
485}
486
487/// History entry from evaluation
488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
489pub struct HistoryEntry {
490    /// Entry number (monotonically increasing)
491    pub number: usize,
492    /// Code that was evaluated
493    pub code: String,
494    /// Result of evaluation (if successful)
495    pub result: Option<String>,
496    /// Timestamp in milliseconds since UNIX epoch
497    pub timestamp: u64,
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_source_location_display() {
506        let loc = SourceLocation { offset: 42, line: 3, column: 15 };
507        assert_eq!(loc.to_string(), "line 3, column 15");
508    }
509
510    #[test]
511    fn test_repl_mode_display() {
512        assert_eq!(ReplMode::Lisp.to_string(), "Lisp");
513        assert_eq!(ReplMode::Sexpr.to_string(), "Sexpr");
514    }
515
516    #[test]
517    fn test_error_kind_display() {
518        assert_eq!(ErrorKind::SyntaxError.to_string(), "Syntax Error");
519        assert_eq!(ErrorKind::RuntimeError.to_string(), "Runtime Error");
520    }
521
522    #[test]
523    fn test_error_info_display() {
524        let error = ErrorInfo {
525            kind: ErrorKind::SyntaxError,
526            message: "Unexpected token".to_string(),
527            location: Some(SourceLocation { offset: 10, line: 2, column: 5 }),
528            details: None,
529        };
530        assert_eq!(error.to_string(), "Syntax Error: Unexpected token at line 2, column 5");
531    }
532
533    #[test]
534    fn test_session_id_new() {
535        let id1 = SessionId::new("test-session");
536        let id2 = SessionId::new(String::from("test-session"));
537        assert_eq!(id1, id2);
538        assert_eq!(id1.as_str(), "test-session");
539    }
540
541    #[test]
542    fn test_session_id_display() {
543        let id = SessionId::new("my-session");
544        assert_eq!(id.to_string(), "my-session");
545    }
546
547    #[test]
548    fn test_session_id_into_inner() {
549        let id = SessionId::new("test");
550        let inner: String = id.into_inner();
551        assert_eq!(inner, "test");
552    }
553
554    #[test]
555    fn test_message_id_new() {
556        let id = MessageId::new(42);
557        assert_eq!(id.as_u64(), 42);
558    }
559
560    #[test]
561    fn test_message_id_increment() {
562        let mut id = MessageId::new(1);
563        id.increment();
564        assert_eq!(id.as_u64(), 2);
565        id.increment();
566        assert_eq!(id.as_u64(), 3);
567    }
568
569    #[test]
570    fn test_message_id_next() {
571        let id = MessageId::new(5);
572        let next = id.next();
573        assert_eq!(id.as_u64(), 5); // Original unchanged
574        assert_eq!(next.as_u64(), 6);
575    }
576
577    #[test]
578    fn test_message_id_display() {
579        let id = MessageId::new(100);
580        assert_eq!(id.to_string(), "100");
581    }
582
583    #[test]
584    fn test_request_roundtrip() {
585        let request = Request {
586            id: MessageId::new(1),
587            session_id: SessionId::new("test-session"),
588            operation: Operation::Eval { code: "(+ 1 2)".to_string(), mode: ReplMode::Lisp },
589        };
590
591        // Serialize to JSON for debugging
592        let json = serde_json::to_string(&request).unwrap();
593        let deserialized: Request = serde_json::from_str(&json).unwrap();
594        assert_eq!(request, deserialized);
595    }
596
597    #[test]
598    fn test_response_success_roundtrip() {
599        let response = Response {
600            request_id: MessageId::new(1),
601            session_id: SessionId::new("test-session"),
602            result: OperationResult::Success {
603                status: Status { tier: 1, cached: false, duration_ms: 5 },
604                value: Some("3".to_string()),
605                stdout: None,
606                stderr: None,
607            },
608        };
609
610        let json = serde_json::to_string(&response).unwrap();
611        let deserialized: Response = serde_json::from_str(&json).unwrap();
612        assert_eq!(response, deserialized);
613    }
614
615    #[test]
616    fn test_response_error_roundtrip() {
617        let response = Response {
618            request_id: MessageId::new(2),
619            session_id: SessionId::new("test-session"),
620            result: OperationResult::Error {
621                error: ErrorInfo {
622                    kind: ErrorKind::RuntimeError,
623                    message: "Division by zero".to_string(),
624                    location: None,
625                    details: None,
626                },
627                stdout: None,
628                stderr: Some("Error occurred".to_string()),
629            },
630        };
631
632        let json = serde_json::to_string(&response).unwrap();
633        let deserialized: Response = serde_json::from_str(&json).unwrap();
634        assert_eq!(response, deserialized);
635    }
636
637    #[test]
638    fn test_all_operations_roundtrip() {
639        let operations = vec![
640            Operation::CreateSession { mode: ReplMode::Lisp },
641            Operation::CreateSession { mode: ReplMode::Sexpr },
642            Operation::Eval { code: "(+ 1 2)".to_string(), mode: ReplMode::Lisp },
643            Operation::Clone { source_session_id: SessionId::new("source") },
644            Operation::LoadFile { path: "test.lisp".to_string(), mode: ReplMode::Lisp },
645            Operation::Interrupt,
646            Operation::Close,
647            Operation::LsSessions,
648            Operation::Describe { symbol: "foo".to_string() },
649            Operation::History { limit: Some(10) },
650            Operation::ClearOutput,
651        ];
652
653        for op in operations {
654            let request = Request {
655                id: MessageId::new(1),
656                session_id: SessionId::new("test"),
657                operation: op.clone(),
658            };
659
660            let json = serde_json::to_string(&request).unwrap();
661            let deserialized: Request = serde_json::from_str(&json).unwrap();
662            assert_eq!(request, deserialized);
663        }
664    }
665
666    #[test]
667    fn test_all_error_kinds_serialization() {
668        let error_kinds = vec![
669            ErrorKind::SyntaxError,
670            ErrorKind::TypeError,
671            ErrorKind::RuntimeError,
672            ErrorKind::CompilationError,
673            ErrorKind::SessionNotFound,
674            ErrorKind::SessionAlreadyExists,
675            ErrorKind::InternalError,
676        ];
677
678        for kind in error_kinds {
679            let error_info = ErrorInfo {
680                kind,
681                message: "Test error".to_string(),
682                location: Some(SourceLocation { offset: 10, line: 5, column: 10 }),
683                details: Some("Additional details".to_string()),
684            };
685
686            let json = serde_json::to_string(&error_info).unwrap();
687            let deserialized: ErrorInfo = serde_json::from_str(&json).unwrap();
688            assert_eq!(error_info, deserialized);
689        }
690    }
691
692    #[test]
693    fn test_session_info_serialization() {
694        let info = SessionInfo {
695            id: SessionId::new("test-session"),
696            name: Some("test".to_string()),
697            mode: ReplMode::Lisp,
698            eval_count: 42,
699            created_at: 1234567890,
700            last_active_at: 1234568000,
701            timeout_ms: 3600000,
702        };
703
704        let json = serde_json::to_string(&info).unwrap();
705        let deserialized: SessionInfo = serde_json::from_str(&json).unwrap();
706        assert_eq!(info, deserialized);
707    }
708
709    #[test]
710    fn test_operation_result_sessions() {
711        let result = OperationResult::Sessions {
712            sessions: vec![
713                SessionInfo {
714                    id: SessionId::new("session-1"),
715                    name: None,
716                    mode: ReplMode::Lisp,
717                    eval_count: 10,
718                    created_at: 1000,
719                    last_active_at: 1100,
720                    timeout_ms: 3600000,
721                },
722                SessionInfo {
723                    id: SessionId::new("session-2"),
724                    name: Some("work".to_string()),
725                    mode: ReplMode::Sexpr,
726                    eval_count: 5,
727                    created_at: 2000,
728                    last_active_at: 2100,
729                    timeout_ms: 3600000,
730                },
731            ],
732        };
733
734        let json = serde_json::to_string(&result).unwrap();
735        let deserialized: OperationResult = serde_json::from_str(&json).unwrap();
736        assert_eq!(result, deserialized);
737    }
738
739    #[test]
740    fn test_source_location_format() {
741        let loc = SourceLocation { offset: 42, line: 10, column: 5 };
742        assert_eq!(loc.to_string(), "line 10, column 5");
743    }
744
745    #[test]
746    fn test_repl_modes_display() {
747        assert_eq!(ReplMode::Lisp.to_string(), "Lisp");
748        assert_eq!(ReplMode::Sexpr.to_string(), "Sexpr");
749    }
750
751    #[test]
752    fn test_error_kinds_display() {
753        assert_eq!(ErrorKind::SyntaxError.to_string(), "Syntax Error");
754        assert_eq!(ErrorKind::RuntimeError.to_string(), "Runtime Error");
755        assert_eq!(ErrorKind::SessionNotFound.to_string(), "Session Not Found");
756    }
757
758    #[test]
759    fn test_error_info_with_location() {
760        let error = ErrorInfo {
761            kind: ErrorKind::SyntaxError,
762            message: "Unexpected token".to_string(),
763            location: Some(SourceLocation { offset: 15, line: 3, column: 15 }),
764            details: None,
765        };
766
767        let display = error.to_string();
768        assert!(display.contains("Syntax Error"));
769        assert!(display.contains("Unexpected token"));
770        assert!(display.contains("line 3, column 15"));
771    }
772}