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