Skip to main content

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