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}
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}