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