1#![cfg_attr(not(feature = "std"), no_std)]
23extern crate alloc;
24
25#[cfg(feature = "reflect")]
26pub use bevy_reflect::Reflect;
27use bincode::de::{BorrowDecoder, Decoder};
28use bincode::enc::Encoder;
29use bincode::enc::write::Writer;
30use bincode::error::{DecodeError, EncodeError};
31use bincode::{BorrowDecode, Decode as dDecode, Decode, Encode, Encode as dEncode};
32use compact_str::CompactString;
33use cu29_clock::{PartialCuTimeRange, Tov};
34use serde::de::{self, SeqAccess, Visitor};
35use serde::{Deserialize, Deserializer, Serialize};
36
37use alloc::borrow::ToOwned;
38use alloc::boxed::Box;
39use alloc::format;
40use alloc::string::{String, ToString};
41use alloc::vec::Vec;
42#[cfg(feature = "std")]
43use core::cell::Cell;
44#[cfg(not(feature = "std"))]
45use core::error::Error as CoreError;
46use core::fmt::{Debug, Display, Formatter};
47#[cfg(feature = "std")]
48use std::error::Error;
49
50#[cfg(not(feature = "std"))]
51use spin::Mutex as SpinMutex;
52
53#[cfg(feature = "std")]
55type DynError = dyn std::error::Error + Send + Sync + 'static;
56#[cfg(not(feature = "std"))]
57type DynError = dyn core::error::Error + Send + Sync + 'static;
58
59#[derive(Debug)]
62struct StringError(String);
63
64impl Display for StringError {
65 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
66 write!(f, "{}", self.0)
67 }
68}
69
70#[cfg(feature = "std")]
71impl std::error::Error for StringError {}
72
73#[cfg(not(feature = "std"))]
74impl core::error::Error for StringError {}
75
76pub struct CuError {
82 message: String,
83 cause: Option<Box<DynError>>,
84}
85
86impl Debug for CuError {
88 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
89 f.debug_struct("CuError")
90 .field("message", &self.message)
91 .field("cause", &self.cause.as_ref().map(|e| e.to_string()))
92 .finish()
93 }
94}
95
96impl Clone for CuError {
98 fn clone(&self) -> Self {
99 CuError {
100 message: self.message.clone(),
101 cause: self
102 .cause
103 .as_ref()
104 .map(|e| Box::new(StringError(e.to_string())) as Box<DynError>),
105 }
106 }
107}
108
109impl Serialize for CuError {
111 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112 where
113 S: serde::Serializer,
114 {
115 use serde::ser::SerializeStruct;
116 let mut state = serializer.serialize_struct("CuError", 2)?;
117 state.serialize_field("message", &self.message)?;
118 state.serialize_field("cause", &self.cause.as_ref().map(|e| e.to_string()))?;
119 state.end()
120 }
121}
122
123impl<'de> Deserialize<'de> for CuError {
125 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126 where
127 D: serde::Deserializer<'de>,
128 {
129 #[derive(Deserialize)]
130 struct CuErrorHelper {
131 message: String,
132 cause: Option<String>,
133 }
134
135 let helper = CuErrorHelper::deserialize(deserializer)?;
136 Ok(CuError {
137 message: helper.message,
138 cause: helper
139 .cause
140 .map(|s| Box::new(StringError(s)) as Box<DynError>),
141 })
142 }
143}
144
145impl Display for CuError {
146 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
147 let context_str = match &self.cause {
148 Some(c) => c.to_string(),
149 None => "None".to_string(),
150 };
151 write!(f, "{}\n context:{}", self.message, context_str)?;
152 Ok(())
153 }
154}
155
156#[cfg(not(feature = "std"))]
157impl CoreError for CuError {
158 fn source(&self) -> Option<&(dyn CoreError + 'static)> {
159 self.cause
160 .as_deref()
161 .map(|e| e as &(dyn CoreError + 'static))
162 }
163}
164
165#[cfg(feature = "std")]
166impl Error for CuError {
167 fn source(&self) -> Option<&(dyn Error + 'static)> {
168 self.cause.as_deref().map(|e| e as &(dyn Error + 'static))
169 }
170}
171
172impl From<&str> for CuError {
173 fn from(s: &str) -> CuError {
174 CuError {
175 message: s.to_string(),
176 cause: None,
177 }
178 }
179}
180
181impl From<String> for CuError {
182 fn from(s: String) -> CuError {
183 CuError {
184 message: s,
185 cause: None,
186 }
187 }
188}
189
190impl CuError {
191 pub fn new(message_index: usize) -> CuError {
197 CuError {
198 message: format!("[interned:{}]", message_index),
199 cause: None,
200 }
201 }
202
203 #[cfg(feature = "std")]
213 pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
214 where
215 E: std::error::Error + Send + Sync + 'static,
216 {
217 CuError {
218 message: message.to_string(),
219 cause: Some(Box::new(cause)),
220 }
221 }
222
223 #[cfg(not(feature = "std"))]
225 pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
226 where
227 E: core::error::Error + Send + Sync + 'static,
228 {
229 CuError {
230 message: message.to_string(),
231 cause: Some(Box::new(cause)),
232 }
233 }
234
235 pub fn add_cause(mut self, context: &str) -> CuError {
246 self.cause = Some(Box::new(StringError(context.to_string())));
247 self
248 }
249
250 #[cfg(feature = "std")]
260 pub fn with_cause<E>(mut self, cause: E) -> CuError
261 where
262 E: std::error::Error + Send + Sync + 'static,
263 {
264 self.cause = Some(Box::new(cause));
265 self
266 }
267
268 #[cfg(not(feature = "std"))]
270 pub fn with_cause<E>(mut self, cause: E) -> CuError
271 where
272 E: core::error::Error + Send + Sync + 'static,
273 {
274 self.cause = Some(Box::new(cause));
275 self
276 }
277
278 pub fn cause(&self) -> Option<&(dyn core::error::Error + Send + Sync + 'static)> {
280 self.cause.as_deref()
281 }
282
283 pub fn message(&self) -> &str {
285 &self.message
286 }
287}
288
289#[cfg(feature = "std")]
301pub fn with_cause<E>(message: &str, cause: E) -> CuError
302where
303 E: std::error::Error + Send + Sync + 'static,
304{
305 CuError::new_with_cause(message, cause)
306}
307
308#[cfg(not(feature = "std"))]
310pub fn with_cause<E>(message: &str, cause: E) -> CuError
311where
312 E: core::error::Error + Send + Sync + 'static,
313{
314 CuError::new_with_cause(message, cause)
315}
316
317pub type CuResult<T> = Result<T, CuError>;
319
320#[cfg(feature = "std")]
321thread_local! {
322 static OBSERVED_ENCODE_BYTES: Cell<Option<usize>> = const { Cell::new(None) };
323}
324
325#[cfg(not(feature = "std"))]
326static OBSERVED_ENCODE_BYTES: SpinMutex<Option<usize>> = SpinMutex::new(None);
327
328pub fn begin_observed_encode() {
330 #[cfg(feature = "std")]
331 OBSERVED_ENCODE_BYTES.with(|bytes| {
332 debug_assert!(
333 bytes.get().is_none(),
334 "observed encode measurement must not be nested"
335 );
336 bytes.set(Some(0));
337 });
338
339 #[cfg(not(feature = "std"))]
340 {
341 let mut bytes = OBSERVED_ENCODE_BYTES.lock();
342 debug_assert!(
343 bytes.is_none(),
344 "observed encode measurement must not be nested"
345 );
346 *bytes = Some(0);
347 }
348}
349
350pub fn finish_observed_encode() -> usize {
352 #[cfg(feature = "std")]
353 {
354 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.replace(None).unwrap_or(0))
355 }
356
357 #[cfg(not(feature = "std"))]
358 {
359 OBSERVED_ENCODE_BYTES.lock().take().unwrap_or(0)
360 }
361}
362
363pub fn abort_observed_encode() {
365 #[cfg(feature = "std")]
366 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.set(None));
367
368 #[cfg(not(feature = "std"))]
369 {
370 *OBSERVED_ENCODE_BYTES.lock() = None;
371 }
372}
373
374pub fn observed_encode_bytes() -> usize {
376 #[cfg(feature = "std")]
377 {
378 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.get().unwrap_or(0))
379 }
380
381 #[cfg(not(feature = "std"))]
382 {
383 OBSERVED_ENCODE_BYTES.lock().as_ref().copied().unwrap_or(0)
384 }
385}
386
387pub fn record_observed_encode_bytes(bytes: usize) {
389 #[cfg(feature = "std")]
390 OBSERVED_ENCODE_BYTES.with(|total| {
391 if let Some(current) = total.get() {
392 total.set(Some(current.saturating_add(bytes)));
393 }
394 });
395
396 #[cfg(not(feature = "std"))]
397 {
398 let mut total = OBSERVED_ENCODE_BYTES.lock();
399 if let Some(current) = *total {
400 *total = Some(current.saturating_add(bytes));
401 }
402 }
403}
404
405pub struct ObservedWriter<W> {
408 inner: W,
409}
410
411impl<W> ObservedWriter<W> {
412 pub const fn new(inner: W) -> Self {
413 Self { inner }
414 }
415
416 pub fn into_inner(self) -> W {
417 self.inner
418 }
419
420 pub fn inner(&self) -> &W {
421 &self.inner
422 }
423
424 pub fn inner_mut(&mut self) -> &mut W {
425 &mut self.inner
426 }
427}
428
429impl<W: Writer> Writer for ObservedWriter<W> {
430 #[inline(always)]
431 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
432 self.inner.write(bytes)?;
433 record_observed_encode_bytes(bytes.len());
434 Ok(())
435 }
436}
437
438pub trait WriteStream<E: Encode>: Debug + Send + Sync {
440 fn log(&mut self, obj: &E) -> CuResult<()>;
441 fn flush(&mut self) -> CuResult<()> {
442 Ok(())
443 }
444 fn last_log_bytes(&self) -> Option<usize> {
446 None
447 }
448}
449
450#[derive(dEncode, dDecode, Copy, Clone, Debug, PartialEq)]
452pub enum UnifiedLogType {
453 Empty, StructuredLogLine, CopperList, FrozenTasks, LastEntry, RuntimeLifecycle, }
460pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
462
463impl Metadata for () {}
464
465#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
467#[cfg_attr(feature = "reflect", derive(Reflect))]
468pub struct CuMsgOrigin {
469 pub subsystem_code: u16,
470 pub instance_id: u32,
471 pub cl_id: u64,
472}
473
474pub trait CuMsgMetadataTrait {
476 fn process_time(&self) -> PartialCuTimeRange;
478
479 fn status_txt(&self) -> &CuCompactString;
481
482 fn origin(&self) -> Option<&CuMsgOrigin> {
484 None
485 }
486}
487
488pub trait ErasedCuStampedData {
490 fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
491 #[cfg(feature = "reflect")]
492 fn payload_reflect(&self) -> Option<&dyn Reflect>;
493 fn tov(&self) -> Tov;
494 fn metadata(&self) -> &dyn CuMsgMetadataTrait;
495}
496
497pub trait ErasedCuStampedDataSet {
500 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
501}
502
503pub trait CuPayloadRawBytes {
505 fn payload_raw_bytes(&self) -> Vec<Option<u64>>;
508}
509
510#[derive(Debug, Clone, Copy)]
515pub struct TaskOutputSpec {
516 pub task_id: &'static str,
517 pub msg_type: &'static str,
518 pub payload_type_path_fn: fn() -> &'static str,
519}
520
521impl TaskOutputSpec {
522 #[inline]
523 pub fn payload_type_path(&self) -> &'static str {
524 (self.payload_type_path_fn)()
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub enum DebugFieldSemantics {
530 Time,
531 OptionalTime,
532 Duration,
533 GeodeticPosition,
534 Quantity {
535 quantity_name: String,
536 unit_symbol: String,
537 },
538}
539
540#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
541#[serde(rename_all = "snake_case")]
542pub enum DebugFieldKind {
543 Scalar,
544 Struct,
545 TupleStruct,
546 Tuple,
547 List,
548 Array,
549 Map,
550 Set,
551 Enum,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
555pub struct DebugFieldDescriptor {
556 pub display_path: String,
557 #[serde(
558 default,
559 skip_serializing_if = "Option::is_none",
560 deserialize_with = "deserialize_debug_binding_name"
561 )]
562 pub binding_name: Option<String>,
563 pub field_type: String,
564 pub value_type_path: String,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub semantics: Option<DebugFieldSemantics>,
567 pub nullable: bool,
568 pub kind: DebugFieldKind,
569 #[serde(default, skip_serializing_if = "Vec::is_empty")]
570 pub children: Vec<DebugFieldDescriptor>,
571}
572
573fn deserialize_debug_binding_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
574where
575 D: Deserializer<'de>,
576{
577 struct BindingNameVisitor;
578
579 impl<'de> Visitor<'de> for BindingNameVisitor {
580 type Value = Option<String>;
581
582 fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583 formatter.write_str("a string, null, or an empty sequence")
584 }
585
586 fn visit_none<E>(self) -> Result<Self::Value, E>
587 where
588 E: de::Error,
589 {
590 Ok(None)
591 }
592
593 fn visit_unit<E>(self) -> Result<Self::Value, E>
594 where
595 E: de::Error,
596 {
597 Ok(None)
598 }
599
600 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
601 where
602 D: Deserializer<'de>,
603 {
604 deserialize_debug_binding_name(deserializer)
605 }
606
607 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
608 where
609 E: de::Error,
610 {
611 Ok(Some(value.to_owned()))
612 }
613
614 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
615 where
616 E: de::Error,
617 {
618 Ok(Some(value))
619 }
620
621 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
622 where
623 A: SeqAccess<'de>,
624 {
625 if seq.next_element::<de::IgnoredAny>()?.is_none() {
626 return Ok(None);
627 }
628 Err(de::Error::invalid_type(
629 de::Unexpected::Seq,
630 &"an empty sequence",
631 ))
632 }
633 }
634
635 deserializer.deserialize_any(BindingNameVisitor)
636}
637
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct DebugScalarRegistration {
640 pub type_path: &'static str,
641 pub field_type: &'static str,
642 pub semantics: DebugFieldSemantics,
643}
644
645pub trait DebugScalarType: 'static {
646 fn debug_scalar_registration() -> DebugScalarRegistration;
647}
648
649pub trait MatchingTasks {
650 fn get_all_task_ids() -> &'static [&'static str];
651
652 fn get_output_specs() -> &'static [TaskOutputSpec] {
653 &[]
654 }
655}
656
657pub trait PayloadSchemas {
666 fn get_payload_schemas() -> Vec<(&'static str, String)> {
671 Vec::new()
672 }
673}
674
675pub trait CopperListTuple:
677 bincode::Encode
678 + bincode::Decode<()>
679 + Debug
680 + Serialize
681 + ErasedCuStampedDataSet
682 + MatchingTasks
683 + Default
684{
685} impl<T> CopperListTuple for T where
689 T: bincode::Encode
690 + bincode::Decode<()>
691 + Debug
692 + Serialize
693 + ErasedCuStampedDataSet
694 + MatchingTasks
695 + Default
696{
697}
698
699pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
703
704#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
705pub struct CuCompactString(pub CompactString);
706
707impl Encode for CuCompactString {
708 fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
709 let CuCompactString(compact_string) = self;
710 let bytes = &compact_string.as_bytes();
711 bytes.encode(encoder)
712 }
713}
714
715impl Debug for CuCompactString {
716 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
717 if self.0.is_empty() {
718 return write!(f, "CuCompactString(Empty)");
719 }
720 write!(f, "CuCompactString({})", self.0)
721 }
722}
723
724impl<Context> Decode<Context> for CuCompactString {
725 fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
726 let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; let compact_string =
728 CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
729 Ok(CuCompactString(compact_string))
730 }
731}
732
733impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
734 fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
735 CuCompactString::decode(decoder)
736 }
737}
738
739#[cfg(feature = "defmt")]
740impl defmt::Format for CuError {
741 fn format(&self, f: defmt::Formatter) {
742 match &self.cause {
743 Some(c) => {
744 let cause_str = c.to_string();
745 defmt::write!(
746 f,
747 "CuError {{ message: {}, cause: {} }}",
748 defmt::Display2Format(&self.message),
749 defmt::Display2Format(&cause_str),
750 )
751 }
752 None => defmt::write!(
753 f,
754 "CuError {{ message: {}, cause: None }}",
755 defmt::Display2Format(&self.message),
756 ),
757 }
758 }
759}
760
761#[cfg(feature = "defmt")]
762impl defmt::Format for CuCompactString {
763 fn format(&self, f: defmt::Formatter) {
764 if self.0.is_empty() {
765 defmt::write!(f, "CuCompactString(Empty)");
766 } else {
767 defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
768 }
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use crate::CuCompactString;
775 use bincode::{config, decode_from_slice, encode_to_vec};
776 use compact_str::CompactString;
777
778 #[test]
779 fn test_cucompactstr_encode_decode_empty() {
780 let cstr = CuCompactString(CompactString::from(""));
781 let config = config::standard();
782 let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
783 assert_eq!(encoded.len(), 1); let (decoded, _): (CuCompactString, usize) =
785 decode_from_slice(&encoded, config).expect("Decoding failed");
786 assert_eq!(cstr.0, decoded.0);
787 }
788
789 #[test]
790 fn test_cucompactstr_encode_decode_small() {
791 let cstr = CuCompactString(CompactString::from("test"));
792 let config = config::standard();
793 let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
794 assert_eq!(encoded.len(), 5); let (decoded, _): (CuCompactString, usize) =
796 decode_from_slice(&encoded, config).expect("Decoding failed");
797 assert_eq!(cstr.0, decoded.0);
798 }
799}
800
801#[cfg(all(test, feature = "std"))]
803mod std_tests {
804 use crate::{CuError, DebugFieldDescriptor, DebugFieldKind, DebugFieldSemantics, with_cause};
805 use serde_json::json;
806
807 #[test]
808 fn test_cuerror_from_str() {
809 let err = CuError::from("test error");
810 assert_eq!(err.message(), "test error");
811 assert!(err.cause().is_none());
812 }
813
814 #[test]
815 fn test_cuerror_from_string() {
816 let err = CuError::from(String::from("test error"));
817 assert_eq!(err.message(), "test error");
818 assert!(err.cause().is_none());
819 }
820
821 #[test]
822 fn test_cuerror_new_index() {
823 let err = CuError::new(42);
824 assert_eq!(err.message(), "[interned:42]");
825 assert!(err.cause().is_none());
826 }
827
828 #[test]
829 fn test_cuerror_new_with_cause() {
830 let io_err = std::io::Error::other("io error");
831 let err = CuError::new_with_cause("wrapped error", io_err);
832 assert_eq!(err.message(), "wrapped error");
833 assert!(err.cause().is_some());
834 assert!(err.cause().unwrap().to_string().contains("io error"));
835 }
836
837 #[test]
838 fn test_cuerror_add_cause() {
839 let err = CuError::from("base error").add_cause("additional context");
840 assert_eq!(err.message(), "base error");
841 assert!(err.cause().is_some());
842 assert_eq!(err.cause().unwrap().to_string(), "additional context");
843 }
844
845 #[test]
846 fn test_cuerror_with_cause_method() {
847 let io_err = std::io::Error::other("io error");
848 let err = CuError::from("base error").with_cause(io_err);
849 assert_eq!(err.message(), "base error");
850 assert!(err.cause().is_some());
851 }
852
853 #[test]
854 fn test_cuerror_with_cause_free_function() {
855 let io_err = std::io::Error::other("io error");
856 let err = with_cause("wrapped", io_err);
857 assert_eq!(err.message(), "wrapped");
858 assert!(err.cause().is_some());
859 }
860
861 #[test]
862 fn test_cuerror_clone() {
863 let io_err = std::io::Error::other("io error");
864 let err = CuError::new_with_cause("test", io_err);
865 let cloned = err.clone();
866 assert_eq!(err.message(), cloned.message());
867 assert_eq!(
869 err.cause().map(|c| c.to_string()),
870 cloned.cause().map(|c| c.to_string())
871 );
872 }
873
874 #[test]
875 fn test_cuerror_serialize_deserialize_json() {
876 let io_err = std::io::Error::other("io error");
877 let err = CuError::new_with_cause("test", io_err);
878
879 let serialized = serde_json::to_string(&err).unwrap();
880 let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
881
882 assert_eq!(err.message(), deserialized.message());
883 assert!(deserialized.cause().is_some());
885 }
886
887 #[test]
888 fn test_cuerror_serialize_deserialize_no_cause() {
889 let err = CuError::from("simple error");
890
891 let serialized = serde_json::to_string(&err).unwrap();
892 let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
893
894 assert_eq!(err.message(), deserialized.message());
895 assert!(deserialized.cause().is_none());
896 }
897
898 #[test]
899 fn test_cuerror_display() {
900 let err = CuError::from("test error").add_cause("some context");
901 let display = format!("{}", err);
902 assert!(display.contains("test error"));
903 assert!(display.contains("some context"));
904 }
905
906 #[test]
907 fn test_cuerror_debug() {
908 let err = CuError::from("test error").add_cause("some context");
909 let debug = format!("{:?}", err);
910 assert!(debug.contains("test error"));
911 assert!(debug.contains("some context"));
912 }
913
914 #[test]
915 fn debug_field_descriptor_skips_missing_binding_name_on_serialize() {
916 let descriptor = DebugFieldDescriptor {
917 display_path: "meta.process_time.start_ns".to_owned(),
918 binding_name: None,
919 field_type: "integer".to_owned(),
920 value_type_path: "cu29_clock::CuTime".to_owned(),
921 semantics: Some(DebugFieldSemantics::Time),
922 nullable: true,
923 kind: DebugFieldKind::Scalar,
924 children: Vec::new(),
925 };
926
927 let encoded = serde_json::to_value(&descriptor).unwrap();
928 assert!(encoded.get("binding_name").is_none());
929 }
930
931 #[test]
932 fn debug_field_descriptor_accepts_empty_array_binding_name() {
933 let encoded = json!({
934 "display_path": "meta.process_time.start_ns",
935 "binding_name": [],
936 "field_type": "integer",
937 "value_type_path": "cu29_clock::CuTime",
938 "semantics": "Time",
939 "nullable": true,
940 "kind": "scalar",
941 });
942
943 let descriptor: DebugFieldDescriptor = serde_json::from_value(encoded).unwrap();
944 assert_eq!(descriptor.binding_name, None);
945 assert_eq!(descriptor.semantics, Some(DebugFieldSemantics::Time));
946 assert_eq!(descriptor.kind, DebugFieldKind::Scalar);
947 assert!(descriptor.children.is_empty());
948 }
949}