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::enc::write::Writer;
30use bincode::error::{DecodeError, EncodeError};
31use bincode::{BorrowDecode, Decode as dDecode, Decode, Encode, Encode as dEncode};
32use compact_str::CompactString;
33use cu29_clock::{PartialCuTimeRange, Tov};
34use serde::de::{self, SeqAccess, Visitor};
35use serde::{Deserialize, Deserializer, Serialize};
36
37use alloc::borrow::ToOwned;
38use alloc::boxed::Box;
39use alloc::format;
40use alloc::string::{String, ToString};
41use alloc::vec::Vec;
42#[cfg(feature = "std")]
43use core::cell::Cell;
44#[cfg(not(feature = "std"))]
45use core::error::Error as CoreError;
46use core::fmt::{Debug, Display, Formatter};
47#[cfg(feature = "std")]
48use std::error::Error;
49
50#[cfg(not(feature = "std"))]
51use spin::Mutex as SpinMutex;
52
53// Type alias for the boxed error type to simplify conditional compilation
54#[cfg(feature = "std")]
55type DynError = dyn std::error::Error + Send + Sync + 'static;
56#[cfg(not(feature = "std"))]
57type DynError = dyn core::error::Error + Send + Sync + 'static;
58
59/// A simple wrapper around String that implements Error trait.
60/// Used for cloning and deserializing CuError causes.
61#[derive(Debug)]
62struct StringError(String);
63
64impl Display for StringError {
65    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
66        write!(f, "{}", self.0)
67    }
68}
69
70#[cfg(feature = "std")]
71impl std::error::Error for StringError {}
72
73#[cfg(not(feature = "std"))]
74impl core::error::Error for StringError {}
75
76/// Common copper Error type.
77///
78/// This error type stores an optional cause as a boxed dynamic error,
79/// allowing for proper error chaining while maintaining Clone and
80/// Serialize/Deserialize support through custom implementations.
81pub struct CuError {
82    message: String,
83    cause: Option<Box<DynError>>,
84}
85
86// Custom Debug implementation that formats cause as string
87impl Debug for CuError {
88    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
89        f.debug_struct("CuError")
90            .field("message", &self.message)
91            .field("cause", &self.cause.as_ref().map(|e| e.to_string()))
92            .finish()
93    }
94}
95
96// Custom Clone implementation - clones cause as StringError wrapper
97impl Clone for CuError {
98    fn clone(&self) -> Self {
99        CuError {
100            message: self.message.clone(),
101            cause: self
102                .cause
103                .as_ref()
104                .map(|e| Box::new(StringError(e.to_string())) as Box<DynError>),
105        }
106    }
107}
108
109// Custom Serialize - serializes cause as Option<String>
110impl Serialize for CuError {
111    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112    where
113        S: serde::Serializer,
114    {
115        use serde::ser::SerializeStruct;
116        let mut state = serializer.serialize_struct("CuError", 2)?;
117        state.serialize_field("message", &self.message)?;
118        state.serialize_field("cause", &self.cause.as_ref().map(|e| e.to_string()))?;
119        state.end()
120    }
121}
122
123// Custom Deserialize - deserializes cause as StringError wrapper
124impl<'de> Deserialize<'de> for CuError {
125    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126    where
127        D: serde::Deserializer<'de>,
128    {
129        #[derive(Deserialize)]
130        struct CuErrorHelper {
131            message: String,
132            cause: Option<String>,
133        }
134
135        let helper = CuErrorHelper::deserialize(deserializer)?;
136        Ok(CuError {
137            message: helper.message,
138            cause: helper
139                .cause
140                .map(|s| Box::new(StringError(s)) as Box<DynError>),
141        })
142    }
143}
144
145impl Display for CuError {
146    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
147        let context_str = match &self.cause {
148            Some(c) => c.to_string(),
149            None => "None".to_string(),
150        };
151        write!(f, "{}\n   context:{}", self.message, context_str)?;
152        Ok(())
153    }
154}
155
156#[cfg(not(feature = "std"))]
157impl CoreError for CuError {
158    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
159        self.cause
160            .as_deref()
161            .map(|e| e as &(dyn CoreError + 'static))
162    }
163}
164
165#[cfg(feature = "std")]
166impl Error for CuError {
167    fn source(&self) -> Option<&(dyn Error + 'static)> {
168        self.cause.as_deref().map(|e| e as &(dyn Error + 'static))
169    }
170}
171
172impl From<&str> for CuError {
173    fn from(s: &str) -> CuError {
174        CuError {
175            message: s.to_string(),
176            cause: None,
177        }
178    }
179}
180
181impl From<String> for CuError {
182    fn from(s: String) -> CuError {
183        CuError {
184            message: s,
185            cause: None,
186        }
187    }
188}
189
190impl CuError {
191    /// Creates a new CuError from an interned string index.
192    /// Used by the cu_error! macro.
193    ///
194    /// The index is stored as a placeholder string `[interned:{index}]`.
195    /// Actual string resolution happens at logging time via the unified logger.
196    pub fn new(message_index: usize) -> CuError {
197        CuError {
198            message: format!("[interned:{}]", message_index),
199            cause: None,
200        }
201    }
202
203    /// Creates a new CuError with a message and an underlying cause.
204    ///
205    /// # Example
206    /// ```
207    /// use cu29_traits::CuError;
208    ///
209    /// let io_err = std::io::Error::other("io error");
210    /// let err = CuError::new_with_cause("Failed to read file", io_err);
211    /// ```
212    #[cfg(feature = "std")]
213    pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
214    where
215        E: std::error::Error + Send + Sync + 'static,
216    {
217        CuError {
218            message: message.to_string(),
219            cause: Some(Box::new(cause)),
220        }
221    }
222
223    /// Creates a new CuError with a message and an underlying cause.
224    #[cfg(not(feature = "std"))]
225    pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
226    where
227        E: core::error::Error + Send + Sync + 'static,
228    {
229        CuError {
230            message: message.to_string(),
231            cause: Some(Box::new(cause)),
232        }
233    }
234
235    /// Adds or replaces the cause with a context string.
236    ///
237    /// This is useful for adding context to errors during propagation.
238    ///
239    /// # Example
240    /// ```
241    /// use cu29_traits::CuError;
242    ///
243    /// let err = CuError::from("base error").add_cause("additional context");
244    /// ```
245    pub fn add_cause(mut self, context: &str) -> CuError {
246        self.cause = Some(Box::new(StringError(context.to_string())));
247        self
248    }
249
250    /// Adds a cause error to this CuError (builder pattern).
251    ///
252    /// # Example
253    /// ```
254    /// use cu29_traits::CuError;
255    ///
256    /// let io_err = std::io::Error::other("io error");
257    /// let err = CuError::from("Operation failed").with_cause(io_err);
258    /// ```
259    #[cfg(feature = "std")]
260    pub fn with_cause<E>(mut self, cause: E) -> CuError
261    where
262        E: std::error::Error + Send + Sync + 'static,
263    {
264        self.cause = Some(Box::new(cause));
265        self
266    }
267
268    /// Adds a cause error to this CuError (builder pattern).
269    #[cfg(not(feature = "std"))]
270    pub fn with_cause<E>(mut self, cause: E) -> CuError
271    where
272        E: core::error::Error + Send + Sync + 'static,
273    {
274        self.cause = Some(Box::new(cause));
275        self
276    }
277
278    /// Returns a reference to the underlying cause, if any.
279    pub fn cause(&self) -> Option<&(dyn core::error::Error + Send + Sync + 'static)> {
280        self.cause.as_deref()
281    }
282
283    /// Returns the error message.
284    pub fn message(&self) -> &str {
285        &self.message
286    }
287}
288
289/// Creates a CuError with a message and cause in a single call.
290///
291/// This is a convenience function for use with `.map_err()`.
292///
293/// # Example
294/// ```
295/// use cu29_traits::with_cause;
296///
297/// let result: Result<(), std::io::Error> = Err(std::io::Error::other("io error"));
298/// let cu_result = result.map_err(|e| with_cause("Failed to read file", e));
299/// ```
300#[cfg(feature = "std")]
301pub fn with_cause<E>(message: &str, cause: E) -> CuError
302where
303    E: std::error::Error + Send + Sync + 'static,
304{
305    CuError::new_with_cause(message, cause)
306}
307
308/// Creates a CuError with a message and cause in a single call.
309#[cfg(not(feature = "std"))]
310pub fn with_cause<E>(message: &str, cause: E) -> CuError
311where
312    E: core::error::Error + Send + Sync + 'static,
313{
314    CuError::new_with_cause(message, cause)
315}
316
317// Generic Result type for copper.
318pub type CuResult<T> = Result<T, CuError>;
319
320#[cfg(feature = "std")]
321thread_local! {
322    static OBSERVED_ENCODE_BYTES: Cell<Option<usize>> = const { Cell::new(None) };
323}
324
325#[cfg(not(feature = "std"))]
326static OBSERVED_ENCODE_BYTES: SpinMutex<Option<usize>> = SpinMutex::new(None);
327
328/// Starts observed byte counting for the current encode pass.
329pub fn begin_observed_encode() {
330    #[cfg(feature = "std")]
331    OBSERVED_ENCODE_BYTES.with(|bytes| {
332        debug_assert!(
333            bytes.get().is_none(),
334            "observed encode measurement must not be nested"
335        );
336        bytes.set(Some(0));
337    });
338
339    #[cfg(not(feature = "std"))]
340    {
341        let mut bytes = OBSERVED_ENCODE_BYTES.lock();
342        debug_assert!(
343            bytes.is_none(),
344            "observed encode measurement must not be nested"
345        );
346        *bytes = Some(0);
347    }
348}
349
350/// Ends observed byte counting and returns the total bytes written.
351pub fn finish_observed_encode() -> usize {
352    #[cfg(feature = "std")]
353    {
354        OBSERVED_ENCODE_BYTES.with(|bytes| bytes.replace(None).unwrap_or(0))
355    }
356
357    #[cfg(not(feature = "std"))]
358    {
359        OBSERVED_ENCODE_BYTES.lock().take().unwrap_or(0)
360    }
361}
362
363/// Aborts any active observed byte counting session.
364pub fn abort_observed_encode() {
365    #[cfg(feature = "std")]
366    OBSERVED_ENCODE_BYTES.with(|bytes| bytes.set(None));
367
368    #[cfg(not(feature = "std"))]
369    {
370        *OBSERVED_ENCODE_BYTES.lock() = None;
371    }
372}
373
374/// Returns the number of bytes written so far in the current observed encode pass.
375pub fn observed_encode_bytes() -> usize {
376    #[cfg(feature = "std")]
377    {
378        OBSERVED_ENCODE_BYTES.with(|bytes| bytes.get().unwrap_or(0))
379    }
380
381    #[cfg(not(feature = "std"))]
382    {
383        OBSERVED_ENCODE_BYTES.lock().as_ref().copied().unwrap_or(0)
384    }
385}
386
387/// Records bytes written by an observed writer.
388pub fn record_observed_encode_bytes(bytes: usize) {
389    #[cfg(feature = "std")]
390    OBSERVED_ENCODE_BYTES.with(|total| {
391        if let Some(current) = total.get() {
392            total.set(Some(current.saturating_add(bytes)));
393        }
394    });
395
396    #[cfg(not(feature = "std"))]
397    {
398        let mut total = OBSERVED_ENCODE_BYTES.lock();
399        if let Some(current) = *total {
400            *total = Some(current.saturating_add(bytes));
401        }
402    }
403}
404
405/// A bincode writer wrapper that reports every encoded byte to Copper's
406/// observation counters.
407pub struct ObservedWriter<W> {
408    inner: W,
409}
410
411impl<W> ObservedWriter<W> {
412    pub const fn new(inner: W) -> Self {
413        Self { inner }
414    }
415
416    pub fn into_inner(self) -> W {
417        self.inner
418    }
419
420    pub fn inner(&self) -> &W {
421        &self.inner
422    }
423
424    pub fn inner_mut(&mut self) -> &mut W {
425        &mut self.inner
426    }
427}
428
429impl<W: Writer> Writer for ObservedWriter<W> {
430    #[inline(always)]
431    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
432        self.inner.write(bytes)?;
433        record_observed_encode_bytes(bytes.len());
434        Ok(())
435    }
436}
437
438/// Defines a basic write, append only stream trait to be able to log or send serializable objects.
439pub trait WriteStream<E: Encode>: Debug + Send + Sync {
440    fn log(&mut self, obj: &E) -> CuResult<()>;
441    fn flush(&mut self) -> CuResult<()> {
442        Ok(())
443    }
444    /// Optional byte count of the last successful `log` call, if the implementation can report it.
445    fn last_log_bytes(&self) -> Option<usize> {
446        None
447    }
448}
449
450/// Defines the types of what can be logged in the unified logger.
451#[derive(dEncode, dDecode, Copy, Clone, Debug, PartialEq)]
452pub enum UnifiedLogType {
453    Empty,             // Dummy default used as a debug marker
454    StructuredLogLine, // This is for the structured logs (ie. debug! etc..)
455    CopperList,        // This is the actual data log storing activities between tasks.
456    FrozenTasks,       // Log of all frozen state of the tasks.
457    LastEntry,         // This is a special entry that is used to signal the end of the log.
458    RuntimeLifecycle,  // Runtime lifecycle events (mission/config/stack context).
459}
460/// Represent the minimum set of traits to be usable as Metadata in Copper.
461pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
462
463impl Metadata for () {}
464
465/// Origin metadata captured when a Copper-aware transport receives a remote message.
466#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
467#[cfg_attr(feature = "reflect", derive(Reflect))]
468pub struct CuMsgOrigin {
469    pub subsystem_code: u16,
470    pub instance_id: u32,
471    pub cl_id: u64,
472}
473
474/// Key metadata piece attached to every message in Copper.
475pub trait CuMsgMetadataTrait {
476    /// The time range used for the processing of this message
477    fn process_time(&self) -> PartialCuTimeRange;
478
479    /// Small status text for user UI to get the realtime state of task (max 24 chrs)
480    fn status_txt(&self) -> &CuCompactString;
481
482    /// Remote Copper provenance captured on receive.
483    fn origin(&self) -> Option<&CuMsgOrigin> {
484        None
485    }
486}
487
488/// A generic trait to expose the generated CuStampedDataSet from the task graph.
489pub trait ErasedCuStampedData {
490    fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
491    #[cfg(feature = "reflect")]
492    fn payload_reflect(&self) -> Option<&dyn Reflect>;
493    fn tov(&self) -> Tov;
494    fn metadata(&self) -> &dyn CuMsgMetadataTrait;
495}
496
497/// Trait to get a vector of type-erased CuStampedDataSet
498/// This is used for generic serialization of the copperlists
499pub trait ErasedCuStampedDataSet {
500    fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
501}
502
503/// Provides per-output raw payload sizes aligned with `ErasedCuStampedDataSet::cumsgs` order.
504pub trait CuPayloadRawBytes {
505    /// Returns raw payload sizes (stack + heap) for each output message.
506    /// `None` indicates the payload was not produced for that output.
507    fn payload_raw_bytes(&self) -> Vec<Option<u64>>;
508}
509
510/// Trait to trace back from the CopperList the origin of each message slot.
511///
512/// The returned slice must be aligned with `ErasedCuStampedDataSet::cumsgs()`:
513/// index `i` maps to copperlist slot `i`.
514#[derive(Debug, Clone, Copy)]
515pub struct TaskOutputSpec {
516    pub task_id: &'static str,
517    pub msg_type: &'static str,
518    pub payload_type_path_fn: fn() -> &'static str,
519}
520
521impl TaskOutputSpec {
522    #[inline]
523    pub fn payload_type_path(&self) -> &'static str {
524        (self.payload_type_path_fn)()
525    }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub enum DebugFieldSemantics {
530    Time,
531    OptionalTime,
532    Duration,
533    GeodeticPosition,
534    Quantity {
535        quantity_name: String,
536        unit_symbol: String,
537    },
538}
539
540#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
541#[serde(rename_all = "snake_case")]
542pub enum DebugFieldKind {
543    Scalar,
544    Struct,
545    TupleStruct,
546    Tuple,
547    List,
548    Array,
549    Map,
550    Set,
551    Enum,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
555pub struct DebugFieldDescriptor {
556    pub display_path: String,
557    #[serde(
558        default,
559        skip_serializing_if = "Option::is_none",
560        deserialize_with = "deserialize_debug_binding_name"
561    )]
562    pub binding_name: Option<String>,
563    pub field_type: String,
564    pub value_type_path: String,
565    #[serde(default, skip_serializing_if = "Option::is_none")]
566    pub semantics: Option<DebugFieldSemantics>,
567    pub nullable: bool,
568    pub kind: DebugFieldKind,
569    #[serde(default, skip_serializing_if = "Vec::is_empty")]
570    pub children: Vec<DebugFieldDescriptor>,
571}
572
573fn deserialize_debug_binding_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
574where
575    D: Deserializer<'de>,
576{
577    struct BindingNameVisitor;
578
579    impl<'de> Visitor<'de> for BindingNameVisitor {
580        type Value = Option<String>;
581
582        fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583            formatter.write_str("a string, null, or an empty sequence")
584        }
585
586        fn visit_none<E>(self) -> Result<Self::Value, E>
587        where
588            E: de::Error,
589        {
590            Ok(None)
591        }
592
593        fn visit_unit<E>(self) -> Result<Self::Value, E>
594        where
595            E: de::Error,
596        {
597            Ok(None)
598        }
599
600        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
601        where
602            D: Deserializer<'de>,
603        {
604            deserialize_debug_binding_name(deserializer)
605        }
606
607        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
608        where
609            E: de::Error,
610        {
611            Ok(Some(value.to_owned()))
612        }
613
614        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
615        where
616            E: de::Error,
617        {
618            Ok(Some(value))
619        }
620
621        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
622        where
623            A: SeqAccess<'de>,
624        {
625            if seq.next_element::<de::IgnoredAny>()?.is_none() {
626                return Ok(None);
627            }
628            Err(de::Error::invalid_type(
629                de::Unexpected::Seq,
630                &"an empty sequence",
631            ))
632        }
633    }
634
635    deserializer.deserialize_any(BindingNameVisitor)
636}
637
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct DebugScalarRegistration {
640    pub type_path: &'static str,
641    pub field_type: &'static str,
642    pub semantics: DebugFieldSemantics,
643}
644
645pub trait DebugScalarType: 'static {
646    fn debug_scalar_registration() -> DebugScalarRegistration;
647}
648
649pub trait MatchingTasks {
650    fn get_all_task_ids() -> &'static [&'static str];
651
652    fn get_output_specs() -> &'static [TaskOutputSpec] {
653        &[]
654    }
655}
656
657/// Trait for providing JSON schemas for CopperList payload types.
658///
659/// This trait is implemented by the generated CuMsgs type via the `gen_cumsgs!` macro
660/// when MCAP export support is enabled. It provides compile-time schema information
661/// for each task's payload type, enabling proper schema generation for Foxglove.
662///
663/// The default implementation returns an empty vector for backwards compatibility
664/// with code that doesn't need MCAP export support.
665pub trait PayloadSchemas {
666    /// Returns a vector of (task_id, schema_json) pairs.
667    ///
668    /// Each entry corresponds to a CopperList output slot, in slot order.
669    /// The schema is a JSON Schema string generated from the payload type.
670    fn get_payload_schemas() -> Vec<(&'static str, String)> {
671        Vec::new()
672    }
673}
674
675/// A CopperListTuple needs to be encodable, decodable and fixed size in memory.
676pub trait CopperListTuple:
677    bincode::Encode
678    + bincode::Decode<()>
679    + Debug
680    + Serialize
681    + ErasedCuStampedDataSet
682    + MatchingTasks
683    + Default
684{
685} // Decode forces Sized already
686
687// Also anything that follows this contract can be a payload (blanket implementation)
688impl<T> CopperListTuple for T where
689    T: bincode::Encode
690        + bincode::Decode<()>
691        + Debug
692        + Serialize
693        + ErasedCuStampedDataSet
694        + MatchingTasks
695        + Default
696{
697}
698
699// We use this type to convey very small status messages.
700// MAX_SIZE from their repr module is not accessible so we need to copy paste their definition for 24
701// which is the maximum size for inline allocation (no heap)
702pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
703
704#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
705pub struct CuCompactString(pub CompactString);
706
707impl Encode for CuCompactString {
708    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
709        let CuCompactString(compact_string) = self;
710        let bytes = &compact_string.as_bytes();
711        bytes.encode(encoder)
712    }
713}
714
715impl Debug for CuCompactString {
716    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
717        if self.0.is_empty() {
718            return write!(f, "CuCompactString(Empty)");
719        }
720        write!(f, "CuCompactString({})", self.0)
721    }
722}
723
724impl<Context> Decode<Context> for CuCompactString {
725    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
726        let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; // Decode into a byte buffer
727        let compact_string =
728            CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
729        Ok(CuCompactString(compact_string))
730    }
731}
732
733impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
734    fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
735        CuCompactString::decode(decoder)
736    }
737}
738
739#[cfg(feature = "defmt")]
740impl defmt::Format for CuError {
741    fn format(&self, f: defmt::Formatter) {
742        match &self.cause {
743            Some(c) => {
744                let cause_str = c.to_string();
745                defmt::write!(
746                    f,
747                    "CuError {{ message: {}, cause: {} }}",
748                    defmt::Display2Format(&self.message),
749                    defmt::Display2Format(&cause_str),
750                )
751            }
752            None => defmt::write!(
753                f,
754                "CuError {{ message: {}, cause: None }}",
755                defmt::Display2Format(&self.message),
756            ),
757        }
758    }
759}
760
761#[cfg(feature = "defmt")]
762impl defmt::Format for CuCompactString {
763    fn format(&self, f: defmt::Formatter) {
764        if self.0.is_empty() {
765            defmt::write!(f, "CuCompactString(Empty)");
766        } else {
767            defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
768        }
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    use crate::CuCompactString;
775    use bincode::{config, decode_from_slice, encode_to_vec};
776    use compact_str::CompactString;
777
778    #[test]
779    fn test_cucompactstr_encode_decode_empty() {
780        let cstr = CuCompactString(CompactString::from(""));
781        let config = config::standard();
782        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
783        assert_eq!(encoded.len(), 1); // This encodes the usize 0 in variable encoding so 1 byte which is 0.
784        let (decoded, _): (CuCompactString, usize) =
785            decode_from_slice(&encoded, config).expect("Decoding failed");
786        assert_eq!(cstr.0, decoded.0);
787    }
788
789    #[test]
790    fn test_cucompactstr_encode_decode_small() {
791        let cstr = CuCompactString(CompactString::from("test"));
792        let config = config::standard();
793        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
794        assert_eq!(encoded.len(), 5); // This encodes a 4-byte string "test" plus 1 byte for the length prefix.
795        let (decoded, _): (CuCompactString, usize) =
796            decode_from_slice(&encoded, config).expect("Decoding failed");
797        assert_eq!(cstr.0, decoded.0);
798    }
799}
800
801// Tests that require std feature
802#[cfg(all(test, feature = "std"))]
803mod std_tests {
804    use crate::{CuError, DebugFieldDescriptor, DebugFieldKind, DebugFieldSemantics, with_cause};
805    use serde_json::json;
806
807    #[test]
808    fn test_cuerror_from_str() {
809        let err = CuError::from("test error");
810        assert_eq!(err.message(), "test error");
811        assert!(err.cause().is_none());
812    }
813
814    #[test]
815    fn test_cuerror_from_string() {
816        let err = CuError::from(String::from("test error"));
817        assert_eq!(err.message(), "test error");
818        assert!(err.cause().is_none());
819    }
820
821    #[test]
822    fn test_cuerror_new_index() {
823        let err = CuError::new(42);
824        assert_eq!(err.message(), "[interned:42]");
825        assert!(err.cause().is_none());
826    }
827
828    #[test]
829    fn test_cuerror_new_with_cause() {
830        let io_err = std::io::Error::other("io error");
831        let err = CuError::new_with_cause("wrapped error", io_err);
832        assert_eq!(err.message(), "wrapped error");
833        assert!(err.cause().is_some());
834        assert!(err.cause().unwrap().to_string().contains("io error"));
835    }
836
837    #[test]
838    fn test_cuerror_add_cause() {
839        let err = CuError::from("base error").add_cause("additional context");
840        assert_eq!(err.message(), "base error");
841        assert!(err.cause().is_some());
842        assert_eq!(err.cause().unwrap().to_string(), "additional context");
843    }
844
845    #[test]
846    fn test_cuerror_with_cause_method() {
847        let io_err = std::io::Error::other("io error");
848        let err = CuError::from("base error").with_cause(io_err);
849        assert_eq!(err.message(), "base error");
850        assert!(err.cause().is_some());
851    }
852
853    #[test]
854    fn test_cuerror_with_cause_free_function() {
855        let io_err = std::io::Error::other("io error");
856        let err = with_cause("wrapped", io_err);
857        assert_eq!(err.message(), "wrapped");
858        assert!(err.cause().is_some());
859    }
860
861    #[test]
862    fn test_cuerror_clone() {
863        let io_err = std::io::Error::other("io error");
864        let err = CuError::new_with_cause("test", io_err);
865        let cloned = err.clone();
866        assert_eq!(err.message(), cloned.message());
867        // Cause string representation should match
868        assert_eq!(
869            err.cause().map(|c| c.to_string()),
870            cloned.cause().map(|c| c.to_string())
871        );
872    }
873
874    #[test]
875    fn test_cuerror_serialize_deserialize_json() {
876        let io_err = std::io::Error::other("io error");
877        let err = CuError::new_with_cause("test", io_err);
878
879        let serialized = serde_json::to_string(&err).unwrap();
880        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
881
882        assert_eq!(err.message(), deserialized.message());
883        // Cause should be preserved as string
884        assert!(deserialized.cause().is_some());
885    }
886
887    #[test]
888    fn test_cuerror_serialize_deserialize_no_cause() {
889        let err = CuError::from("simple error");
890
891        let serialized = serde_json::to_string(&err).unwrap();
892        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
893
894        assert_eq!(err.message(), deserialized.message());
895        assert!(deserialized.cause().is_none());
896    }
897
898    #[test]
899    fn test_cuerror_display() {
900        let err = CuError::from("test error").add_cause("some context");
901        let display = format!("{}", err);
902        assert!(display.contains("test error"));
903        assert!(display.contains("some context"));
904    }
905
906    #[test]
907    fn test_cuerror_debug() {
908        let err = CuError::from("test error").add_cause("some context");
909        let debug = format!("{:?}", err);
910        assert!(debug.contains("test error"));
911        assert!(debug.contains("some context"));
912    }
913
914    #[test]
915    fn debug_field_descriptor_skips_missing_binding_name_on_serialize() {
916        let descriptor = DebugFieldDescriptor {
917            display_path: "meta.process_time.start_ns".to_owned(),
918            binding_name: None,
919            field_type: "integer".to_owned(),
920            value_type_path: "cu29_clock::CuTime".to_owned(),
921            semantics: Some(DebugFieldSemantics::Time),
922            nullable: true,
923            kind: DebugFieldKind::Scalar,
924            children: Vec::new(),
925        };
926
927        let encoded = serde_json::to_value(&descriptor).unwrap();
928        assert!(encoded.get("binding_name").is_none());
929    }
930
931    #[test]
932    fn debug_field_descriptor_accepts_empty_array_binding_name() {
933        let encoded = json!({
934            "display_path": "meta.process_time.start_ns",
935            "binding_name": [],
936            "field_type": "integer",
937            "value_type_path": "cu29_clock::CuTime",
938            "semantics": "Time",
939            "nullable": true,
940            "kind": "scalar",
941        });
942
943        let descriptor: DebugFieldDescriptor = serde_json::from_value(encoded).unwrap();
944        assert_eq!(descriptor.binding_name, None);
945        assert_eq!(descriptor.semantics, Some(DebugFieldSemantics::Time));
946        assert_eq!(descriptor.kind, DebugFieldKind::Scalar);
947        assert!(descriptor.children.is_empty());
948    }
949}