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    StreamContinuity,  // Received archive provenance, gaps and verified restart boundaries.
462}
463/// Represent the minimum set of traits to be usable as Metadata in Copper.
464pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
465
466impl Metadata for () {}
467
468/// Origin metadata captured when a Copper-aware transport receives a remote message.
469#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
470#[cfg_attr(feature = "reflect", derive(Reflect))]
471pub struct CuMsgOrigin {
472    pub subsystem_code: u16,
473    pub instance_id: u32,
474    pub cl_id: u64,
475}
476
477/// Key metadata piece attached to every message in Copper.
478pub trait CuMsgMetadataTrait {
479    /// The time range used for the processing of this message
480    fn process_time(&self) -> PartialCuTimeRange;
481
482    /// Small status text for user UI to get the realtime state of task (max 24 chrs)
483    fn status_txt(&self) -> &CuCompactString;
484
485    /// Remote Copper provenance captured on receive.
486    fn origin(&self) -> Option<&CuMsgOrigin> {
487        None
488    }
489}
490
491/// A generic trait to expose the generated CuStampedDataSet from the task graph.
492pub trait ErasedCuStampedData {
493    fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
494    #[cfg(feature = "reflect")]
495    fn payload_reflect(&self) -> Option<&dyn Reflect>;
496    fn tov(&self) -> Tov;
497    fn metadata(&self) -> &dyn CuMsgMetadataTrait;
498}
499
500/// Trait to get a vector of type-erased CuStampedDataSet
501/// This is used for generic serialization of the copperlists
502pub trait ErasedCuStampedDataSet {
503    fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
504}
505
506/// Provides per-output raw payload sizes aligned with `ErasedCuStampedDataSet::cumsgs` order.
507pub trait CuPayloadRawBytes {
508    /// Returns raw payload sizes (stack + heap) for each output message.
509    /// `None` indicates the payload was not produced for that output.
510    fn payload_raw_bytes(&self) -> Vec<Option<u64>>;
511}
512
513/// Trait to trace back from the CopperList the origin of each message slot.
514///
515/// The returned slice must be aligned with `ErasedCuStampedDataSet::cumsgs()`:
516/// index `i` maps to copperlist slot `i`.
517#[derive(Debug, Clone, Copy)]
518pub struct TaskOutputSpec {
519    pub task_id: &'static str,
520    pub msg_type: &'static str,
521    pub payload_type_path_fn: fn() -> &'static str,
522    #[cfg(feature = "reflect")]
523    payload_type_registration_fn: fn(&mut TypeRegistry),
524}
525
526impl TaskOutputSpec {
527    #[cfg(feature = "reflect")]
528    pub const fn new<T>(task_id: &'static str, msg_type: &'static str) -> Self
529    where
530        T: GetTypeRegistration + TypePath,
531    {
532        Self {
533            task_id,
534            msg_type,
535            payload_type_path_fn: payload_type_path::<T>,
536            payload_type_registration_fn: register_payload_type::<T>,
537        }
538    }
539
540    #[cfg(not(feature = "reflect"))]
541    pub const fn new<T>(task_id: &'static str, msg_type: &'static str) -> Self {
542        Self {
543            task_id,
544            msg_type,
545            payload_type_path_fn: payload_type_path::<T>,
546        }
547    }
548
549    #[inline]
550    pub fn payload_type_path(&self) -> &'static str {
551        (self.payload_type_path_fn)()
552    }
553
554    #[cfg(feature = "reflect")]
555    #[inline]
556    pub fn register_payload_type(&self, registry: &mut TypeRegistry) {
557        (self.payload_type_registration_fn)(registry);
558    }
559}
560
561#[cfg(feature = "reflect")]
562fn payload_type_path<T: TypePath>() -> &'static str {
563    T::type_path()
564}
565
566#[cfg(not(feature = "reflect"))]
567fn payload_type_path<T>() -> &'static str {
568    core::any::type_name::<T>()
569}
570
571#[cfg(feature = "reflect")]
572fn register_payload_type<T: GetTypeRegistration>(registry: &mut TypeRegistry) {
573    registry.register::<T>();
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
577pub enum DebugFieldSemantics {
578    Time,
579    OptionalTime,
580    Duration,
581    GeodeticPosition,
582    Quantity {
583        quantity_name: String,
584        unit_symbol: String,
585    },
586}
587
588#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
589#[serde(rename_all = "snake_case")]
590pub enum DebugFieldKind {
591    Scalar,
592    Struct,
593    TupleStruct,
594    Tuple,
595    List,
596    Array,
597    Map,
598    Set,
599    Enum,
600}
601
602#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
603#[serde(rename_all = "snake_case")]
604pub enum DebugScalarKind {
605    Bool,
606    I8,
607    I16,
608    I32,
609    I64,
610    I128,
611    Isize,
612    U8,
613    U16,
614    U32,
615    U64,
616    U128,
617    Usize,
618    F32,
619    F64,
620    Char,
621    String,
622}
623
624impl DebugScalarKind {
625    pub const fn type_name(self) -> &'static str {
626        match self {
627            Self::Bool => "bool",
628            Self::I8 => "i8",
629            Self::I16 => "i16",
630            Self::I32 => "i32",
631            Self::I64 => "i64",
632            Self::I128 => "i128",
633            Self::Isize => "isize",
634            Self::U8 => "u8",
635            Self::U16 => "u16",
636            Self::U32 => "u32",
637            Self::U64 => "u64",
638            Self::U128 => "u128",
639            Self::Usize => "usize",
640            Self::F32 => "f32",
641            Self::F64 => "f64",
642            Self::Char => "char",
643            Self::String => "String",
644        }
645    }
646
647    pub const fn is_numeric(self) -> bool {
648        matches!(
649            self,
650            Self::I8
651                | Self::I16
652                | Self::I32
653                | Self::I64
654                | Self::I128
655                | Self::Isize
656                | Self::U8
657                | Self::U16
658                | Self::U32
659                | Self::U64
660                | Self::U128
661                | Self::Usize
662                | Self::F32
663                | Self::F64
664        )
665    }
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
669#[serde(rename_all = "snake_case")]
670pub enum DebugEnumVariantKind {
671    Unit,
672    Tuple,
673    Struct,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
677pub struct DebugEnumVariantDescriptor {
678    pub name: String,
679    pub kind: DebugEnumVariantKind,
680    #[serde(default, skip_serializing_if = "Vec::is_empty")]
681    pub fields: Vec<DebugFieldDescriptor>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685pub struct DebugFieldDescriptor {
686    pub display_path: String,
687    #[serde(
688        default,
689        skip_serializing_if = "Option::is_none",
690        deserialize_with = "deserialize_debug_binding_name"
691    )]
692    pub binding_name: Option<String>,
693    pub value_type_path: String,
694    #[serde(default, skip_serializing_if = "Option::is_none")]
695    pub scalar_kind: Option<DebugScalarKind>,
696    #[serde(default, skip_serializing_if = "Option::is_none")]
697    pub semantics: Option<DebugFieldSemantics>,
698    pub nullable: bool,
699    pub kind: DebugFieldKind,
700    #[serde(default, skip_serializing_if = "Vec::is_empty")]
701    pub children: Vec<DebugFieldDescriptor>,
702    #[serde(default, skip_serializing_if = "Option::is_none")]
703    pub map_key: Option<Box<DebugFieldDescriptor>>,
704    #[serde(default, skip_serializing_if = "Option::is_none")]
705    pub map_value: Option<Box<DebugFieldDescriptor>>,
706    #[serde(default, skip_serializing_if = "Vec::is_empty")]
707    pub enum_variants: Vec<DebugEnumVariantDescriptor>,
708}
709
710fn deserialize_debug_binding_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
711where
712    D: Deserializer<'de>,
713{
714    struct BindingNameVisitor;
715
716    impl<'de> Visitor<'de> for BindingNameVisitor {
717        type Value = Option<String>;
718
719        fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
720            formatter.write_str("a string, null, or an empty sequence")
721        }
722
723        fn visit_none<E>(self) -> Result<Self::Value, E>
724        where
725            E: de::Error,
726        {
727            Ok(None)
728        }
729
730        fn visit_unit<E>(self) -> Result<Self::Value, E>
731        where
732            E: de::Error,
733        {
734            Ok(None)
735        }
736
737        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
738        where
739            D: Deserializer<'de>,
740        {
741            deserialize_debug_binding_name(deserializer)
742        }
743
744        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
745        where
746            E: de::Error,
747        {
748            Ok(Some(value.to_owned()))
749        }
750
751        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
752        where
753            E: de::Error,
754        {
755            Ok(Some(value))
756        }
757
758        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
759        where
760            A: SeqAccess<'de>,
761        {
762            if seq.next_element::<de::IgnoredAny>()?.is_none() {
763                return Ok(None);
764            }
765            Err(de::Error::invalid_type(
766                de::Unexpected::Seq,
767                &"an empty sequence",
768            ))
769        }
770    }
771
772    deserializer.deserialize_any(BindingNameVisitor)
773}
774
775#[derive(Debug, Clone, PartialEq, Eq)]
776pub struct DebugScalarRegistration {
777    pub type_path: &'static str,
778    pub scalar_kind: DebugScalarKind,
779    pub semantics: DebugFieldSemantics,
780}
781
782pub trait DebugScalarType: 'static {
783    fn debug_scalar_registration() -> DebugScalarRegistration;
784}
785
786pub trait MatchingTasks {
787    fn get_all_task_ids() -> &'static [&'static str];
788
789    fn get_output_specs() -> &'static [TaskOutputSpec] {
790        &[]
791    }
792}
793
794/// Describes the serialized JSON representation of a reusable payload type.
795///
796/// Implement this next to a payload's [`Serialize`] implementation when its
797/// wire representation cannot be inferred accurately from reflected fields.
798/// Add `SerializedPayloadSchema` to the type's `#[reflect(...)]` attribute so
799/// reflection-based exporters discover the implementation automatically.
800/// Exporters can then consume the schema without the payload depending on a
801/// particular export format such as MCAP.
802pub trait SerializedPayloadSchema {
803    /// Returns a JSON Schema for the value emitted by [`Serialize`].
804    fn serialized_payload_schema() -> &'static str;
805}
806
807/// Reflected type metadata for [`SerializedPayloadSchema`].
808#[derive(Clone, Copy)]
809pub struct ReflectSerializedPayloadSchema {
810    schema_fn: fn() -> &'static str,
811}
812
813impl ReflectSerializedPayloadSchema {
814    pub fn schema(&self) -> &'static str {
815        (self.schema_fn)()
816    }
817}
818
819#[cfg(feature = "reflect")]
820impl<T> bevy_reflect::FromType<T> for ReflectSerializedPayloadSchema
821where
822    T: SerializedPayloadSchema,
823{
824    fn from_type() -> Self {
825        Self {
826            schema_fn: T::serialized_payload_schema,
827        }
828    }
829}
830
831/// Trait for providing JSON schemas for CopperList payload types.
832///
833/// This legacy hook remains available to callers that manage explicit schema
834/// lists. Generated logreaders use [`MatchingTasks::get_output_specs`] so MCAP
835/// export does not require an application-maintained implementation.
836///
837/// The default implementation returns an empty vector for backwards compatibility
838/// with code that doesn't need MCAP export support.
839#[deprecated(
840    since = "1.2.0",
841    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"
842)]
843pub trait PayloadSchemas {
844    /// Returns a vector of (task_id, schema_json) pairs.
845    ///
846    /// Each entry corresponds to a CopperList output slot, in slot order.
847    /// The schema is a JSON Schema string generated from the payload type.
848    fn get_payload_schemas() -> Vec<(&'static str, String)> {
849        Vec::new()
850    }
851}
852
853/// A CopperListTuple needs to be encodable, decodable and fixed size in memory.
854pub trait CopperListTuple:
855    bincode::Encode
856    + bincode::Decode<()>
857    + Debug
858    + Serialize
859    + ErasedCuStampedDataSet
860    + MatchingTasks
861    + Default
862{
863} // Decode forces Sized already
864
865// Also anything that follows this contract can be a payload (blanket implementation)
866impl<T> CopperListTuple for T where
867    T: bincode::Encode
868        + bincode::Decode<()>
869        + Debug
870        + Serialize
871        + ErasedCuStampedDataSet
872        + MatchingTasks
873        + Default
874{
875}
876
877// We use this type to convey very small status messages.
878// MAX_SIZE from their repr module is not accessible so we need to copy paste their definition for 24
879// which is the maximum size for inline allocation (no heap)
880pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
881
882#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
883pub struct CuCompactString(pub CompactString);
884
885impl Encode for CuCompactString {
886    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
887        let CuCompactString(compact_string) = self;
888        let bytes = &compact_string.as_bytes();
889        bytes.encode(encoder)
890    }
891}
892
893impl Debug for CuCompactString {
894    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
895        if self.0.is_empty() {
896            return write!(f, "CuCompactString(Empty)");
897        }
898        write!(f, "CuCompactString({})", self.0)
899    }
900}
901
902impl<Context> Decode<Context> for CuCompactString {
903    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
904        let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; // Decode into a byte buffer
905        let compact_string =
906            CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
907        Ok(CuCompactString(compact_string))
908    }
909}
910
911impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
912    fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
913        CuCompactString::decode(decoder)
914    }
915}
916
917#[cfg(feature = "defmt")]
918impl defmt::Format for CuError {
919    fn format(&self, f: defmt::Formatter) {
920        match &self.cause {
921            Some(c) => {
922                let cause_str = c.to_string();
923                defmt::write!(
924                    f,
925                    "CuError {{ message: {}, cause: {} }}",
926                    defmt::Display2Format(&self.message),
927                    defmt::Display2Format(&cause_str),
928                )
929            }
930            None => defmt::write!(
931                f,
932                "CuError {{ message: {}, cause: None }}",
933                defmt::Display2Format(&self.message),
934            ),
935        }
936    }
937}
938
939#[cfg(feature = "defmt")]
940impl defmt::Format for CuCompactString {
941    fn format(&self, f: defmt::Formatter) {
942        if self.0.is_empty() {
943            defmt::write!(f, "CuCompactString(Empty)");
944        } else {
945            defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
946        }
947    }
948}
949
950#[cfg(test)]
951mod tests {
952    use crate::CuCompactString;
953    use bincode::{config, decode_from_slice, encode_to_vec};
954    use compact_str::CompactString;
955
956    #[test]
957    fn test_cucompactstr_encode_decode_empty() {
958        let cstr = CuCompactString(CompactString::from(""));
959        let config = config::standard();
960        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
961        assert_eq!(encoded.len(), 1); // This encodes the usize 0 in variable encoding so 1 byte which is 0.
962        let (decoded, _): (CuCompactString, usize) =
963            decode_from_slice(&encoded, config).expect("Decoding failed");
964        assert_eq!(cstr.0, decoded.0);
965    }
966
967    #[test]
968    fn test_cucompactstr_encode_decode_small() {
969        let cstr = CuCompactString(CompactString::from("test"));
970        let config = config::standard();
971        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
972        assert_eq!(encoded.len(), 5); // This encodes a 4-byte string "test" plus 1 byte for the length prefix.
973        let (decoded, _): (CuCompactString, usize) =
974            decode_from_slice(&encoded, config).expect("Decoding failed");
975        assert_eq!(cstr.0, decoded.0);
976    }
977}
978
979// Tests that require std feature
980#[cfg(all(test, feature = "std"))]
981mod std_tests {
982    use crate::{
983        CuError, DebugFieldDescriptor, DebugFieldKind, DebugFieldSemantics, DebugScalarKind,
984        with_cause,
985    };
986    use serde_json::json;
987
988    #[test]
989    fn test_cuerror_from_str() {
990        let err = CuError::from("test error");
991        assert_eq!(err.message(), "test error");
992        assert!(err.cause().is_none());
993    }
994
995    #[test]
996    fn test_cuerror_from_string() {
997        let err = CuError::from(String::from("test error"));
998        assert_eq!(err.message(), "test error");
999        assert!(err.cause().is_none());
1000    }
1001
1002    #[test]
1003    fn test_cuerror_new_index() {
1004        let err = CuError::new(42);
1005        assert_eq!(err.message(), "[interned:42]");
1006        assert!(err.cause().is_none());
1007    }
1008
1009    #[test]
1010    fn test_cuerror_new_with_cause() {
1011        let io_err = std::io::Error::other("io error");
1012        let err = CuError::new_with_cause("wrapped error", io_err);
1013        assert_eq!(err.message(), "wrapped error");
1014        assert!(err.cause().is_some());
1015        assert!(err.cause().unwrap().to_string().contains("io error"));
1016    }
1017
1018    #[test]
1019    fn test_cuerror_add_cause() {
1020        let err = CuError::from("base error").add_cause("additional context");
1021        assert_eq!(err.message(), "base error");
1022        assert!(err.cause().is_some());
1023        assert_eq!(err.cause().unwrap().to_string(), "additional context");
1024    }
1025
1026    #[test]
1027    fn test_cuerror_with_cause_method() {
1028        let io_err = std::io::Error::other("io error");
1029        let err = CuError::from("base error").with_cause(io_err);
1030        assert_eq!(err.message(), "base error");
1031        assert!(err.cause().is_some());
1032    }
1033
1034    #[test]
1035    fn test_cuerror_with_cause_free_function() {
1036        let io_err = std::io::Error::other("io error");
1037        let err = with_cause("wrapped", io_err);
1038        assert_eq!(err.message(), "wrapped");
1039        assert!(err.cause().is_some());
1040    }
1041
1042    #[test]
1043    fn test_cuerror_clone() {
1044        let io_err = std::io::Error::other("io error");
1045        let err = CuError::new_with_cause("test", io_err);
1046        let cloned = err.clone();
1047        assert_eq!(err.message(), cloned.message());
1048        // Cause string representation should match
1049        assert_eq!(
1050            err.cause().map(|c| c.to_string()),
1051            cloned.cause().map(|c| c.to_string())
1052        );
1053    }
1054
1055    #[test]
1056    fn test_cuerror_serialize_deserialize_json() {
1057        let io_err = std::io::Error::other("io error");
1058        let err = CuError::new_with_cause("test", io_err);
1059
1060        let serialized = serde_json::to_string(&err).unwrap();
1061        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
1062
1063        assert_eq!(err.message(), deserialized.message());
1064        // Cause should be preserved as string
1065        assert!(deserialized.cause().is_some());
1066    }
1067
1068    #[test]
1069    fn test_cuerror_serialize_deserialize_no_cause() {
1070        let err = CuError::from("simple error");
1071
1072        let serialized = serde_json::to_string(&err).unwrap();
1073        let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
1074
1075        assert_eq!(err.message(), deserialized.message());
1076        assert!(deserialized.cause().is_none());
1077    }
1078
1079    #[test]
1080    fn test_cuerror_display() {
1081        let err = CuError::from("test error").add_cause("some context");
1082        let display = err.to_string();
1083        assert!(display.contains("test error"));
1084        assert!(display.contains("some context"));
1085    }
1086
1087    #[test]
1088    fn test_cuerror_debug() {
1089        let err = CuError::from("test error").add_cause("some context");
1090        let debug = format!("{:?}", err);
1091        assert!(debug.contains("test error"));
1092        assert!(debug.contains("some context"));
1093    }
1094
1095    #[test]
1096    fn debug_field_descriptor_skips_missing_binding_name_on_serialize() {
1097        let descriptor = DebugFieldDescriptor {
1098            display_path: "meta.process_time.start_ns".to_owned(),
1099            binding_name: None,
1100            value_type_path: "cu29_clock::CuTime".to_owned(),
1101            scalar_kind: Some(DebugScalarKind::U64),
1102            semantics: Some(DebugFieldSemantics::Time),
1103            nullable: true,
1104            kind: DebugFieldKind::Scalar,
1105            children: Vec::new(),
1106            map_key: None,
1107            map_value: None,
1108            enum_variants: Vec::new(),
1109        };
1110
1111        let encoded = serde_json::to_value(&descriptor).unwrap();
1112        assert!(encoded.get("binding_name").is_none());
1113    }
1114
1115    #[test]
1116    fn debug_field_descriptor_accepts_empty_array_binding_name() {
1117        let encoded = json!({
1118            "display_path": "meta.process_time.start_ns",
1119            "binding_name": [],
1120            "value_type_path": "cu29_clock::CuTime",
1121            "semantics": "Time",
1122            "nullable": true,
1123            "kind": "scalar",
1124        });
1125
1126        let descriptor: DebugFieldDescriptor = serde_json::from_value(encoded).unwrap();
1127        assert_eq!(descriptor.binding_name, None);
1128        assert_eq!(descriptor.semantics, Some(DebugFieldSemantics::Time));
1129        assert_eq!(descriptor.scalar_kind, None);
1130        assert_eq!(descriptor.kind, DebugFieldKind::Scalar);
1131        assert!(descriptor.children.is_empty());
1132    }
1133}