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