hyperactor_config/
attrs.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Attribute dictionary for type-safe, heterogeneous key-value storage with serde support.
10//!
11//! This module provides `Attrs`, a type-safe dictionary that can store heterogeneous values
12//! and serialize/deserialize them using serde. All stored values must implement
13//! `AttrValue` to ensure the entire dictionary can be serialized.
14//!
15//! Keys are automatically registered at compile time using the `declare_attrs!` macro and the
16//! inventory crate, eliminating the need for manual registry management.
17//!
18//! # Basic Usage
19//!
20//! ```
21//! use std::time::Duration;
22//!
23//! use hyperactor_config::attrs::Attrs;
24//! use hyperactor_config::attrs::declare_attrs;
25//!
26//! // Declare keys with their associated types
27//! declare_attrs! {
28//!    /// Request timeout
29//!    attr TIMEOUT: Duration;
30//!
31//!   /// Maximum retry count
32//!   attr MAX_RETRIES: u32 = 3;  // with default value
33//! }
34//!
35//! let mut attrs = Attrs::new();
36//! attrs.set(TIMEOUT, Duration::from_secs(30));
37//!
38//! assert_eq!(attrs.get(TIMEOUT), Some(&Duration::from_secs(30)));
39//! assert_eq!(attrs.get(MAX_RETRIES), Some(&3));
40//! ```
41//!
42//! # Serialization
43//!
44//! `Attrs` can be serialized to and deserialized automatically:
45//!
46//! ```
47//! use std::time::Duration;
48//!
49//! use hyperactor_config::attrs::Attrs;
50//! use hyperactor_config::attrs::declare_attrs;
51//!
52//! declare_attrs! {
53//!   /// Request timeout
54//!   pub attr TIMEOUT: Duration;
55//! }
56//!
57//! let mut attrs = Attrs::new();
58//! attrs.set(TIMEOUT, Duration::from_secs(30));
59//!
60//! // Serialize to JSON
61//! let json = serde_json::to_string(&attrs).unwrap();
62//!
63//! // Deserialize from JSON (no manual registry needed!)
64//! let deserialized: Attrs = serde_json::from_str(&json).unwrap();
65//!
66//! assert_eq!(deserialized.get(TIMEOUT), Some(&Duration::from_secs(30)));
67//! ```
68//!
69//! ## Meta attributes
70//!
71//! An attribute can be assigned a set of attribute values,
72//! associated with the attribute key. These are specified by
73//! @-annotations in the `declare_attrs!` macro:
74//!
75//! ```
76//! use std::time::Duration;
77//!
78//! use hyperactor_config::attrs::Attrs;
79//! use hyperactor_config::attrs::declare_attrs;
80//!
81//! declare_attrs! {
82//!   /// Is experimental?
83//!   pub attr EXPERIMENTAL: bool;
84//!
85//!   /// Request timeout
86//!   @meta(EXPERIMENTAL = true)
87//!   pub attr TIMEOUT: Duration;
88//! }
89//!
90//! assert!(TIMEOUT.attrs().get(EXPERIMENTAL).unwrap());
91//! ```
92//!
93//! Meta attributes can be used to provide more generic functionality
94//! on top of the basic attributes. For example, a library can use
95//! meta-attributes to specify the behavior of an attribute.
96
97use std::any::Any;
98use std::collections::HashMap;
99use std::fmt::Display;
100use std::ops::Index;
101use std::ops::IndexMut;
102use std::str::FromStr;
103use std::sync::LazyLock;
104
105use chrono::DateTime;
106use chrono::Utc;
107use erased_serde::Deserializer as ErasedDeserializer;
108use erased_serde::Serialize as ErasedSerialize;
109use serde::Deserialize;
110use serde::Deserializer;
111use serde::Serialize;
112use serde::Serializer;
113use serde::de::DeserializeOwned;
114use serde::de::MapAccess;
115use serde::de::Visitor;
116use serde::ser::SerializeMap;
117use typeuri::Named;
118
119// Information about an attribute key, used for automatic registration.
120// This needs to be public to be accessible from other crates, but it is
121// not part of the public API.
122#[doc(hidden)]
123pub struct AttrKeyInfo {
124    /// Name of the key
125    pub name: &'static str,
126    /// Unique hash for the key (FNV-1a hash of name)
127    pub key_hash: u64,
128    /// Function to get the type hash of the associated value type
129    pub typehash: fn() -> u64,
130    /// Deserializer function that deserializes directly from any deserializer
131    pub deserialize_erased:
132        fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
133    /// Deserializer function for bincode bytes (used by Flattrs)
134    pub deserialize_bincode:
135        fn(&[u8]) -> Result<Box<dyn SerializableValue>, Box<bincode::ErrorKind>>,
136    /// Meta-attributes.
137    pub meta: &'static LazyLock<Attrs>,
138    /// Display an attribute value using AttrValue::display.
139    pub display: fn(&dyn SerializableValue) -> String,
140    /// Parse an attribute value using AttrValue::parse.
141    pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
142    /// Default value for the attribute, if any.
143    pub default: Option<&'static dyn SerializableValue>,
144    /// A reference to the relevant key object with the associated
145    /// type parameter erased. Can be downcast to a concrete Key<T>.
146    pub erased: &'static dyn ErasedKey,
147}
148
149inventory::collect!(AttrKeyInfo);
150
151/// Look up a key info by its hash using the global registry.
152///
153/// Returns `None` if no key with this hash is registered.
154/// Uses a lazy-initialized hash map for O(1) lookup after first access.
155pub fn lookup_key_info(key_hash: u64) -> Option<&'static AttrKeyInfo> {
156    static KEYS_BY_HASH: std::sync::LazyLock<std::collections::HashMap<u64, &'static AttrKeyInfo>> =
157        std::sync::LazyLock::new(|| {
158            inventory::iter::<AttrKeyInfo>()
159                .map(|info| (info.key_hash, info))
160                .collect()
161        });
162    KEYS_BY_HASH.get(&key_hash).copied()
163}
164
165/// A typed key for the attribute dictionary.
166///
167/// Each key is associated with a specific type T and has a unique name.
168/// Keys are typically created using the `declare_attrs!` macro which ensures they have
169/// static lifetime and automatically registers them for serialization.
170pub struct Key<T: 'static> {
171    name: &'static str,
172    default_value: Option<&'static T>,
173    attrs: &'static LazyLock<Attrs>,
174}
175
176impl<T> Key<T> {
177    /// Returns the name of this key.
178    pub fn name(&self) -> &'static str {
179        self.name
180    }
181
182    /// Returns a unique ID for this key, computed as FNV-1a hash of the name.
183    ///
184    /// This ID is stable and can be used for efficient wire transmission
185    /// instead of the full key name string.
186    pub const fn key_hash(&self) -> u64 {
187        fnv1a_hash(self.name.as_bytes())
188    }
189}
190
191/// Compute FNV-1a hash of a byte slice (const-compatible).
192///
193/// FNV-1a is a simple, fast hash with good distribution properties.
194/// We use 64-bit version for low collision probability.
195pub const fn fnv1a_hash(bytes: &[u8]) -> u64 {
196    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
197    const FNV_PRIME: u64 = 0x100000001b3;
198
199    let mut hash = FNV_OFFSET_BASIS;
200    let mut i = 0;
201    while i < bytes.len() {
202        hash ^= bytes[i] as u64;
203        hash = hash.wrapping_mul(FNV_PRIME);
204        i += 1;
205    }
206    hash
207}
208
209impl<T: Named + 'static> Key<T> {
210    /// Creates a new key with the given name.
211    pub const fn new(
212        name: &'static str,
213        default_value: Option<&'static T>,
214        attrs: &'static LazyLock<Attrs>,
215    ) -> Self {
216        Self {
217            name,
218            default_value,
219            attrs,
220        }
221    }
222
223    /// Returns a reference to the default value for this key, if one exists.
224    pub fn default(&self) -> Option<&'static T> {
225        self.default_value
226    }
227
228    /// Returns whether this key has a default value.
229    pub fn has_default(&self) -> bool {
230        self.default_value.is_some()
231    }
232
233    /// Returns the type hash of the associated value type.
234    pub fn typehash(&self) -> u64 {
235        T::typehash()
236    }
237
238    /// The attributes associated with this key.
239    pub fn attrs(&self) -> &'static LazyLock<Attrs> {
240        self.attrs
241    }
242}
243
244impl<T: 'static> Clone for Key<T> {
245    fn clone(&self) -> Self {
246        // Use Copy.
247        *self
248    }
249}
250
251impl<T: 'static> Copy for Key<T> {}
252
253/// A trait for type-erased keys.
254pub trait ErasedKey: Any + Send + Sync + 'static {
255    /// The name of the key.
256    fn name(&self) -> &'static str;
257
258    /// The unique ID of the key (FNV-1a hash of name).
259    fn key_hash(&self) -> u64;
260
261    /// The typehash of the key's associated type.
262    fn typehash(&self) -> u64;
263
264    /// The typename of the key's associated type.
265    fn typename(&self) -> &'static str;
266}
267
268impl dyn ErasedKey {
269    /// Downcast a type-erased key to a specific key type.
270    pub fn downcast_ref<T: Named + 'static>(&'static self) -> Option<&'static Key<T>> {
271        (self as &dyn Any).downcast_ref::<Key<T>>()
272    }
273}
274
275impl<T: AttrValue> ErasedKey for Key<T> {
276    fn name(&self) -> &'static str {
277        self.name
278    }
279
280    fn key_hash(&self) -> u64 {
281        self.key_hash()
282    }
283
284    fn typehash(&self) -> u64 {
285        T::typehash()
286    }
287
288    fn typename(&self) -> &'static str {
289        T::typename()
290    }
291}
292
293// Enable attr[key] syntax.
294impl<T: AttrValue> Index<Key<T>> for Attrs {
295    type Output = T;
296
297    fn index(&self, key: Key<T>) -> &Self::Output {
298        self.get(key).unwrap()
299    }
300}
301
302// TODO: separately type keys with defaults, so that we can statically enforce that indexmut is only
303// called on keys with defaults.
304impl<T: AttrValue> IndexMut<Key<T>> for Attrs {
305    fn index_mut(&mut self, key: Key<T>) -> &mut Self::Output {
306        self.get_mut(key).unwrap()
307    }
308}
309
310/// This trait must be implemented by all attribute values. In addition to enforcing
311/// the supertrait `Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone`,
312/// `AttrValue` requires that the type be representable in "display" format.
313///
314/// `AttrValue` includes its own `display` and `parse` so that behavior can be tailored
315/// for attribute purposes specifically, allowing common types like `Duration` to be used
316/// without modification.
317///
318/// This crate includes a derive macro for AttrValue, which uses the type's
319/// `std::string::ToString` for display, and `std::str::FromStr` for parsing.
320pub trait AttrValue:
321    Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone + 'static
322{
323    /// Display the value, typically using [`std::fmt::Display`].
324    /// This is called to show the output in human-readable form.
325    fn display(&self) -> String;
326
327    /// Parse a value from a string, typically using [`std::str::FromStr`].
328    fn parse(value: &str) -> Result<Self, anyhow::Error>;
329}
330
331/// Marker trait for types that implement a "display" that never produces an
332/// empty string. This allows Option<T> where T: DisplayNonEmpty to not require
333/// a Some(...) wrapper, and can use the empty string as the "None" value.
334trait DisplayNonEmpty {}
335
336/// Macro to implement AttrValue for types that implement ToString and FromStr.
337///
338/// This macro provides a convenient way to implement AttrValue for types that already
339/// have string conversion capabilities through the standard ToString and FromStr traits.
340///
341/// # Usage
342///
343/// ```ignore
344/// impl_attrvalue!(i32, u64, f64);
345/// ```
346///
347/// This will generate AttrValue implementations for i32, u64, and f64 that use
348/// their ToString and FromStr implementations for display and parsing.
349#[macro_export]
350macro_rules! impl_attrvalue {
351    ($($ty:ty),+ $(,)?) => {
352        $(
353            impl $crate::attrs::AttrValue for $ty {
354                fn display(&self) -> String {
355                    self.to_string()
356                }
357
358                fn parse(value: &str) -> Result<Self, anyhow::Error> {
359                    value.parse().map_err(|e| anyhow::anyhow!("failed to parse {}: {}", stringify!($ty), e))
360                }
361            }
362        )+
363    };
364}
365
366#[macro_export]
367macro_rules! impl_attrvalue_and_display_non_empty {
368    ($($ty:ty),+ $(,)?) => {
369        $(
370            impl_attrvalue!($ty);
371            impl $crate::attrs::DisplayNonEmpty for $ty {}
372        )+
373    };
374}
375
376// pub use impl_attrvalue;
377
378// Implement AttrValue for common standard library types
379impl_attrvalue_and_display_non_empty!(
380    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64,
381);
382
383// String has a display that can produce an empty string, so it cannot impl
384// DisplayNonEmpty
385impl_attrvalue!(
386    String,
387    std::net::IpAddr,
388    std::net::Ipv4Addr,
389    std::net::Ipv6Addr,
390);
391
392impl AttrValue for std::time::Duration {
393    fn display(&self) -> String {
394        humantime::format_duration(*self).to_string()
395    }
396
397    fn parse(value: &str) -> Result<Self, anyhow::Error> {
398        Ok(humantime::parse_duration(value)?)
399    }
400}
401
402// Durations never have an empty display.
403impl DisplayNonEmpty for std::time::Duration {}
404
405impl AttrValue for std::time::SystemTime {
406    fn display(&self) -> String {
407        let datetime: DateTime<Utc> = (*self).into();
408        datetime.to_rfc3339()
409    }
410
411    fn parse(value: &str) -> Result<Self, anyhow::Error> {
412        let datetime = DateTime::parse_from_rfc3339(value)?;
413        Ok(datetime.into())
414    }
415}
416
417// SystemTimes never have an empty display.
418impl DisplayNonEmpty for std::time::SystemTime {}
419
420impl<T> AttrValue for Vec<T>
421where
422    T: AttrValue,
423{
424    fn display(&self) -> String {
425        serde_json::to_string(self).unwrap_or_else(|_| "[]".to_string())
426    }
427
428    fn parse(value: &str) -> Result<Self, anyhow::Error> {
429        Ok(serde_json::from_str(value)?)
430    }
431}
432
433impl<T, E> AttrValue for std::ops::Range<T>
434where
435    T: Named
436        + Display
437        + FromStr<Err = E>
438        + Send
439        + Sync
440        + Serialize
441        + DeserializeOwned
442        + Clone
443        + 'static,
444    E: Into<anyhow::Error> + Send + Sync + 'static,
445{
446    fn display(&self) -> String {
447        format!("{}..{}", self.start, self.end)
448    }
449
450    fn parse(value: &str) -> Result<Self, anyhow::Error> {
451        let (start, end) = value.split_once("..").ok_or_else(|| {
452            anyhow::anyhow!("expected range in format `start..end`, got `{}`", value)
453        })?;
454        let start = start.parse().map_err(|e: E| e.into())?;
455        let end = end.parse().map_err(|e: E| e.into())?;
456        Ok(start..end)
457    }
458}
459
460impl AttrValue for bool {
461    fn display(&self) -> String {
462        if *self { 1.to_string() } else { 0.to_string() }
463    }
464
465    fn parse(value: &str) -> Result<Self, anyhow::Error> {
466        let value = value.to_ascii_lowercase();
467        match value.as_str() {
468            "0" | "false" => Ok(false),
469            "1" | "true" => Ok(true),
470            _ => Err(anyhow::anyhow!(
471                "expected `0`, `1`, `true` or `false`, got `{}`",
472                value
473            )),
474        }
475    }
476}
477
478impl DisplayNonEmpty for bool {}
479
480impl<T: AttrValue + DisplayNonEmpty> AttrValue for Option<T> {
481    fn display(&self) -> String {
482        match self {
483            Some(value) => value.display(),
484            None => String::new(),
485        }
486    }
487
488    fn parse(value: &str) -> Result<Self, anyhow::Error> {
489        if value.is_empty() {
490            Ok(None)
491        } else {
492            Ok(Some(T::parse(value)?))
493        }
494    }
495}
496
497// Internal trait for type-erased serialization
498#[doc(hidden)]
499pub trait SerializableValue: Send + Sync {
500    /// Get a reference to this value as Any for downcasting
501    fn as_any(&self) -> &dyn Any;
502    /// Get a mutable reference to this value as Any for downcasting
503    fn as_any_mut(&mut self) -> &mut dyn Any;
504    /// Get a reference to this value as an erased serializable trait object
505    fn as_erased_serialize(&self) -> &dyn ErasedSerialize;
506    /// Clone the underlying value, retaining dyn compatibility.
507    fn cloned(&self) -> Box<dyn SerializableValue>;
508    /// Display the value
509    fn display(&self) -> String;
510    /// Serialize to bincode bytes for wire transmission
511    fn serialize_bincode(&self) -> Vec<u8>;
512}
513
514impl<T: AttrValue> SerializableValue for T {
515    fn as_any(&self) -> &dyn Any {
516        self
517    }
518
519    fn as_any_mut(&mut self) -> &mut dyn Any {
520        self
521    }
522
523    fn as_erased_serialize(&self) -> &dyn ErasedSerialize {
524        self
525    }
526
527    fn cloned(&self) -> Box<dyn SerializableValue> {
528        Box::new(self.clone())
529    }
530
531    fn display(&self) -> String {
532        self.display()
533    }
534
535    fn serialize_bincode(&self) -> Vec<u8> {
536        bincode::serialize(self).expect("bincode serialization failed")
537    }
538}
539
540/// A heterogeneous, strongly-typed attribute dictionary with serialization support.
541///
542/// This dictionary stores key-value pairs where:
543/// - Keys are type-safe and must be predefined with their associated types
544/// - Values must implement [`AttrValue`]
545/// - The entire dictionary can be serialized to/from JSON automatically
546///
547/// # Type Safety
548///
549/// The dictionary enforces type safety at compile time. You cannot retrieve a value
550/// with the wrong type, and the compiler will catch such errors.
551///
552/// # Serialization
553///
554/// The dictionary can be serialized using serde. During serialization, each value
555/// is serialized with its key name. During deserialization, the automatically registered
556/// key information is used to determine the correct type for each value.
557pub struct Attrs {
558    values: HashMap<&'static str, Box<dyn SerializableValue>>,
559}
560
561impl Attrs {
562    /// Create a new empty attribute dictionary.
563    pub fn new() -> Self {
564        Self {
565            values: HashMap::new(),
566        }
567    }
568
569    /// Set a value for the given key.
570    pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
571        self.values.insert(key.name, Box::new(value));
572    }
573
574    fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
575        if self.contains_key(key) {
576            return;
577        }
578        let Some(default) = key.default() else { return };
579        self.set(key, default.clone());
580    }
581
582    /// Get a value for the given key, returning None if not present. If the key has a default value,
583    /// that is returned instead.
584    pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
585        self.values
586            .get(key.name)
587            .and_then(|value| value.as_any().downcast_ref::<T>())
588            .or_else(|| key.default())
589    }
590
591    /// Get a mutable reference to a value for the given key. If the key has a default value, it is
592    /// first set, and then returned as a mutable reference.
593    pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
594        self.maybe_set_from_default(key);
595        self.values
596            .get_mut(key.name)
597            .and_then(|value| value.as_any_mut().downcast_mut::<T>())
598    }
599
600    /// Remove a value for the given key, returning it if present.
601    pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
602        // TODO: return value (this is tricky because of the type erasure)
603        self.values.remove(key.name).is_some()
604    }
605
606    /// Checks if the given key exists in the dictionary.
607    pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
608        self.values.contains_key(key.name)
609    }
610
611    /// Returns the number of key-value pairs in the dictionary.
612    pub fn len(&self) -> usize {
613        self.values.len()
614    }
615
616    /// Returns true if the dictionary is empty.
617    pub fn is_empty(&self) -> bool {
618        self.values.is_empty()
619    }
620
621    /// Iterate over all key-value pairs.
622    ///
623    /// Returns an iterator of (key_name, value) pairs.
624    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &dyn SerializableValue)> {
625        self.values.iter().map(|(k, v)| (*k, v.as_ref()))
626    }
627
628    /// Clear all key-value pairs from the dictionary.
629    pub fn clear(&mut self) {
630        self.values.clear();
631    }
632
633    // Internal methods for config guard support
634    /// Take a value by key name, returning the boxed value if present
635    pub fn remove_value<T: 'static>(&mut self, key: Key<T>) -> Option<Box<dyn SerializableValue>> {
636        self.values.remove(key.name)
637    }
638
639    /// Restore a value by key name
640    pub fn insert_value<T: 'static>(&mut self, key: Key<T>, value: Box<dyn SerializableValue>) {
641        self.values.insert(key.name, value);
642    }
643
644    /// Restore a value by key name
645    pub fn insert_value_by_name_unchecked(
646        &mut self,
647        name: &'static str,
648        value: Box<dyn SerializableValue>,
649    ) {
650        self.values.insert(name, value);
651    }
652
653    /// Internal getter by key name for explicitly-set values (no
654    /// defaults).
655    pub fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
656        self.values.get(name).map(|b| b.as_ref())
657    }
658
659    /// Merge all attributes from `other` into this set, consuming
660    /// `other`.
661    ///
662    /// For each key in `other`, moves its value into `self`,
663    /// overwriting any existing value for the same key.
664    pub fn merge(&mut self, other: Attrs) {
665        self.values.extend(other.values);
666    }
667}
668
669impl Clone for Attrs {
670    fn clone(&self) -> Self {
671        let mut values = HashMap::new();
672        for (key, value) in &self.values {
673            values.insert(*key, value.cloned());
674        }
675        Self { values }
676    }
677}
678
679impl std::fmt::Display for Attrs {
680    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681        let mut first = true;
682        for (key, value) in &self.values {
683            if first {
684                first = false;
685            } else {
686                write!(f, ",")?;
687            }
688            write!(f, "{}={}", key, value.display())?
689        }
690        Ok(())
691    }
692}
693
694impl std::fmt::Debug for Attrs {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        // Create a map of key names to their JSON representation for debugging
697        let mut debug_map = std::collections::BTreeMap::new();
698        for (key, value) in &self.values {
699            match serde_json::to_string(value.as_erased_serialize()) {
700                Ok(json) => {
701                    debug_map.insert(*key, json);
702                }
703                Err(_) => {
704                    debug_map.insert(*key, "<serialization error>".to_string());
705                }
706            }
707        }
708
709        f.debug_struct("Attrs").field("values", &debug_map).finish()
710    }
711}
712
713impl Default for Attrs {
714    fn default() -> Self {
715        Self::new()
716    }
717}
718
719impl Serialize for Attrs {
720    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
721    where
722        S: Serializer,
723    {
724        let mut map = serializer.serialize_map(Some(self.values.len()))?;
725
726        for (key_name, value) in &self.values {
727            map.serialize_entry(key_name, value.as_erased_serialize())?;
728        }
729
730        map.end()
731    }
732}
733
734struct AttrsVisitor;
735
736impl<'de> Visitor<'de> for AttrsVisitor {
737    type Value = Attrs;
738
739    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
740        formatter.write_str("a map of attribute keys to their serialized values")
741    }
742
743    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
744    where
745        M: MapAccess<'de>,
746    {
747        static KEYS_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static AttrKeyInfo>> =
748            std::sync::LazyLock::new(|| {
749                inventory::iter::<AttrKeyInfo>()
750                    .map(|info| (info.name, info))
751                    .collect()
752            });
753        let keys_by_name = &*KEYS_BY_NAME;
754
755        let exe_name = std::env::current_exe()
756            .ok()
757            .map(|p| p.display().to_string())
758            .unwrap_or_else(|| "<unknown-exe>".to_string());
759
760        let mut attrs = Attrs::new();
761        while let Some(key_name) = access.next_key::<String>()? {
762            let Some(&key) = keys_by_name.get(key_name.as_str()) else {
763                // We hit an attribute key that this binary doesn't
764                // know about.
765                //
766                // In JSON we'd just deserialize that value into
767                // `IgnoredAny` and move on. With bincode we *can't*
768                // do that safely:
769                //
770                // - We don't know this key's value type.
771                // - That means we don't know how many bytes to
772                //   consume for the value.
773                // - If we guess wrong or try `IgnoredAny`, bincode
774                //   would need `Deserializer::deserialize_any()` to
775                //   skip it, but bincode refuses because it can't
776                //   know how many bytes to advance.
777                //
778                // Result: we cannot safely "skip" the unknown value
779                // without risking desync of the remaining stream. So
780                // we abort here and surface which key caused it, and
781                // the caller must strip it before sending.
782                access.next_value::<serde::de::IgnoredAny>().map_err(|_| {
783                    serde::de::Error::custom(format!(
784                        "unknown attr key '{}' on binary '{}'; \
785                         this binary doesn't know this key and cannot skip its value safely under bincode",
786                        key_name, exe_name,
787                    ))
788                })?;
789                continue;
790            };
791
792            // Create a seed to deserialize the value using erased_serde
793            let seed = ValueDeserializeSeed {
794                deserialize_erased: key.deserialize_erased,
795            };
796            match access.next_value_seed(seed) {
797                Ok(value) => {
798                    attrs.values.insert(key.name, value);
799                }
800                Err(err) => {
801                    return Err(serde::de::Error::custom(format!(
802                        "failed to deserialize value for key {}: {}",
803                        key_name, err
804                    )));
805                }
806            }
807        }
808
809        Ok(attrs)
810    }
811}
812
813/// Helper struct to deserialize values using erased_serde
814struct ValueDeserializeSeed {
815    deserialize_erased:
816        fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
817}
818
819impl<'de> serde::de::DeserializeSeed<'de> for ValueDeserializeSeed {
820    type Value = Box<dyn SerializableValue>;
821
822    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
823    where
824        D: serde::de::Deserializer<'de>,
825    {
826        let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
827        (self.deserialize_erased)(&mut erased).map_err(serde::de::Error::custom)
828    }
829}
830
831impl<'de> Deserialize<'de> for Attrs {
832    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
833    where
834        D: Deserializer<'de>,
835    {
836        deserializer.deserialize_map(AttrsVisitor)
837    }
838}
839
840// Converts an ASCII string to lowercase at compile time.
841// Returns a const string with lowercase ASCII characters.
842#[doc(hidden)]
843pub const fn ascii_to_lowercase_const<const N: usize>(input: &str) -> [u8; N] {
844    let bytes = input.as_bytes();
845    let mut result = [0u8; N];
846    let mut i = 0;
847
848    while i < bytes.len() && i < N {
849        let byte = bytes[i];
850        if byte >= b'A' && byte <= b'Z' {
851            result[i] = byte + 32; // Convert to lowercase
852        } else {
853            result[i] = byte;
854        }
855        i += 1;
856    }
857
858    result
859}
860
861// Macro to generate a const lowercase string at compile time
862#[doc(hidden)]
863#[macro_export]
864macro_rules! const_ascii_lowercase {
865    ($s:expr) => {{
866        const INPUT: &str = $s;
867        const LEN: usize = INPUT.len();
868        const BYTES: [u8; LEN] = $crate::attrs::ascii_to_lowercase_const::<LEN>(INPUT);
869        // Safety: We're converting ASCII to ASCII, so it's valid UTF-8
870        unsafe { std::str::from_utf8_unchecked(&BYTES) }
871    }};
872}
873
874/// Macro to check check that a trait is implemented, generating a
875/// nice error message if it isn't.
876#[doc(hidden)]
877#[macro_export]
878macro_rules! assert_impl {
879    ($ty:ty, $trait:path) => {
880        const _: fn() = || {
881            fn check<T: $trait>() {}
882            check::<$ty>();
883        };
884    };
885}
886
887/// Declares attribute keys using a lazy_static! style syntax.
888///
889/// # Syntax
890///
891/// ```ignore
892/// declare_attrs! {
893///     /// Documentation for the key (default visibility).
894///     attr KEY_NAME: Type = default_value;
895///
896///     /// Another key (default value is optional)
897///     pub attr ANOTHER_KEY: AnotherType;
898/// }
899/// ```
900///
901/// # Arguments
902///
903/// * Optional visibility modifier (`pub`, `pub(crate)`, etc.)
904/// * `attr` keyword (required)
905/// * Key name (identifier)
906/// * Type of values this key can store
907/// * Optional default value
908///
909/// # Example
910///
911/// ```
912/// use std::time::Duration;
913///
914/// use hyperactor_config::attrs::Attrs;
915/// use hyperactor_config::attrs::declare_attrs;
916///
917/// declare_attrs! {
918///     /// Timeout for RPC operations
919///     pub attr TIMEOUT: Duration = Duration::from_secs(30);
920///
921///     /// Maximum number of retry attempts (no default specified)
922///     attr MAX_RETRIES: u32;
923/// }
924///
925/// let mut attrs = Attrs::new();
926/// assert_eq!(attrs.get(TIMEOUT), Some(&Duration::from_secs(30)));
927/// attrs.set(MAX_RETRIES, 5);
928/// ```
929#[macro_export]
930macro_rules! declare_attrs {
931    // Handle multiple attribute keys with optional default values and optional meta attributes
932    ($(
933        $(#[$attr:meta])*
934        $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))*
935        $vis:vis attr $name:ident: $type:ty $(= $default:expr)?;
936    )*) => {
937        $(
938            $crate::declare_attrs! {
939                @single
940                $(@meta($($meta_key = $meta_value),*))*
941                $(#[$attr])* ;
942                $vis attr $name: $type $(= $default)?;
943            }
944        )*
945    };
946
947    // Handle single attribute key with default value and meta attributes
948    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
949        $crate::assert_impl!($type, $crate::attrs::AttrValue);
950
951        // Create a static default value
952        $crate::paste! {
953            static [<$name _DEFAULT>]: $type = $default;
954            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
955                std::sync::LazyLock::new(|| {
956                    #[allow(unused_mut)]
957                    let mut attrs = $crate::attrs::Attrs::new();
958                    $($(
959                        attrs.set($meta_key, $meta_value);
960                    )*)*
961                    attrs
962                });
963        }
964
965        $(#[$attr])*
966        $vis static $name: $crate::attrs::Key<$type> = {
967            $crate::assert_impl!($type, $crate::attrs::AttrValue);
968
969            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
970            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
971            $crate::paste! {
972                $crate::attrs::Key::new(
973                    LOWER_NAME,
974                    Some(&[<$name _DEFAULT>]),
975                    $crate::paste! { &[<$name _META_ATTRS>] },
976                )
977            }
978        };
979
980        // Register the key for serialization
981        $crate::submit! {
982            $crate::attrs::AttrKeyInfo {
983                name: {
984                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
985                    $crate::const_ascii_lowercase!(FULL_NAME)
986                },
987                key_hash: {
988                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
989                    const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
990                    $crate::attrs::fnv1a_hash(LOWER_NAME.as_bytes())
991                },
992                typehash: <$type as $crate::typeuri::Named>::typehash,
993                deserialize_erased: |deserializer| {
994                    let value: $type = erased_serde::deserialize(deserializer)?;
995                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
996                },
997                deserialize_bincode: |bytes| {
998                    let value: $type = $crate::bincode::deserialize(bytes)?;
999                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1000                },
1001                meta: $crate::paste! { &[<$name _META_ATTRS>] },
1002                display: |value: &dyn $crate::attrs::SerializableValue| {
1003                    let value = value.as_any().downcast_ref::<$type>().unwrap();
1004                    $crate::attrs::AttrValue::display(value)
1005                },
1006                parse: |value: &str| {
1007                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
1008                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1009                },
1010                default: Some($crate::paste! { &[<$name _DEFAULT>] }),
1011                erased: &$name,
1012            }
1013        }
1014    };
1015
1016    // Handle single attribute key without default value but with meta attributes
1017    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
1018        $crate::assert_impl!($type, $crate::attrs::AttrValue);
1019
1020        $crate::paste! {
1021            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
1022            std::sync::LazyLock::new(|| {
1023                #[allow(unused_mut)]
1024                let mut attrs = $crate::attrs::Attrs::new();
1025                $($(
1026                    // Note: This assumes meta keys are already declared somewhere
1027                    // The user needs to ensure the meta keys exist and are in scope
1028                    attrs.set($meta_key, $meta_value);
1029                )*)*
1030                attrs
1031            });
1032        }
1033
1034        $(#[$attr])*
1035        $vis static $name: $crate::attrs::Key<$type> = {
1036            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1037            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1038            $crate::attrs::Key::new(LOWER_NAME, None, $crate::paste! { &[<$name _META_ATTRS>] })
1039        };
1040
1041
1042        // Register the key for serialization
1043        $crate::submit! {
1044            $crate::attrs::AttrKeyInfo {
1045                name: {
1046                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1047                    $crate::const_ascii_lowercase!(FULL_NAME)
1048                },
1049                key_hash: {
1050                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1051                    const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1052                    $crate::attrs::fnv1a_hash(LOWER_NAME.as_bytes())
1053                },
1054                typehash: <$type as $crate::typeuri::Named>::typehash,
1055                deserialize_erased: |deserializer| {
1056                    let value: $type = erased_serde::deserialize(deserializer)?;
1057                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1058                },
1059                deserialize_bincode: |bytes| {
1060                    let value: $type = $crate::bincode::deserialize(bytes)?;
1061                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1062                },
1063                meta: $crate::paste! { &[<$name _META_ATTRS>] },
1064                display: |value: &dyn $crate::attrs::SerializableValue| {
1065                    let value = value.as_any().downcast_ref::<$type>().unwrap();
1066                    $crate::attrs::AttrValue::display(value)
1067                },
1068                parse: |value: &str| {
1069                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
1070                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1071                },
1072                default: None,
1073                erased: &$name,
1074            }
1075        }
1076    };
1077}
1078
1079pub use declare_attrs;
1080
1081#[cfg(test)]
1082mod tests {
1083    use std::time::Duration;
1084
1085    use super::*;
1086
1087    declare_attrs! {
1088        attr TEST_TIMEOUT: Duration;
1089        attr TEST_COUNT: u32;
1090        @meta(TEST_COUNT = 42)
1091        pub attr TEST_NAME: String;
1092        attr TEST_OPTION_COUNT: Option<u32>;
1093        attr TEST_OPTION_TIMEOUT: Option<Duration>;
1094    }
1095
1096    #[test]
1097    fn test_basic_operations() {
1098        let mut attrs = Attrs::new();
1099
1100        // Test setting and getting values
1101        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1102        attrs.set(TEST_COUNT, 42u32);
1103        attrs.set(TEST_NAME, "test".to_string());
1104
1105        assert_eq!(attrs.get(TEST_TIMEOUT), Some(&Duration::from_secs(5)));
1106        assert_eq!(attrs.get(TEST_COUNT), Some(&42u32));
1107        assert_eq!(attrs.get(TEST_NAME), Some(&"test".to_string()));
1108
1109        // Test contains_key
1110        assert!(attrs.contains_key(TEST_TIMEOUT));
1111        assert!(attrs.contains_key(TEST_COUNT));
1112        assert!(attrs.contains_key(TEST_NAME));
1113
1114        // Test len
1115        assert_eq!(attrs.len(), 3);
1116        assert!(!attrs.is_empty());
1117
1118        // Meta attribute:
1119        assert_eq!(TEST_NAME.attrs().get(TEST_COUNT).unwrap(), &42u32);
1120    }
1121
1122    #[test]
1123    fn test_get_mut() {
1124        let mut attrs = Attrs::new();
1125        attrs.set(TEST_COUNT, 10u32);
1126
1127        if let Some(count) = attrs.get_mut(TEST_COUNT) {
1128            *count += 5;
1129        }
1130
1131        assert_eq!(attrs.get(TEST_COUNT), Some(&15u32));
1132    }
1133
1134    #[test]
1135    fn test_remove() {
1136        let mut attrs = Attrs::new();
1137        attrs.set(TEST_COUNT, 42u32);
1138
1139        let removed = attrs.remove(TEST_COUNT);
1140        assert!(removed);
1141        assert_eq!(attrs.get(TEST_COUNT), None);
1142        assert!(!attrs.contains_key(TEST_COUNT));
1143    }
1144
1145    #[test]
1146    fn test_clear() {
1147        let mut attrs = Attrs::new();
1148        attrs.set(TEST_TIMEOUT, Duration::from_secs(1));
1149        attrs.set(TEST_COUNT, 42u32);
1150
1151        attrs.clear();
1152        assert!(attrs.is_empty());
1153        assert_eq!(attrs.len(), 0);
1154    }
1155
1156    #[test]
1157    fn test_key_properties() {
1158        assert_eq!(
1159            TEST_TIMEOUT.name(),
1160            "hyperactor_config::attrs::tests::test_timeout"
1161        );
1162    }
1163
1164    #[test]
1165    fn test_serialization() {
1166        let mut attrs = Attrs::new();
1167        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1168        attrs.set(TEST_COUNT, 42u32);
1169        attrs.set(TEST_NAME, "test".to_string());
1170
1171        // Test serialization
1172        let serialized = serde_json::to_string(&attrs).expect("Failed to serialize");
1173
1174        // The serialized string should contain the key names and their values
1175        assert!(serialized.contains("hyperactor_config::attrs::tests::test_timeout"));
1176        assert!(serialized.contains("hyperactor_config::attrs::tests::test_count"));
1177        assert!(serialized.contains("hyperactor_config::attrs::tests::test_name"));
1178    }
1179
1180    #[test]
1181    fn test_deserialization() {
1182        // Create original attrs
1183        let mut original_attrs = Attrs::new();
1184        original_attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1185        original_attrs.set(TEST_COUNT, 42u32);
1186        original_attrs.set(TEST_NAME, "test".to_string());
1187
1188        // Serialize
1189        let serialized = serde_json::to_string(&original_attrs).expect("Failed to serialize");
1190
1191        // Deserialize (no manual registry needed!)
1192        let deserialized_attrs: Attrs =
1193            serde_json::from_str(&serialized).expect("Failed to deserialize");
1194
1195        // Verify the deserialized values
1196        assert_eq!(
1197            deserialized_attrs.get(TEST_TIMEOUT),
1198            Some(&Duration::from_secs(5))
1199        );
1200        assert_eq!(deserialized_attrs.get(TEST_COUNT), Some(&42u32));
1201        assert_eq!(deserialized_attrs.get(TEST_NAME), Some(&"test".to_string()));
1202    }
1203
1204    #[test]
1205    fn test_roundtrip_serialization() {
1206        // Create original attrs
1207        let mut original = Attrs::new();
1208        original.set(TEST_TIMEOUT, Duration::from_secs(10));
1209        original.set(TEST_COUNT, 5u32);
1210        original.set(TEST_OPTION_COUNT, Some(5u32));
1211        original.set(TEST_OPTION_TIMEOUT, None);
1212        original.set(TEST_NAME, "test-service".to_string());
1213
1214        // Serialize
1215        let serialized = serde_json::to_string(&original).unwrap();
1216
1217        // Deserialize
1218        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1219
1220        // Verify round-trip worked
1221        assert_eq!(
1222            deserialized.get(TEST_TIMEOUT),
1223            Some(&Duration::from_secs(10))
1224        );
1225        assert_eq!(deserialized.get(TEST_COUNT), Some(&5u32));
1226        assert_eq!(deserialized.get(TEST_OPTION_COUNT), Some(&Some(5u32)));
1227        assert_eq!(deserialized.get(TEST_OPTION_TIMEOUT), Some(&None));
1228        assert_eq!(
1229            deserialized.get(TEST_NAME),
1230            Some(&"test-service".to_string())
1231        );
1232    }
1233
1234    #[test]
1235    fn test_empty_attrs_serialization() {
1236        let attrs = Attrs::new();
1237        let serialized = serde_json::to_string(&attrs).unwrap();
1238
1239        // Empty attrs should serialize to empty JSON object
1240        assert_eq!(serialized, "{}");
1241
1242        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1243
1244        assert!(deserialized.is_empty());
1245    }
1246
1247    #[test]
1248    fn test_format_independence() {
1249        // Test that proves we're using the serializer directly, not JSON internally
1250        let mut attrs = Attrs::new();
1251        attrs.set(TEST_COUNT, 42u32);
1252        attrs.set(TEST_NAME, "test".to_string());
1253
1254        // Serialize to different formats
1255        let json_output = serde_json::to_string(&attrs).unwrap();
1256        let yaml_output = serde_yaml::to_string(&attrs).unwrap();
1257
1258        // JSON should have colons and quotes
1259        assert!(json_output.contains(":"));
1260        assert!(json_output.contains("\""));
1261
1262        // JSON should serialize numbers as numbers, not strings
1263        assert!(json_output.contains("42"));
1264        assert!(!json_output.contains("\"42\""));
1265
1266        // YAML should have colons but different formatting
1267        assert!(yaml_output.contains(":"));
1268        assert!(yaml_output.contains("42"));
1269
1270        // YAML shouldn't quote simple strings or numbers
1271        assert!(!yaml_output.contains("\"42\""));
1272
1273        // The outputs should be different (proving different serializers were used)
1274        assert_ne!(json_output, yaml_output);
1275
1276        // Verify that both can be deserialized correctly
1277        let from_json: Attrs = serde_json::from_str(&json_output).unwrap();
1278        let from_yaml: Attrs = serde_yaml::from_str(&yaml_output).unwrap();
1279
1280        assert_eq!(from_json.get(TEST_COUNT), Some(&42u32));
1281        assert_eq!(from_yaml.get(TEST_COUNT), Some(&42u32));
1282        assert_eq!(from_json.get(TEST_NAME), Some(&"test".to_string()));
1283        assert_eq!(from_yaml.get(TEST_NAME), Some(&"test".to_string()));
1284    }
1285
1286    #[test]
1287    fn test_clone() {
1288        // Create original attrs with multiple types
1289        let mut original = Attrs::new();
1290        original.set(TEST_COUNT, 42u32);
1291        original.set(TEST_NAME, "test".to_string());
1292        original.set(TEST_TIMEOUT, std::time::Duration::from_secs(10));
1293
1294        // Clone the attrs
1295        let cloned = original.clone();
1296
1297        // Verify that the clone has the same values
1298        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32));
1299        assert_eq!(cloned.get(TEST_NAME), Some(&"test".to_string()));
1300        assert_eq!(
1301            cloned.get(TEST_TIMEOUT),
1302            Some(&std::time::Duration::from_secs(10))
1303        );
1304
1305        // Verify that modifications to the original don't affect the clone
1306        original.set(TEST_COUNT, 100u32);
1307        assert_eq!(original.get(TEST_COUNT), Some(&100u32));
1308        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32)); // Clone should be unchanged
1309
1310        // Verify that modifications to the clone don't affect the original
1311        let mut cloned_mut = cloned.clone();
1312        cloned_mut.set(TEST_NAME, "modified".to_string());
1313        assert_eq!(cloned_mut.get(TEST_NAME), Some(&"modified".to_string()));
1314        assert_eq!(original.get(TEST_NAME), Some(&"test".to_string())); // Original should be unchanged
1315    }
1316
1317    #[test]
1318    fn test_debug_with_json() {
1319        let mut attrs = Attrs::new();
1320        attrs.set(TEST_COUNT, 42u32);
1321        attrs.set(TEST_NAME, "test".to_string());
1322
1323        // Test that Debug implementation works and contains JSON representations
1324        let debug_output = format!("{:?}", attrs);
1325
1326        // Should contain the struct name
1327        assert!(debug_output.contains("Attrs"));
1328
1329        // Should contain JSON representations of the values
1330        assert!(debug_output.contains("42"));
1331
1332        // Should contain the key names
1333        assert!(debug_output.contains("hyperactor_config::attrs::tests::test_count"));
1334        assert!(debug_output.contains("hyperactor_config::attrs::tests::test_name"));
1335
1336        // For strings, the JSON representation should be the escaped version
1337        // Let's check that the test string is actually present in some form
1338        assert!(debug_output.contains("test"));
1339    }
1340
1341    declare_attrs! {
1342        /// With default...
1343        attr TIMEOUT_WITH_DEFAULT: Duration = Duration::from_secs(10);
1344
1345        /// Just to ensure visibilty is parsed.
1346        pub(crate) attr CRATE_LOCAL_ATTR: String;
1347    }
1348
1349    #[test]
1350    fn test_defaults() {
1351        assert!(TIMEOUT_WITH_DEFAULT.has_default());
1352        assert!(!CRATE_LOCAL_ATTR.has_default());
1353
1354        assert_eq!(
1355            Attrs::new().get(TIMEOUT_WITH_DEFAULT),
1356            Some(&Duration::from_secs(10))
1357        );
1358    }
1359
1360    #[test]
1361    fn test_indexing() {
1362        let mut attrs = Attrs::new();
1363
1364        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(10));
1365        attrs[TIMEOUT_WITH_DEFAULT] = Duration::from_secs(100);
1366        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(100));
1367
1368        attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
1369        assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
1370    }
1371
1372    #[test]
1373    fn attrs_deserialize_unknown_key_is_error() {
1374        // Build a real Attrs, but inject a key that this binary does
1375        // NOT know about. We do that with
1376        // insert_value_by_name_unchecked(), which bypasses the
1377        // declare_attrs!/inventory registration path.
1378        //
1379        // Then:
1380        //   1. Serialize that Attrs with bincode (the real wire
1381        //      format).
1382        //   2. Attempt to bincode-deserialize those bytes back into
1383        //      Attrs.
1384        //
1385        // During deserialization, AttrsVisitor::visit_map() will:
1386        //   - read the key string
1387        //   - fail to find it in KEYS_BY_NAME (the compiled-in
1388        //     registry)
1389        //   - immediately error instead of trying to skip the value,
1390        //     because with bincode we can't safely consume an unknown
1391        //     typed value without risking stream desync.
1392        //
1393        // This reproduces exactly what happens when a parent proc
1394        // sends an Attrs containing a key the child binary wasn't
1395        // built with.
1396
1397        // Definitely not declared in this crate's inventory:
1398        let bad_key: &'static str = "monarch_hyperactor::pytokio::unawaited_pytokio_traceback";
1399
1400        // Make an Attrs that pretends to have that key. u32 already
1401        // implements AttrValue -> SerializableValue, so we can just
1402        // box a 0u32.
1403        let mut attrs = Attrs::new();
1404        attrs.insert_value_by_name_unchecked(bad_key, Box::new(0u32));
1405
1406        // Serialize this Attrs using bincode (same codec we use on
1407        // the wire).
1408        let wire_bytes = bincode::serialize(&attrs).unwrap();
1409
1410        // Now try to decode those bytes back into Attrs. This should
1411        // hit the unknown-key branch and return Err.
1412        let err = bincode::deserialize::<Attrs>(&wire_bytes)
1413            .expect_err("should error on unknown attr key");
1414
1415        let exe_str = std::env::current_exe()
1416            .ok()
1417            .map(|p| p.display().to_string())
1418            .unwrap_or_else(|| "<unknown-exe>".to_string());
1419        let msg = format!("{err}");
1420
1421        assert!(msg.contains("unknown attr key"), "got: {msg}");
1422        assert!(msg.contains(bad_key), "got: {msg}");
1423        assert!(msg.contains(&exe_str), "got: {msg}");
1424    }
1425
1426    // Verify that Vec<T> values work as attribute values when T:
1427    // AttrValue.
1428    //
1429    // This checks two things:
1430    //  1. A Vec<String> can be stored and retrieved through the Attrs
1431    //    API.
1432    //  2. The value survives a JSON round-trip via serde
1433    //     serialization.
1434    #[test]
1435    fn test_vec_string_attr() {
1436        declare_attrs! {
1437            attr TEST_VEC: Vec<String>;
1438        }
1439        let mut attrs = Attrs::new();
1440        let original = vec!["a".to_string(), "b".to_string()];
1441        attrs.set(TEST_VEC, original.clone());
1442        assert_eq!(attrs.get(TEST_VEC), Some(&original));
1443        // Round-trip through Attrs JSON serialization
1444        let json = serde_json::to_string(&attrs).unwrap();
1445        let attrs2: Attrs = serde_json::from_str(&json).unwrap();
1446        assert_eq!(attrs2.get(TEST_VEC), Some(&original));
1447        // Round-trip through AttrValue display/parse
1448        let displayed = AttrValue::display(&original);
1449        let parsed: Vec<String> = AttrValue::parse(&displayed).unwrap();
1450        assert_eq!(parsed, original);
1451    }
1452}