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