Skip to main content

cu29_traits/
lib.rs

1//! Common copper traits and types for robotics systems.
2//!
3//! This crate is no_std compatible by default. Enable the "std" feature for additional
4//! functionality like implementing `std::error::Error` for `CuError` and the
5//! `new_with_cause` method that accepts types implementing `std::error::Error`.
6//!
7//! # Features
8//!
9//! - `std` (default): Enables standard library support
10//!   - Implements `std::error::Error` for `CuError`
11//!   - Adds `CuError::new_with_cause()` method for interop with std error types
12//!
13//! # no_std Usage
14//!
15//! To use without the standard library:
16//!
17//! ```toml
18//! [dependencies]
19//! cu29-traits = { version = "0.9", default-features = false }
20//! ```
21
22#![cfg_attr(not(feature = "std"), no_std)]
23extern crate alloc;
24
25#[cfg(feature = "reflect")]
26pub use bevy_reflect::Reflect;
27use bincode::de::{BorrowDecoder, Decoder};
28use bincode::enc::Encoder;
29use bincode::error::{DecodeError, EncodeError};
30use bincode::{BorrowDecode, Decode as dDecode, Decode, Encode, Encode as dEncode};
31use compact_str::CompactString;
32use cu29_clock::{PartialCuTimeRange, Tov};
33use serde::{Deserialize, Serialize};
34
35use alloc::boxed::Box;
36use alloc::format;
37use alloc::string::{String, ToString};
38use alloc::vec::Vec;
39#[cfg(not(feature = "std"))]
40use core::error::Error as CoreError;
41use core::fmt::{Debug, Display, Formatter};
42#[cfg(feature = "std")]
43use std::error::Error;
44
45// Type alias for the boxed error type to simplify conditional compilation
46#[cfg(feature = "std")]
47type DynError = dyn std::error::Error + Send + Sync + 'static;
48#[cfg(not(feature = "std"))]
49type DynError = dyn core::error::Error + Send + Sync + 'static;
50
51/// A simple wrapper around String that implements Error trait.
52/// Used for cloning and deserializing CuError causes.
53#[derive(Debug)]
54struct StringError(String);
55
56impl Display for StringError {
57    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
58        write!(f, "{}", self.0)
59    }
60}
61
62#[cfg(feature = "std")]
63impl std::error::Error for StringError {}
64
65#[cfg(not(feature = "std"))]
66impl core::error::Error for StringError {}
67
68/// Common copper Error type.
69///
70/// This error type stores an optional cause as a boxed dynamic error,
71/// allowing for proper error chaining while maintaining Clone and
72/// Serialize/Deserialize support through custom implementations.
73pub struct CuError {
74    message: String,
75    cause: Option<Box<DynError>>,
76}
77
78// Custom Debug implementation that formats cause as string
79impl Debug for CuError {
80    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
81        f.debug_struct("CuError")
82            .field("message", &self.message)
83            .field("cause", &self.cause.as_ref().map(|e| e.to_string()))
84            .finish()
85    }
86}
87
88// Custom Clone implementation - clones cause as StringError wrapper
89impl Clone for CuError {
90    fn clone(&self) -> Self {
91        CuError {
92            message: self.message.clone(),
93            cause: self
94                .cause
95                .as_ref()
96                .map(|e| Box::new(StringError(e.to_string())) as Box<DynError>),
97        }
98    }
99}
100
101// Custom Serialize - serializes cause as Option<String>
102impl Serialize for CuError {
103    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104    where
105        S: serde::Serializer,
106    {
107        use serde::ser::SerializeStruct;
108        let mut state = serializer.serialize_struct("CuError", 2)?;
109        state.serialize_field("message", &self.message)?;
110        state.serialize_field("cause", &self.cause.as_ref().map(|e| e.to_string()))?;
111        state.end()
112    }
113}
114
115// Custom Deserialize - deserializes cause as StringError wrapper
116impl<'de> Deserialize<'de> for CuError {
117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118    where
119        D: serde::Deserializer<'de>,
120    {
121        #[derive(Deserialize)]
122        struct CuErrorHelper {
123            message: String,
124            cause: Option<String>,
125        }
126
127        let helper = CuErrorHelper::deserialize(deserializer)?;
128        Ok(CuError {
129            message: helper.message,
130            cause: helper
131                .cause
132                .map(|s| Box::new(StringError(s)) as Box<DynError>),
133        })
134    }
135}
136
137impl Display for CuError {
138    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
139        let context_str = match &self.cause {
140            Some(c) => c.to_string(),
141            None => "None".to_string(),
142        };
143        write!(f, "{}\n   context:{}", self.message, context_str)?;
144        Ok(())
145    }
146}
147
148#[cfg(not(feature = "std"))]
149impl CoreError for CuError {
150    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
151        self.cause
152            .as_deref()
153            .map(|e| e as &(dyn CoreError + 'static))
154    }
155}
156
157#[cfg(feature = "std")]
158impl Error for CuError {
159    fn source(&self) -> Option<&(dyn Error + 'static)> {
160        self.cause.as_deref().map(|e| e as &(dyn Error + 'static))
161    }
162}
163
164impl From<&str> for CuError {
165    fn from(s: &str) -> CuError {
166        CuError {
167            message: s.to_string(),
168            cause: None,
169        }
170    }
171}
172
173impl From<String> for CuError {
174    fn from(s: String) -> CuError {
175        CuError {
176            message: s,
177            cause: None,
178        }
179    }
180}
181
182impl CuError {
183    /// Creates a new CuError from an interned string index.
184    /// Used by the cu_error! macro.
185    ///
186    /// The index is stored as a placeholder string `[interned:{index}]`.
187    /// Actual string resolution happens at logging time via the unified logger.
188    pub fn new(message_index: usize) -> CuError {
189        CuError {
190            message: format!("[interned:{}]", message_index),
191            cause: None,
192        }
193    }
194
195    /// Creates a new CuError with a message and an underlying cause.
196    ///
197    /// # Example
198    /// ```
199    /// use cu29_traits::CuError;
200    ///
201    /// let io_err = std::io::Error::other("io error");
202    /// let err = CuError::new_with_cause("Failed to read file", io_err);
203    /// ```
204    #[cfg(feature = "std")]
205    pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
206    where
207        E: std::error::Error + Send + Sync + 'static,
208    {
209        CuError {
210            message: message.to_string(),
211            cause: Some(Box::new(cause)),
212        }
213    }
214
215    /// Creates a new CuError with a message and an underlying cause.
216    #[cfg(not(feature = "std"))]
217    pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
218    where
219        E: core::error::Error + Send + Sync + 'static,
220    {
221        CuError {
222            message: message.to_string(),
223            cause: Some(Box::new(cause)),
224        }
225    }
226
227    /// Adds or replaces the cause with a context string.
228    ///
229    /// This is useful for adding context to errors during propagation.
230    ///
231    /// # Example
232    /// ```
233    /// use cu29_traits::CuError;
234    ///
235    /// let err = CuError::from("base error").add_cause("additional context");
236    /// ```
237    pub fn add_cause(mut self, context: &str) -> CuError {
238        self.cause = Some(Box::new(StringError(context.to_string())));
239        self
240    }
241
242    /// Adds a cause error to this CuError (builder pattern).
243    ///
244    /// # Example
245    /// ```
246    /// use cu29_traits::CuError;
247    ///
248    /// let io_err = std::io::Error::other("io error");
249    /// let err = CuError::from("Operation failed").with_cause(io_err);
250    /// ```
251    #[cfg(feature = "std")]
252    pub fn with_cause<E>(mut self, cause: E) -> CuError
253    where
254        E: std::error::Error + Send + Sync + 'static,
255    {
256        self.cause = Some(Box::new(cause));
257        self
258    }
259
260    /// Adds a cause error to this CuError (builder pattern).
261    #[cfg(not(feature = "std"))]
262    pub fn with_cause<E>(mut self, cause: E) -> CuError
263    where
264        E: core::error::Error + Send + Sync + 'static,
265    {
266        self.cause = Some(Box::new(cause));
267        self
268    }
269
270    /// Returns a reference to the underlying cause, if any.
271    pub fn cause(&self) -> Option<&(dyn core::error::Error + Send + Sync + 'static)> {
272        self.cause.as_deref()
273    }
274
275    /// Returns the error message.
276    pub fn message(&self) -> &str {
277        &self.message
278    }
279}
280
281/// Creates a CuError with a message and cause in a single call.
282///
283/// This is a convenience function for use with `.map_err()`.
284///
285/// # Example
286/// ```
287/// use cu29_traits::with_cause;
288///
289/// let result: Result<(), std::io::Error> = Err(std::io::Error::other("io error"));
290/// let cu_result = result.map_err(|e| with_cause("Failed to read file", e));
291/// ```
292#[cfg(feature = "std")]
293pub fn with_cause<E>(message: &str, cause: E) -> CuError
294where
295    E: std::error::Error + Send + Sync + 'static,
296{
297    CuError::new_with_cause(message, cause)
298}
299
300/// Creates a CuError with a message and cause in a single call.
301#[cfg(not(feature = "std"))]
302pub fn with_cause<E>(message: &str, cause: E) -> CuError
303where
304    E: core::error::Error + Send + Sync + 'static,
305{
306    CuError::new_with_cause(message, cause)
307}
308
309// Generic Result type for copper.
310pub type CuResult<T> = Result<T, CuError>;
311
312/// Defines a basic write, append only stream trait to be able to log or send serializable objects.
313pub trait WriteStream<E: Encode>: Debug + Send + Sync {
314    fn log(&mut self, obj: &E) -> CuResult<()>;
315    fn flush(&mut self) -> CuResult<()> {
316        Ok(())
317    }
318    /// Optional byte count of the last successful `log` call, if the implementation can report it.
319    fn last_log_bytes(&self) -> Option<usize> {
320        None
321    }
322}
323
324/// Defines the types of what can be logged in the unified logger.
325#[derive(dEncode, dDecode, Copy, Clone, Debug, PartialEq)]
326pub enum UnifiedLogType {
327    Empty,             // Dummy default used as a debug marker
328    StructuredLogLine, // This is for the structured logs (ie. debug! etc..)
329    CopperList,        // This is the actual data log storing activities between tasks.
330    FrozenTasks,       // Log of all frozen state of the tasks.
331    LastEntry,         // This is a special entry that is used to signal the end of the log.
332    RuntimeLifecycle,  // Runtime lifecycle events (mission/config/stack context).
333}
334/// Represent the minimum set of traits to be usable as Metadata in Copper.
335pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
336
337impl Metadata for () {}
338
339/// Key metadata piece attached to every message in Copper.
340pub trait CuMsgMetadataTrait {
341    /// The time range used for the processing of this message
342    fn process_time(&self) -> PartialCuTimeRange;
343
344    /// Small status text for user UI to get the realtime state of task (max 24 chrs)
345    fn status_txt(&self) -> &CuCompactString;
346}
347
348/// A generic trait to expose the generated CuStampedDataSet from the task graph.
349pub trait ErasedCuStampedData {
350    fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
351    #[cfg(feature = "reflect")]
352    fn payload_reflect(&self) -> Option<&dyn Reflect>;
353    fn tov(&self) -> Tov;
354    fn metadata(&self) -> &dyn CuMsgMetadataTrait;
355}
356
357/// Trait to get a vector of type-erased CuStampedDataSet
358/// This is used for generic serialization of the copperlists
359pub trait ErasedCuStampedDataSet {
360    fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
361}
362
363/// Provides per-output raw payload sizes aligned with `ErasedCuStampedDataSet::cumsgs` order.
364pub trait CuPayloadRawBytes {
365    /// Returns raw payload sizes (stack + heap) for each output message.
366    /// `None` indicates the payload was not produced for that output.
367    fn payload_raw_bytes(&self) -> Vec<Option<u64>>;
368}
369
370/// Trait to trace back from the CopperList the origin of each message slot.
371///
372/// The returned slice must be aligned with `ErasedCuStampedDataSet::cumsgs()`:
373/// index `i` maps to copperlist slot `i`.
374pub trait MatchingTasks {
375    fn get_all_task_ids() -> &'static [&'static str];
376}
377
378/// Trait for providing JSON schemas for CopperList payload types.
379///
380/// This trait is implemented by the generated CuMsgs type via the `gen_cumsgs!` macro
381/// when MCAP export support is enabled. It provides compile-time schema information
382/// for each task's payload type, enabling proper schema generation for Foxglove.
383///
384/// The default implementation returns an empty vector for backwards compatibility
385/// with code that doesn't need MCAP export support.
386pub trait PayloadSchemas {
387    /// Returns a vector of (task_id, schema_json) pairs.
388    ///
389    /// Each entry corresponds to a CopperList output slot, in slot order.
390    /// The schema is a JSON Schema string generated from the payload type.
391    fn get_payload_schemas() -> Vec<(&'static str, String)> {
392        Vec::new()
393    }
394}
395
396/// A CopperListTuple needs to be encodable, decodable and fixed size in memory.
397pub trait CopperListTuple:
398    bincode::Encode
399    + bincode::Decode<()>
400    + Debug
401    + Serialize
402    + ErasedCuStampedDataSet
403    + MatchingTasks
404    + Default
405{
406} // Decode forces Sized already
407
408// Also anything that follows this contract can be a payload (blanket implementation)
409impl<T> CopperListTuple for T where
410    T: bincode::Encode
411        + bincode::Decode<()>
412        + Debug
413        + Serialize
414        + ErasedCuStampedDataSet
415        + MatchingTasks
416        + Default
417{
418}
419
420// We use this type to convey very small status messages.
421// MAX_SIZE from their repr module is not accessible so we need to copy paste their definition for 24
422// which is the maximum size for inline allocation (no heap)
423pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
424
425#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
426pub struct CuCompactString(pub CompactString);
427
428impl Encode for CuCompactString {
429    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
430        let CuCompactString(compact_string) = self;
431        let bytes = &compact_string.as_bytes();
432        bytes.encode(encoder)
433    }
434}
435
436impl Debug for CuCompactString {
437    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
438        if self.0.is_empty() {
439            return write!(f, "CuCompactString(Empty)");
440        }
441        write!(f, "CuCompactString({})", self.0)
442    }
443}
444
445impl<Context> Decode<Context> for CuCompactString {
446    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
447        let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; // Decode into a byte buffer
448        let compact_string =
449            CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
450        Ok(CuCompactString(compact_string))
451    }
452}
453
454impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
455    fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
456        CuCompactString::decode(decoder)
457    }
458}
459
460#[cfg(feature = "defmt")]
461impl defmt::Format for CuError {
462    fn format(&self, f: defmt::Formatter) {
463        match &self.cause {
464            Some(c) => {
465                let cause_str = c.to_string();
466                defmt::write!(
467                    f,
468                    "CuError {{ message: {}, cause: {} }}",
469                    defmt::Display2Format(&self.message),
470                    defmt::Display2Format(&cause_str),
471                )
472            }
473            None => defmt::write!(
474                f,
475                "CuError {{ message: {}, cause: None }}",
476                defmt::Display2Format(&self.message),
477            ),
478        }
479    }
480}
481
482#[cfg(feature = "defmt")]
483impl defmt::Format for CuCompactString {
484    fn format(&self, f: defmt::Formatter) {
485        if self.0.is_empty() {
486            defmt::write!(f, "CuCompactString(Empty)");
487        } else {
488            defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
489        }
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use crate::CuCompactString;
496    use bincode::{config, decode_from_slice, encode_to_vec};
497    use compact_str::CompactString;
498
499    #[test]
500    fn test_cucompactstr_encode_decode_empty() {
501        let cstr = CuCompactString(CompactString::from(""));
502        let config = config::standard();
503        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
504        assert_eq!(encoded.len(), 1); // This encodes the usize 0 in variable encoding so 1 byte which is 0.
505        let (decoded, _): (CuCompactString, usize) =
506            decode_from_slice(&encoded, config).expect("Decoding failed");
507        assert_eq!(cstr.0, decoded.0);
508    }
509
510    #[test]
511    fn test_cucompactstr_encode_decode_small() {
512        let cstr = CuCompactString(CompactString::from("test"));
513        let config = config::standard();
514        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
515        assert_eq!(encoded.len(), 5); // This encodes a 4-byte string "test" plus 1 byte for the length prefix.
516        let (decoded, _): (CuCompactString, usize) =
517            decode_from_slice(&encoded, config).expect("Decoding failed");
518        assert_eq!(cstr.0, decoded.0);
519    }
520}
521
522// Tests that require std feature
523#[cfg(all(test, feature = "std"))]
524mod std_tests {
525    use crate::{CuError, with_cause};
526
527    #[test]
528    fn test_cuerror_from_str() {
529        let err = CuError::from("test error");
530        assert_eq!(err.message(), "test error");
531        assert!(err.cause().is_none());
532    }
533
534    #[test]
535    fn test_cuerror_from_string() {
536        let err = CuError::from(String::from("test error"));
537        assert_eq!(err.message(), "test error");
538        assert!(err.cause().is_none());
539    }
540
541    #[test]
542    fn test_cuerror_new_index() {
543        let err = CuError::new(42);
544        assert_eq!(err.message(), "[interned:42]");
545        assert!(err.cause().is_none());
546    }
547
548    #[test]
549    fn test_cuerror_new_with_cause() {
550        let io_err = std::io::Error::other("io error");
551        let err = CuError::new_with_cause("wrapped error", io_err);
552        assert_eq!(err.message(), "wrapped error");
553        assert!(err.cause().is_some());
554        assert!(err.cause().unwrap().to_string().contains("io error"));
555    }
556
557    #[test]
558    fn test_cuerror_add_cause() {
559        let err = CuError::from("base error").add_cause("additional context");
560        assert_eq!(err.message(), "base error");
561        assert!(err.cause().is_some());
562        assert_eq!(err.cause().unwrap().to_string(), "additional context");
563    }
564
565    #[test]
566    fn test_cuerror_with_cause_method() {
567        let io_err = std::io::Error::other("io error");
568        let err = CuError::from("base error").with_cause(io_err);
569        assert_eq!(err.message(), "base error");
570        assert!(err.cause().is_some());
571    }
572
573    #[test]
574    fn test_cuerror_with_cause_free_function() {
575        let io_err = std::io::Error::other("io error");
576        let err = with_cause("wrapped", io_err);
577        assert_eq!(err.message(), "wrapped");
578        assert!(err.cause().is_some());
579    }
580
581    #[test]
582    fn test_cuerror_clone() {
583        let io_err = std::io::Error::other("io error");
584        let err = CuError::new_with_cause("test", io_err);
585        let cloned = err.clone();
586        assert_eq!(err.message(), cloned.message());
587        // Cause string representation should match
588        assert_eq!(
589            err.cause().map(|c| c.to_string()),
590            cloned.cause().map(|c| c.to_string())
591        );
592    }
593
594    #[test]
595    fn test_cuerror_serialize_deserialize_json() {
596        let io_err = std::io::Error::other("io error");
597        let err = CuError::new_with_cause("test", io_err);
598
599        let serialized = serde_json::to_string(&err).unwrap();
600        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
601
602        assert_eq!(err.message(), deserialized.message());
603        // Cause should be preserved as string
604        assert!(deserialized.cause().is_some());
605    }
606
607    #[test]
608    fn test_cuerror_serialize_deserialize_no_cause() {
609        let err = CuError::from("simple error");
610
611        let serialized = serde_json::to_string(&err).unwrap();
612        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
613
614        assert_eq!(err.message(), deserialized.message());
615        assert!(deserialized.cause().is_none());
616    }
617
618    #[test]
619    fn test_cuerror_display() {
620        let err = CuError::from("test error").add_cause("some context");
621        let display = format!("{}", err);
622        assert!(display.contains("test error"));
623        assert!(display.contains("some context"));
624    }
625
626    #[test]
627    fn test_cuerror_debug() {
628        let err = CuError::from("test error").add_cause("some context");
629        let debug = format!("{:?}", err);
630        assert!(debug.contains("test error"));
631        assert!(debug.contains("some context"));
632    }
633}