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 SyncMutex;
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: SyncMutex<Option<usize>> = SyncMutex::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, Copy, Serialize, Deserialize, PartialEq, Eq)]
555#[serde(rename_all = "snake_case")]
556pub enum DebugScalarKind {
557    Bool,
558    I8,
559    I16,
560    I32,
561    I64,
562    I128,
563    Isize,
564    U8,
565    U16,
566    U32,
567    U64,
568    U128,
569    Usize,
570    F32,
571    F64,
572    Char,
573    String,
574}
575
576impl DebugScalarKind {
577    pub const fn type_name(self) -> &'static str {
578        match self {
579            Self::Bool => "bool",
580            Self::I8 => "i8",
581            Self::I16 => "i16",
582            Self::I32 => "i32",
583            Self::I64 => "i64",
584            Self::I128 => "i128",
585            Self::Isize => "isize",
586            Self::U8 => "u8",
587            Self::U16 => "u16",
588            Self::U32 => "u32",
589            Self::U64 => "u64",
590            Self::U128 => "u128",
591            Self::Usize => "usize",
592            Self::F32 => "f32",
593            Self::F64 => "f64",
594            Self::Char => "char",
595            Self::String => "String",
596        }
597    }
598
599    pub const fn is_numeric(self) -> bool {
600        matches!(
601            self,
602            Self::I8
603                | Self::I16
604                | Self::I32
605                | Self::I64
606                | Self::I128
607                | Self::Isize
608                | Self::U8
609                | Self::U16
610                | Self::U32
611                | Self::U64
612                | Self::U128
613                | Self::Usize
614                | Self::F32
615                | Self::F64
616        )
617    }
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
621pub struct DebugFieldDescriptor {
622    pub display_path: String,
623    #[serde(
624        default,
625        skip_serializing_if = "Option::is_none",
626        deserialize_with = "deserialize_debug_binding_name"
627    )]
628    pub binding_name: Option<String>,
629    pub value_type_path: String,
630    #[serde(default, skip_serializing_if = "Option::is_none")]
631    pub scalar_kind: Option<DebugScalarKind>,
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub semantics: Option<DebugFieldSemantics>,
634    pub nullable: bool,
635    pub kind: DebugFieldKind,
636    #[serde(default, skip_serializing_if = "Vec::is_empty")]
637    pub children: Vec<DebugFieldDescriptor>,
638}
639
640fn deserialize_debug_binding_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
641where
642    D: Deserializer<'de>,
643{
644    struct BindingNameVisitor;
645
646    impl<'de> Visitor<'de> for BindingNameVisitor {
647        type Value = Option<String>;
648
649        fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
650            formatter.write_str("a string, null, or an empty sequence")
651        }
652
653        fn visit_none<E>(self) -> Result<Self::Value, E>
654        where
655            E: de::Error,
656        {
657            Ok(None)
658        }
659
660        fn visit_unit<E>(self) -> Result<Self::Value, E>
661        where
662            E: de::Error,
663        {
664            Ok(None)
665        }
666
667        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
668        where
669            D: Deserializer<'de>,
670        {
671            deserialize_debug_binding_name(deserializer)
672        }
673
674        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
675        where
676            E: de::Error,
677        {
678            Ok(Some(value.to_owned()))
679        }
680
681        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
682        where
683            E: de::Error,
684        {
685            Ok(Some(value))
686        }
687
688        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
689        where
690            A: SeqAccess<'de>,
691        {
692            if seq.next_element::<de::IgnoredAny>()?.is_none() {
693                return Ok(None);
694            }
695            Err(de::Error::invalid_type(
696                de::Unexpected::Seq,
697                &"an empty sequence",
698            ))
699        }
700    }
701
702    deserializer.deserialize_any(BindingNameVisitor)
703}
704
705#[derive(Debug, Clone, PartialEq, Eq)]
706pub struct DebugScalarRegistration {
707    pub type_path: &'static str,
708    pub scalar_kind: DebugScalarKind,
709    pub semantics: DebugFieldSemantics,
710}
711
712pub trait DebugScalarType: 'static {
713    fn debug_scalar_registration() -> DebugScalarRegistration;
714}
715
716pub trait MatchingTasks {
717    fn get_all_task_ids() -> &'static [&'static str];
718
719    fn get_output_specs() -> &'static [TaskOutputSpec] {
720        &[]
721    }
722}
723
724/// Trait for providing JSON schemas for CopperList payload types.
725///
726/// This trait is implemented by the generated CuMsgs type via the `gen_cumsgs!` macro
727/// when MCAP export support is enabled. It provides compile-time schema information
728/// for each task's payload type, enabling proper schema generation for Foxglove.
729///
730/// The default implementation returns an empty vector for backwards compatibility
731/// with code that doesn't need MCAP export support.
732pub trait PayloadSchemas {
733    /// Returns a vector of (task_id, schema_json) pairs.
734    ///
735    /// Each entry corresponds to a CopperList output slot, in slot order.
736    /// The schema is a JSON Schema string generated from the payload type.
737    fn get_payload_schemas() -> Vec<(&'static str, String)> {
738        Vec::new()
739    }
740}
741
742/// A CopperListTuple needs to be encodable, decodable and fixed size in memory.
743pub trait CopperListTuple:
744    bincode::Encode
745    + bincode::Decode<()>
746    + Debug
747    + Serialize
748    + ErasedCuStampedDataSet
749    + MatchingTasks
750    + Default
751{
752} // Decode forces Sized already
753
754// Also anything that follows this contract can be a payload (blanket implementation)
755impl<T> CopperListTuple for T where
756    T: bincode::Encode
757        + bincode::Decode<()>
758        + Debug
759        + Serialize
760        + ErasedCuStampedDataSet
761        + MatchingTasks
762        + Default
763{
764}
765
766// We use this type to convey very small status messages.
767// MAX_SIZE from their repr module is not accessible so we need to copy paste their definition for 24
768// which is the maximum size for inline allocation (no heap)
769pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
770
771#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
772pub struct CuCompactString(pub CompactString);
773
774impl Encode for CuCompactString {
775    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
776        let CuCompactString(compact_string) = self;
777        let bytes = &compact_string.as_bytes();
778        bytes.encode(encoder)
779    }
780}
781
782impl Debug for CuCompactString {
783    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
784        if self.0.is_empty() {
785            return write!(f, "CuCompactString(Empty)");
786        }
787        write!(f, "CuCompactString({})", self.0)
788    }
789}
790
791impl<Context> Decode<Context> for CuCompactString {
792    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
793        let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; // Decode into a byte buffer
794        let compact_string =
795            CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
796        Ok(CuCompactString(compact_string))
797    }
798}
799
800impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
801    fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
802        CuCompactString::decode(decoder)
803    }
804}
805
806#[cfg(feature = "defmt")]
807impl defmt::Format for CuError {
808    fn format(&self, f: defmt::Formatter) {
809        match &self.cause {
810            Some(c) => {
811                let cause_str = c.to_string();
812                defmt::write!(
813                    f,
814                    "CuError {{ message: {}, cause: {} }}",
815                    defmt::Display2Format(&self.message),
816                    defmt::Display2Format(&cause_str),
817                )
818            }
819            None => defmt::write!(
820                f,
821                "CuError {{ message: {}, cause: None }}",
822                defmt::Display2Format(&self.message),
823            ),
824        }
825    }
826}
827
828#[cfg(feature = "defmt")]
829impl defmt::Format for CuCompactString {
830    fn format(&self, f: defmt::Formatter) {
831        if self.0.is_empty() {
832            defmt::write!(f, "CuCompactString(Empty)");
833        } else {
834            defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
835        }
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use crate::CuCompactString;
842    use bincode::{config, decode_from_slice, encode_to_vec};
843    use compact_str::CompactString;
844
845    #[test]
846    fn test_cucompactstr_encode_decode_empty() {
847        let cstr = CuCompactString(CompactString::from(""));
848        let config = config::standard();
849        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
850        assert_eq!(encoded.len(), 1); // This encodes the usize 0 in variable encoding so 1 byte which is 0.
851        let (decoded, _): (CuCompactString, usize) =
852            decode_from_slice(&encoded, config).expect("Decoding failed");
853        assert_eq!(cstr.0, decoded.0);
854    }
855
856    #[test]
857    fn test_cucompactstr_encode_decode_small() {
858        let cstr = CuCompactString(CompactString::from("test"));
859        let config = config::standard();
860        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
861        assert_eq!(encoded.len(), 5); // This encodes a 4-byte string "test" plus 1 byte for the length prefix.
862        let (decoded, _): (CuCompactString, usize) =
863            decode_from_slice(&encoded, config).expect("Decoding failed");
864        assert_eq!(cstr.0, decoded.0);
865    }
866}
867
868// Tests that require std feature
869#[cfg(all(test, feature = "std"))]
870mod std_tests {
871    use crate::{
872        CuError, DebugFieldDescriptor, DebugFieldKind, DebugFieldSemantics, DebugScalarKind,
873        with_cause,
874    };
875    use serde_json::json;
876
877    #[test]
878    fn test_cuerror_from_str() {
879        let err = CuError::from("test error");
880        assert_eq!(err.message(), "test error");
881        assert!(err.cause().is_none());
882    }
883
884    #[test]
885    fn test_cuerror_from_string() {
886        let err = CuError::from(String::from("test error"));
887        assert_eq!(err.message(), "test error");
888        assert!(err.cause().is_none());
889    }
890
891    #[test]
892    fn test_cuerror_new_index() {
893        let err = CuError::new(42);
894        assert_eq!(err.message(), "[interned:42]");
895        assert!(err.cause().is_none());
896    }
897
898    #[test]
899    fn test_cuerror_new_with_cause() {
900        let io_err = std::io::Error::other("io error");
901        let err = CuError::new_with_cause("wrapped error", io_err);
902        assert_eq!(err.message(), "wrapped error");
903        assert!(err.cause().is_some());
904        assert!(err.cause().unwrap().to_string().contains("io error"));
905    }
906
907    #[test]
908    fn test_cuerror_add_cause() {
909        let err = CuError::from("base error").add_cause("additional context");
910        assert_eq!(err.message(), "base error");
911        assert!(err.cause().is_some());
912        assert_eq!(err.cause().unwrap().to_string(), "additional context");
913    }
914
915    #[test]
916    fn test_cuerror_with_cause_method() {
917        let io_err = std::io::Error::other("io error");
918        let err = CuError::from("base error").with_cause(io_err);
919        assert_eq!(err.message(), "base error");
920        assert!(err.cause().is_some());
921    }
922
923    #[test]
924    fn test_cuerror_with_cause_free_function() {
925        let io_err = std::io::Error::other("io error");
926        let err = with_cause("wrapped", io_err);
927        assert_eq!(err.message(), "wrapped");
928        assert!(err.cause().is_some());
929    }
930
931    #[test]
932    fn test_cuerror_clone() {
933        let io_err = std::io::Error::other("io error");
934        let err = CuError::new_with_cause("test", io_err);
935        let cloned = err.clone();
936        assert_eq!(err.message(), cloned.message());
937        // Cause string representation should match
938        assert_eq!(
939            err.cause().map(|c| c.to_string()),
940            cloned.cause().map(|c| c.to_string())
941        );
942    }
943
944    #[test]
945    fn test_cuerror_serialize_deserialize_json() {
946        let io_err = std::io::Error::other("io error");
947        let err = CuError::new_with_cause("test", io_err);
948
949        let serialized = serde_json::to_string(&err).unwrap();
950        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
951
952        assert_eq!(err.message(), deserialized.message());
953        // Cause should be preserved as string
954        assert!(deserialized.cause().is_some());
955    }
956
957    #[test]
958    fn test_cuerror_serialize_deserialize_no_cause() {
959        let err = CuError::from("simple error");
960
961        let serialized = serde_json::to_string(&err).unwrap();
962        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
963
964        assert_eq!(err.message(), deserialized.message());
965        assert!(deserialized.cause().is_none());
966    }
967
968    #[test]
969    fn test_cuerror_display() {
970        let err = CuError::from("test error").add_cause("some context");
971        let display = format!("{}", err);
972        assert!(display.contains("test error"));
973        assert!(display.contains("some context"));
974    }
975
976    #[test]
977    fn test_cuerror_debug() {
978        let err = CuError::from("test error").add_cause("some context");
979        let debug = format!("{:?}", err);
980        assert!(debug.contains("test error"));
981        assert!(debug.contains("some context"));
982    }
983
984    #[test]
985    fn debug_field_descriptor_skips_missing_binding_name_on_serialize() {
986        let descriptor = DebugFieldDescriptor {
987            display_path: "meta.process_time.start_ns".to_owned(),
988            binding_name: None,
989            value_type_path: "cu29_clock::CuTime".to_owned(),
990            scalar_kind: Some(DebugScalarKind::U64),
991            semantics: Some(DebugFieldSemantics::Time),
992            nullable: true,
993            kind: DebugFieldKind::Scalar,
994            children: Vec::new(),
995        };
996
997        let encoded = serde_json::to_value(&descriptor).unwrap();
998        assert!(encoded.get("binding_name").is_none());
999    }
1000
1001    #[test]
1002    fn debug_field_descriptor_accepts_empty_array_binding_name() {
1003        let encoded = json!({
1004            "display_path": "meta.process_time.start_ns",
1005            "binding_name": [],
1006            "value_type_path": "cu29_clock::CuTime",
1007            "semantics": "Time",
1008            "nullable": true,
1009            "kind": "scalar",
1010        });
1011
1012        let descriptor: DebugFieldDescriptor = serde_json::from_value(encoded).unwrap();
1013        assert_eq!(descriptor.binding_name, None);
1014        assert_eq!(descriptor.semantics, Some(DebugFieldSemantics::Time));
1015        assert_eq!(descriptor.scalar_kind, None);
1016        assert_eq!(descriptor.kind, DebugFieldKind::Scalar);
1017        assert!(descriptor.children.is_empty());
1018    }
1019}