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<T: AttrValue + DisplayNonEmpty> AttrValue for Option<T> {
591    fn display(&self) -> String {
592        match self {
593            Some(value) => value.display(),
594            None => String::new(),
595        }
596    }
597
598    fn parse(value: &str) -> Result<Self, anyhow::Error> {
599        if value.is_empty() {
600            Ok(None)
601        } else {
602            Ok(Some(T::parse(value)?))
603        }
604    }
605}
606
607// Internal trait for type-erased serialization
608#[doc(hidden)]
609pub trait SerializableValue: Send + Sync {
610    /// Get a reference to this value as Any for downcasting
611    fn as_any(&self) -> &dyn Any;
612    /// Get a mutable reference to this value as Any for downcasting
613    fn as_any_mut(&mut self) -> &mut dyn Any;
614    /// Get a reference to this value as an erased serializable trait object
615    fn as_erased_serialize(&self) -> &dyn ErasedSerialize;
616    /// Clone the underlying value, retaining dyn compatibility.
617    fn cloned(&self) -> Box<dyn SerializableValue>;
618    /// Display the value
619    fn display(&self) -> String;
620    /// Serialize to bincode bytes for wire transmission
621    fn serialize_bincode(&self) -> Vec<u8>;
622}
623
624impl<T: AttrValue> SerializableValue for T {
625    fn as_any(&self) -> &dyn Any {
626        self
627    }
628
629    fn as_any_mut(&mut self) -> &mut dyn Any {
630        self
631    }
632
633    fn as_erased_serialize(&self) -> &dyn ErasedSerialize {
634        self
635    }
636
637    fn cloned(&self) -> Box<dyn SerializableValue> {
638        Box::new(self.clone())
639    }
640
641    fn display(&self) -> String {
642        self.display()
643    }
644
645    fn serialize_bincode(&self) -> Vec<u8> {
646        bincode::serde::encode_to_vec(self, bincode::config::legacy())
647            .expect("bincode serialization failed")
648    }
649}
650
651/// A heterogeneous, strongly-typed attribute dictionary with serialization support.
652///
653/// This dictionary stores key-value pairs where:
654/// - Keys are type-safe and must be predefined with their associated types
655/// - Values must implement [`AttrValue`]
656/// - The entire dictionary can be serialized to/from JSON automatically
657///
658/// # Type Safety
659///
660/// The dictionary enforces type safety at compile time. You cannot retrieve a value
661/// with the wrong type, and the compiler will catch such errors.
662///
663/// # Serialization
664///
665/// The dictionary can be serialized using serde. During serialization, each value
666/// is serialized with its key name. During deserialization, the automatically registered
667/// key information is used to determine the correct type for each value.
668pub struct Attrs {
669    values: HashMap<&'static str, Box<dyn SerializableValue>>,
670}
671
672impl Attrs {
673    /// Create a new empty attribute dictionary.
674    pub fn new() -> Self {
675        Self {
676            values: HashMap::new(),
677        }
678    }
679
680    /// Set a value for the given key.
681    pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
682        self.values.insert(key.name, Box::new(value));
683    }
684
685    fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
686        if self.contains_key(key) {
687            return;
688        }
689        let Some(default) = key.default() else { return };
690        self.set(key, default.clone());
691    }
692
693    /// Get a value for the given key, returning None if not present. If the key has a default value,
694    /// that is returned instead.
695    pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
696        self.values
697            .get(key.name)
698            .and_then(|value| value.as_any().downcast_ref::<T>())
699            .or_else(|| key.default())
700    }
701
702    /// Get a mutable reference to a value for the given key. If the key has a default value, it is
703    /// first set, and then returned as a mutable reference.
704    pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
705        self.maybe_set_from_default(key);
706        self.values
707            .get_mut(key.name)
708            .and_then(|value| value.as_any_mut().downcast_mut::<T>())
709    }
710
711    /// Remove a value for the given key, returning it if present.
712    pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
713        // TODO: return value (this is tricky because of the type erasure)
714        self.values.remove(key.name).is_some()
715    }
716
717    /// Checks if the given key exists in the dictionary.
718    pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
719        self.values.contains_key(key.name)
720    }
721
722    /// Returns the number of key-value pairs in the dictionary.
723    pub fn len(&self) -> usize {
724        self.values.len()
725    }
726
727    /// Returns true if the dictionary is empty.
728    pub fn is_empty(&self) -> bool {
729        self.values.is_empty()
730    }
731
732    /// Iterate over all key-value pairs.
733    ///
734    /// Returns an iterator of (key_name, value) pairs.
735    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &dyn SerializableValue)> {
736        self.values.iter().map(|(k, v)| (*k, v.as_ref()))
737    }
738
739    /// Clear all key-value pairs from the dictionary.
740    pub fn clear(&mut self) {
741        self.values.clear();
742    }
743
744    // Internal methods for config guard support
745    /// Take a value by key name, returning the boxed value if present
746    pub fn remove_value<T: 'static>(&mut self, key: Key<T>) -> Option<Box<dyn SerializableValue>> {
747        self.values.remove(key.name)
748    }
749
750    /// Restore a value by key name
751    pub fn insert_value<T: 'static>(&mut self, key: Key<T>, value: Box<dyn SerializableValue>) {
752        self.values.insert(key.name, value);
753    }
754
755    /// Restore a value by key name
756    pub fn insert_value_by_name_unchecked(
757        &mut self,
758        name: &'static str,
759        value: Box<dyn SerializableValue>,
760    ) {
761        self.values.insert(name, value);
762    }
763
764    /// Internal getter by key name for explicitly-set values (no
765    /// defaults).
766    pub fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
767        self.values.get(name).map(|b| b.as_ref())
768    }
769
770    /// Merge all attributes from `other` into this set, consuming
771    /// `other`.
772    ///
773    /// For each key in `other`, moves its value into `self`,
774    /// overwriting any existing value for the same key.
775    pub fn merge(&mut self, other: Attrs) {
776        self.values.extend(other.values);
777    }
778}
779
780impl Clone for Attrs {
781    fn clone(&self) -> Self {
782        let mut values = HashMap::new();
783        for (key, value) in &self.values {
784            values.insert(*key, value.cloned());
785        }
786        Self { values }
787    }
788}
789
790impl std::fmt::Display for Attrs {
791    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
792        let mut first = true;
793        for (key, value) in &self.values {
794            if first {
795                first = false;
796            } else {
797                write!(f, ",")?;
798            }
799            write!(f, "{}={}", key, value.display())?
800        }
801        Ok(())
802    }
803}
804
805impl std::fmt::Debug for Attrs {
806    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
807        // Create a map of key names to their JSON representation for debugging
808        let mut debug_map = std::collections::BTreeMap::new();
809        for (key, value) in &self.values {
810            match serde_json::to_string(value.as_erased_serialize()) {
811                Ok(json) => {
812                    debug_map.insert(*key, json);
813                }
814                Err(_) => {
815                    debug_map.insert(*key, "<serialization error>".to_string());
816                }
817            }
818        }
819
820        f.debug_struct("Attrs").field("values", &debug_map).finish()
821    }
822}
823
824impl Default for Attrs {
825    fn default() -> Self {
826        Self::new()
827    }
828}
829
830impl Serialize for Attrs {
831    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
832    where
833        S: Serializer,
834    {
835        let mut map = serializer.serialize_map(Some(self.values.len()))?;
836
837        for (key_name, value) in &self.values {
838            map.serialize_entry(key_name, value.as_erased_serialize())?;
839        }
840
841        map.end()
842    }
843}
844
845struct AttrsVisitor;
846
847impl<'de> Visitor<'de> for AttrsVisitor {
848    type Value = Attrs;
849
850    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
851        formatter.write_str("a map of attribute keys to their serialized values")
852    }
853
854    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
855    where
856        M: MapAccess<'de>,
857    {
858        static KEYS_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static AttrKeyInfo>> =
859            std::sync::LazyLock::new(|| {
860                inventory::iter::<AttrKeyInfo>()
861                    .map(|info| (info.name, info))
862                    .collect()
863            });
864        let keys_by_name = &*KEYS_BY_NAME;
865
866        let exe_name = std::env::current_exe()
867            .ok()
868            .map(|p| p.display().to_string())
869            .unwrap_or_else(|| "<unknown-exe>".to_string());
870
871        let mut attrs = Attrs::new();
872        while let Some(key_name) = access.next_key::<String>()? {
873            let Some(&key) = keys_by_name.get(key_name.as_str()) else {
874                // We hit an attribute key that this binary doesn't
875                // know about.
876                //
877                // In JSON we'd just deserialize that value into
878                // `IgnoredAny` and move on. With bincode we *can't*
879                // do that safely:
880                //
881                // - We don't know this key's value type.
882                // - That means we don't know how many bytes to
883                //   consume for the value.
884                // - If we guess wrong or try `IgnoredAny`, bincode
885                //   would need `Deserializer::deserialize_any()` to
886                //   skip it, but bincode refuses because it can't
887                //   know how many bytes to advance.
888                //
889                // Result: we cannot safely "skip" the unknown value
890                // without risking desync of the remaining stream. So
891                // we abort here and surface which key caused it, and
892                // the caller must strip it before sending.
893                access.next_value::<serde::de::IgnoredAny>().map_err(|_| {
894                    serde::de::Error::custom(format!(
895                        "unknown attr key '{}' on binary '{}'; \
896                         this binary doesn't know this key and cannot skip its value safely under bincode",
897                        key_name, exe_name,
898                    ))
899                })?;
900                continue;
901            };
902
903            // Create a seed to deserialize the value using erased_serde
904            let seed = ValueDeserializeSeed {
905                deserialize_erased: key.deserialize_erased,
906            };
907            match access.next_value_seed(seed) {
908                Ok(value) => {
909                    attrs.values.insert(key.name, value);
910                }
911                Err(err) => {
912                    return Err(serde::de::Error::custom(format!(
913                        "failed to deserialize value for key {}: {}",
914                        key_name, err
915                    )));
916                }
917            }
918        }
919
920        Ok(attrs)
921    }
922}
923
924/// Helper struct to deserialize values using erased_serde
925struct ValueDeserializeSeed {
926    deserialize_erased:
927        fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
928}
929
930impl<'de> serde::de::DeserializeSeed<'de> for ValueDeserializeSeed {
931    type Value = Box<dyn SerializableValue>;
932
933    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
934    where
935        D: serde::de::Deserializer<'de>,
936    {
937        let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
938        (self.deserialize_erased)(&mut erased).map_err(serde::de::Error::custom)
939    }
940}
941
942impl<'de> Deserialize<'de> for Attrs {
943    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
944    where
945        D: Deserializer<'de>,
946    {
947        deserializer.deserialize_map(AttrsVisitor)
948    }
949}
950
951// Converts an ASCII string to lowercase at compile time.
952// Returns a const string with lowercase ASCII characters.
953#[doc(hidden)]
954pub const fn ascii_to_lowercase_const<const N: usize>(input: &str) -> [u8; N] {
955    let bytes = input.as_bytes();
956    let mut result = [0u8; N];
957    let mut i = 0;
958
959    while i < bytes.len() && i < N {
960        let byte = bytes[i];
961        if byte >= b'A' && byte <= b'Z' {
962            result[i] = byte + 32; // Convert to lowercase
963        } else {
964            result[i] = byte;
965        }
966        i += 1;
967    }
968
969    result
970}
971
972// Macro to generate a const lowercase string at compile time
973#[doc(hidden)]
974#[macro_export]
975macro_rules! const_ascii_lowercase {
976    ($s:expr) => {{
977        const INPUT: &str = $s;
978        const LEN: usize = INPUT.len();
979        const BYTES: [u8; LEN] = $crate::attrs::ascii_to_lowercase_const::<LEN>(INPUT);
980        // Safety: We're converting ASCII to ASCII, so it's valid UTF-8
981        unsafe { std::str::from_utf8_unchecked(&BYTES) }
982    }};
983}
984
985/// Macro to check check that a trait is implemented, generating a
986/// nice error message if it isn't.
987#[doc(hidden)]
988#[macro_export]
989macro_rules! assert_impl {
990    ($ty:ty, $trait:path) => {
991        const _: fn() = || {
992            fn check<T: $trait>() {}
993            check::<$ty>();
994        };
995    };
996}
997
998/// Declares attribute keys using a lazy_static! style syntax.
999///
1000/// # Syntax
1001///
1002/// ```ignore
1003/// declare_attrs! {
1004///     /// Documentation for the key (default visibility).
1005///     attr KEY_NAME: Type = default_value;
1006///
1007///     /// Another key (default value is optional)
1008///     pub attr ANOTHER_KEY: AnotherType;
1009/// }
1010/// ```
1011///
1012/// # Arguments
1013///
1014/// * Optional visibility modifier (`pub`, `pub(crate)`, etc.)
1015/// * `attr` keyword (required)
1016/// * Key name (identifier)
1017/// * Type of values this key can store
1018/// * Optional default value
1019///
1020/// # Example
1021///
1022/// ```
1023/// use std::time::Duration;
1024///
1025/// use hyperactor_config::attrs::Attrs;
1026/// use hyperactor_config::attrs::declare_attrs;
1027///
1028/// declare_attrs! {
1029///     /// Timeout for RPC operations
1030///     pub attr TIMEOUT: Duration = Duration::from_secs(30);
1031///
1032///     /// Maximum number of retry attempts (no default specified)
1033///     attr MAX_RETRIES: u32;
1034/// }
1035///
1036/// let mut attrs = Attrs::new();
1037/// assert_eq!(attrs.get(TIMEOUT), Some(&Duration::from_secs(30)));
1038/// attrs.set(MAX_RETRIES, 5);
1039/// ```
1040#[macro_export]
1041macro_rules! declare_attrs {
1042    // Handle multiple attribute keys with optional default values and optional meta attributes
1043    ($(
1044        $(#[$attr:meta])*
1045        $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))*
1046        $vis:vis attr $name:ident: $type:ty $(= $default:expr)?;
1047    )*) => {
1048        $(
1049            $crate::declare_attrs! {
1050                @single
1051                $(@meta($($meta_key = $meta_value),*))*
1052                $(#[$attr])* ;
1053                $vis attr $name: $type $(= $default)?;
1054            }
1055        )*
1056    };
1057
1058    // Handle single attribute key with default value and meta attributes
1059    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
1060        $crate::assert_impl!($type, $crate::attrs::AttrValue);
1061
1062        // Create a static default value
1063        $crate::paste! {
1064            static [<$name _DEFAULT>]: $type = $default;
1065            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
1066                std::sync::LazyLock::new(|| {
1067                    #[allow(unused_mut)]
1068                    let mut attrs = $crate::attrs::Attrs::new();
1069                    $($(
1070                        attrs.set($meta_key, $meta_value);
1071                    )*)*
1072                    attrs
1073                });
1074        }
1075
1076        $(#[$attr])*
1077        $vis static $name: $crate::attrs::Key<$type> = {
1078            $crate::assert_impl!($type, $crate::attrs::AttrValue);
1079
1080            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1081            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1082            $crate::paste! {
1083                $crate::attrs::Key::new(
1084                    LOWER_NAME,
1085                    Some(&[<$name _DEFAULT>]),
1086                    $crate::paste! { &[<$name _META_ATTRS>] },
1087                )
1088            }
1089        };
1090
1091        // Register the key for serialization
1092        $crate::submit! {
1093            $crate::attrs::AttrKeyInfo {
1094                name: {
1095                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1096                    $crate::const_ascii_lowercase!(FULL_NAME)
1097                },
1098                key_hash: {
1099                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1100                    const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1101                    $crate::attrs::fnv1a_hash(LOWER_NAME.as_bytes())
1102                },
1103                typehash: <$type as $crate::typeuri::Named>::typehash,
1104                deserialize_erased: |deserializer| {
1105                    let value: $type = erased_serde::deserialize(deserializer)?;
1106                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1107                },
1108                deserialize_bincode: |bytes| {
1109                    let value: $type = $crate::bincode::serde::decode_from_slice(bytes, $crate::bincode::config::legacy()).map(|(v, _)| v)?;
1110                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1111                },
1112                meta: $crate::paste! { &[<$name _META_ATTRS>] },
1113                display: |value: &dyn $crate::attrs::SerializableValue| {
1114                    let value = value.as_any().downcast_ref::<$type>().unwrap();
1115                    $crate::attrs::AttrValue::display(value)
1116                },
1117                parse: |value: &str| {
1118                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
1119                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1120                },
1121                default: Some($crate::paste! { &[<$name _DEFAULT>] }),
1122                erased: &$name,
1123            }
1124        }
1125    };
1126
1127    // Handle single attribute key without default value but with meta attributes
1128    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
1129        $crate::assert_impl!($type, $crate::attrs::AttrValue);
1130
1131        $crate::paste! {
1132            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
1133            std::sync::LazyLock::new(|| {
1134                #[allow(unused_mut)]
1135                let mut attrs = $crate::attrs::Attrs::new();
1136                $($(
1137                    // Note: This assumes meta keys are already declared somewhere
1138                    // The user needs to ensure the meta keys exist and are in scope
1139                    attrs.set($meta_key, $meta_value);
1140                )*)*
1141                attrs
1142            });
1143        }
1144
1145        $(#[$attr])*
1146        $vis static $name: $crate::attrs::Key<$type> = {
1147            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1148            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1149            $crate::attrs::Key::new(LOWER_NAME, None, $crate::paste! { &[<$name _META_ATTRS>] })
1150        };
1151
1152
1153        // Register the key for serialization
1154        $crate::submit! {
1155            $crate::attrs::AttrKeyInfo {
1156                name: {
1157                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1158                    $crate::const_ascii_lowercase!(FULL_NAME)
1159                },
1160                key_hash: {
1161                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
1162                    const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
1163                    $crate::attrs::fnv1a_hash(LOWER_NAME.as_bytes())
1164                },
1165                typehash: <$type as $crate::typeuri::Named>::typehash,
1166                deserialize_erased: |deserializer| {
1167                    let value: $type = erased_serde::deserialize(deserializer)?;
1168                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1169                },
1170                deserialize_bincode: |bytes| {
1171                    let value: $type = $crate::bincode::serde::decode_from_slice(bytes, $crate::bincode::config::legacy()).map(|(v, _)| v)?;
1172                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1173                },
1174                meta: $crate::paste! { &[<$name _META_ATTRS>] },
1175                display: |value: &dyn $crate::attrs::SerializableValue| {
1176                    let value = value.as_any().downcast_ref::<$type>().unwrap();
1177                    $crate::attrs::AttrValue::display(value)
1178                },
1179                parse: |value: &str| {
1180                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
1181                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
1182                },
1183                default: None,
1184                erased: &$name,
1185            }
1186        }
1187    };
1188}
1189
1190pub use declare_attrs;
1191
1192#[cfg(test)]
1193mod tests {
1194    use std::time::Duration;
1195
1196    use super::*;
1197
1198    declare_attrs! {
1199        attr TEST_TIMEOUT: Duration;
1200        attr TEST_COUNT: u32;
1201        @meta(TEST_COUNT = 42)
1202        pub attr TEST_NAME: String;
1203        attr TEST_OPTION_COUNT: Option<u32>;
1204        attr TEST_OPTION_TIMEOUT: Option<Duration>;
1205    }
1206
1207    #[test]
1208    fn test_basic_operations() {
1209        let mut attrs = Attrs::new();
1210
1211        // Test setting and getting values
1212        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1213        attrs.set(TEST_COUNT, 42u32);
1214        attrs.set(TEST_NAME, "test".to_string());
1215
1216        assert_eq!(attrs.get(TEST_TIMEOUT), Some(&Duration::from_secs(5)));
1217        assert_eq!(attrs.get(TEST_COUNT), Some(&42u32));
1218        assert_eq!(attrs.get(TEST_NAME), Some(&"test".to_string()));
1219
1220        // Test contains_key
1221        assert!(attrs.contains_key(TEST_TIMEOUT));
1222        assert!(attrs.contains_key(TEST_COUNT));
1223        assert!(attrs.contains_key(TEST_NAME));
1224
1225        // Test len
1226        assert_eq!(attrs.len(), 3);
1227        assert!(!attrs.is_empty());
1228
1229        // Meta attribute:
1230        assert_eq!(TEST_NAME.attrs().get(TEST_COUNT).unwrap(), &42u32);
1231    }
1232
1233    #[test]
1234    fn test_get_mut() {
1235        let mut attrs = Attrs::new();
1236        attrs.set(TEST_COUNT, 10u32);
1237
1238        if let Some(count) = attrs.get_mut(TEST_COUNT) {
1239            *count += 5;
1240        }
1241
1242        assert_eq!(attrs.get(TEST_COUNT), Some(&15u32));
1243    }
1244
1245    #[test]
1246    fn test_remove() {
1247        let mut attrs = Attrs::new();
1248        attrs.set(TEST_COUNT, 42u32);
1249
1250        let removed = attrs.remove(TEST_COUNT);
1251        assert!(removed);
1252        assert_eq!(attrs.get(TEST_COUNT), None);
1253        assert!(!attrs.contains_key(TEST_COUNT));
1254    }
1255
1256    #[test]
1257    fn test_clear() {
1258        let mut attrs = Attrs::new();
1259        attrs.set(TEST_TIMEOUT, Duration::from_secs(1));
1260        attrs.set(TEST_COUNT, 42u32);
1261
1262        attrs.clear();
1263        assert!(attrs.is_empty());
1264        assert_eq!(attrs.len(), 0);
1265    }
1266
1267    #[test]
1268    fn test_key_properties() {
1269        assert_eq!(
1270            TEST_TIMEOUT.name(),
1271            "hyperactor_config::attrs::tests::test_timeout"
1272        );
1273    }
1274
1275    #[test]
1276    fn test_serialization() {
1277        let mut attrs = Attrs::new();
1278        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1279        attrs.set(TEST_COUNT, 42u32);
1280        attrs.set(TEST_NAME, "test".to_string());
1281
1282        // Test serialization
1283        let serialized = serde_json::to_string(&attrs).expect("Failed to serialize");
1284
1285        // The serialized string should contain the key names and their values
1286        assert!(serialized.contains("hyperactor_config::attrs::tests::test_timeout"));
1287        assert!(serialized.contains("hyperactor_config::attrs::tests::test_count"));
1288        assert!(serialized.contains("hyperactor_config::attrs::tests::test_name"));
1289    }
1290
1291    #[test]
1292    fn test_deserialization() {
1293        // Create original attrs
1294        let mut original_attrs = Attrs::new();
1295        original_attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1296        original_attrs.set(TEST_COUNT, 42u32);
1297        original_attrs.set(TEST_NAME, "test".to_string());
1298
1299        // Serialize
1300        let serialized = serde_json::to_string(&original_attrs).expect("Failed to serialize");
1301
1302        // Deserialize (no manual registry needed!)
1303        let deserialized_attrs: Attrs =
1304            serde_json::from_str(&serialized).expect("Failed to deserialize");
1305
1306        // Verify the deserialized values
1307        assert_eq!(
1308            deserialized_attrs.get(TEST_TIMEOUT),
1309            Some(&Duration::from_secs(5))
1310        );
1311        assert_eq!(deserialized_attrs.get(TEST_COUNT), Some(&42u32));
1312        assert_eq!(deserialized_attrs.get(TEST_NAME), Some(&"test".to_string()));
1313    }
1314
1315    #[test]
1316    fn test_roundtrip_serialization() {
1317        // Create original attrs
1318        let mut original = Attrs::new();
1319        original.set(TEST_TIMEOUT, Duration::from_secs(10));
1320        original.set(TEST_COUNT, 5u32);
1321        original.set(TEST_OPTION_COUNT, Some(5u32));
1322        original.set(TEST_OPTION_TIMEOUT, None);
1323        original.set(TEST_NAME, "test-service".to_string());
1324
1325        // Serialize
1326        let serialized = serde_json::to_string(&original).unwrap();
1327
1328        // Deserialize
1329        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1330
1331        // Verify round-trip worked
1332        assert_eq!(
1333            deserialized.get(TEST_TIMEOUT),
1334            Some(&Duration::from_secs(10))
1335        );
1336        assert_eq!(deserialized.get(TEST_COUNT), Some(&5u32));
1337        assert_eq!(deserialized.get(TEST_OPTION_COUNT), Some(&Some(5u32)));
1338        assert_eq!(deserialized.get(TEST_OPTION_TIMEOUT), Some(&None));
1339        assert_eq!(
1340            deserialized.get(TEST_NAME),
1341            Some(&"test-service".to_string())
1342        );
1343    }
1344
1345    #[test]
1346    fn test_empty_attrs_serialization() {
1347        let attrs = Attrs::new();
1348        let serialized = serde_json::to_string(&attrs).unwrap();
1349
1350        // Empty attrs should serialize to empty JSON object
1351        assert_eq!(serialized, "{}");
1352
1353        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1354
1355        assert!(deserialized.is_empty());
1356    }
1357
1358    #[test]
1359    fn test_format_independence() {
1360        // Test that proves we're using the serializer directly, not JSON internally
1361        let mut attrs = Attrs::new();
1362        attrs.set(TEST_COUNT, 42u32);
1363        attrs.set(TEST_NAME, "test".to_string());
1364
1365        // Serialize to different formats
1366        let json_output = serde_json::to_string(&attrs).unwrap();
1367        let yaml_output = serde_yaml::to_string(&attrs).unwrap();
1368
1369        // JSON should have colons and quotes
1370        assert!(json_output.contains(":"));
1371        assert!(json_output.contains("\""));
1372
1373        // JSON should serialize numbers as numbers, not strings
1374        assert!(json_output.contains("42"));
1375        assert!(!json_output.contains("\"42\""));
1376
1377        // YAML should have colons but different formatting
1378        assert!(yaml_output.contains(":"));
1379        assert!(yaml_output.contains("42"));
1380
1381        // YAML shouldn't quote simple strings or numbers
1382        assert!(!yaml_output.contains("\"42\""));
1383
1384        // The outputs should be different (proving different serializers were used)
1385        assert_ne!(json_output, yaml_output);
1386
1387        // Verify that both can be deserialized correctly
1388        let from_json: Attrs = serde_json::from_str(&json_output).unwrap();
1389        let from_yaml: Attrs = serde_yaml::from_str(&yaml_output).unwrap();
1390
1391        assert_eq!(from_json.get(TEST_COUNT), Some(&42u32));
1392        assert_eq!(from_yaml.get(TEST_COUNT), Some(&42u32));
1393        assert_eq!(from_json.get(TEST_NAME), Some(&"test".to_string()));
1394        assert_eq!(from_yaml.get(TEST_NAME), Some(&"test".to_string()));
1395    }
1396
1397    #[test]
1398    fn test_clone() {
1399        // Create original attrs with multiple types
1400        let mut original = Attrs::new();
1401        original.set(TEST_COUNT, 42u32);
1402        original.set(TEST_NAME, "test".to_string());
1403        original.set(TEST_TIMEOUT, std::time::Duration::from_secs(10));
1404
1405        // Clone the attrs
1406        let cloned = original.clone();
1407
1408        // Verify that the clone has the same values
1409        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32));
1410        assert_eq!(cloned.get(TEST_NAME), Some(&"test".to_string()));
1411        assert_eq!(
1412            cloned.get(TEST_TIMEOUT),
1413            Some(&std::time::Duration::from_secs(10))
1414        );
1415
1416        // Verify that modifications to the original don't affect the clone
1417        original.set(TEST_COUNT, 100u32);
1418        assert_eq!(original.get(TEST_COUNT), Some(&100u32));
1419        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32)); // Clone should be unchanged
1420
1421        // Verify that modifications to the clone don't affect the original
1422        let mut cloned_mut = cloned.clone();
1423        cloned_mut.set(TEST_NAME, "modified".to_string());
1424        assert_eq!(cloned_mut.get(TEST_NAME), Some(&"modified".to_string()));
1425        assert_eq!(original.get(TEST_NAME), Some(&"test".to_string())); // Original should be unchanged
1426    }
1427
1428    #[test]
1429    fn test_debug_with_json() {
1430        let mut attrs = Attrs::new();
1431        attrs.set(TEST_COUNT, 42u32);
1432        attrs.set(TEST_NAME, "test".to_string());
1433
1434        // Test that Debug implementation works and contains JSON representations
1435        let debug_output = format!("{:?}", attrs);
1436
1437        // Should contain the struct name
1438        assert!(debug_output.contains("Attrs"));
1439
1440        // Should contain JSON representations of the values
1441        assert!(debug_output.contains("42"));
1442
1443        // Should contain the key names
1444        assert!(debug_output.contains("hyperactor_config::attrs::tests::test_count"));
1445        assert!(debug_output.contains("hyperactor_config::attrs::tests::test_name"));
1446
1447        // For strings, the JSON representation should be the escaped version
1448        // Let's check that the test string is actually present in some form
1449        assert!(debug_output.contains("test"));
1450    }
1451
1452    declare_attrs! {
1453        /// With default...
1454        attr TIMEOUT_WITH_DEFAULT: Duration = Duration::from_secs(10);
1455
1456        /// Just to ensure visibilty is parsed.
1457        pub(crate) attr CRATE_LOCAL_ATTR: String;
1458    }
1459
1460    #[test]
1461    fn test_defaults() {
1462        assert!(TIMEOUT_WITH_DEFAULT.has_default());
1463        assert!(!CRATE_LOCAL_ATTR.has_default());
1464
1465        assert_eq!(
1466            Attrs::new().get(TIMEOUT_WITH_DEFAULT),
1467            Some(&Duration::from_secs(10))
1468        );
1469    }
1470
1471    #[test]
1472    fn test_indexing() {
1473        let mut attrs = Attrs::new();
1474
1475        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(10));
1476        attrs[TIMEOUT_WITH_DEFAULT] = Duration::from_secs(100);
1477        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(100));
1478
1479        attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
1480        assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
1481    }
1482
1483    #[test]
1484    fn attrs_deserialize_unknown_key_is_error() {
1485        // Build a real Attrs, but inject a key that this binary does
1486        // NOT know about. We do that with
1487        // insert_value_by_name_unchecked(), which bypasses the
1488        // declare_attrs!/inventory registration path.
1489        //
1490        // Then:
1491        //   1. Serialize that Attrs with bincode (the real wire
1492        //      format).
1493        //   2. Attempt to bincode-deserialize those bytes back into
1494        //      Attrs.
1495        //
1496        // During deserialization, AttrsVisitor::visit_map() will:
1497        //   - read the key string
1498        //   - fail to find it in KEYS_BY_NAME (the compiled-in
1499        //     registry)
1500        //   - immediately error instead of trying to skip the value,
1501        //     because with bincode we can't safely consume an unknown
1502        //     typed value without risking stream desync.
1503        //
1504        // This reproduces exactly what happens when a parent proc
1505        // sends an Attrs containing a key the child binary wasn't
1506        // built with.
1507
1508        // Definitely not declared in this crate's inventory:
1509        let bad_key: &'static str = "monarch_hyperactor::pytokio::unawaited_pytokio_traceback";
1510
1511        // Make an Attrs that pretends to have that key. u32 already
1512        // implements AttrValue -> SerializableValue, so we can just
1513        // box a 0u32.
1514        let mut attrs = Attrs::new();
1515        attrs.insert_value_by_name_unchecked(bad_key, Box::new(0u32));
1516
1517        // Serialize this Attrs using bincode (same codec we use on
1518        // the wire).
1519        let wire_bytes = bincode::serde::encode_to_vec(&attrs, bincode::config::legacy()).unwrap();
1520
1521        // Now try to decode those bytes back into Attrs. This should
1522        // hit the unknown-key branch and return Err.
1523        let err =
1524            bincode::serde::decode_from_slice::<Attrs, _>(&wire_bytes, bincode::config::legacy())
1525                .map(|(v, _)| v)
1526                .expect_err("should error on unknown attr key");
1527
1528        let exe_str = std::env::current_exe()
1529            .ok()
1530            .map(|p| p.display().to_string())
1531            .unwrap_or_else(|| "<unknown-exe>".to_string());
1532        let msg = format!("{err}");
1533
1534        assert!(msg.contains("unknown attr key"), "got: {msg}");
1535        assert!(msg.contains(bad_key), "got: {msg}");
1536        assert!(msg.contains(&exe_str), "got: {msg}");
1537    }
1538
1539    // Verify that Vec<T> values work as attribute values when T:
1540    // AttrValue.
1541    //
1542    // This checks two things:
1543    //  1. A Vec<String> can be stored and retrieved through the Attrs
1544    //    API.
1545    //  2. The value survives a JSON round-trip via serde
1546    //     serialization.
1547    #[test]
1548    fn test_vec_string_attr() {
1549        declare_attrs! {
1550            attr TEST_VEC: Vec<String>;
1551        }
1552        let mut attrs = Attrs::new();
1553        let original = vec!["a".to_string(), "b".to_string()];
1554        attrs.set(TEST_VEC, original.clone());
1555        assert_eq!(attrs.get(TEST_VEC), Some(&original));
1556        // Round-trip through Attrs JSON serialization
1557        let json = serde_json::to_string(&attrs).unwrap();
1558        let attrs2: Attrs = serde_json::from_str(&json).unwrap();
1559        assert_eq!(attrs2.get(TEST_VEC), Some(&original));
1560        // Round-trip through AttrValue display/parse
1561        let displayed = AttrValue::display(&original);
1562        let parsed: Vec<String> = AttrValue::parse(&displayed).unwrap();
1563        assert_eq!(parsed, original);
1564    }
1565
1566    declare_attrs! {
1567        /// Test-local marker for the generic mechanism's
1568        /// membership / stamp / copy / enumeration tests. Scoped to
1569        /// the test module so the mechanism's coverage does not
1570        /// depend on any caller-defined marker vocabulary.
1571        pub attr TEST_GENERIC_MARKER: bool;
1572
1573        /// Declared attr carrying the test-local marker.
1574        @meta(TEST_GENERIC_MARKER = true)
1575        pub attr TEST_MARKED_ATTR: String;
1576
1577        /// Declared attr NOT carrying the test-local marker.
1578        pub attr TEST_UNMARKED_ATTR: String;
1579    }
1580
1581    // Generic marker-parameterized membership check: the helper
1582    // does not know about any specific category; callers supply
1583    // the marker. Validates that declared keys with
1584    // `@meta(TEST_GENERIC_MARKER = true)` match and keys without
1585    // do not.
1586    #[test]
1587    fn test_is_attr_marked_with() {
1588        assert_eq!(
1589            is_attr_marked_with(TEST_MARKED_ATTR.name(), TEST_GENERIC_MARKER),
1590            Some(true),
1591            "marked key must report Some(true)",
1592        );
1593        assert_eq!(
1594            is_attr_marked_with(TEST_UNMARKED_ATTR.name(), TEST_GENERIC_MARKER),
1595            None,
1596            "unmarked key must report None (marker not present on its meta)",
1597        );
1598        assert_eq!(
1599            is_attr_marked_with(
1600                "hyperactor_config::attrs::tests::no_such_key_declared_anywhere",
1601                TEST_GENERIC_MARKER,
1602            ),
1603            None,
1604            "unknown names must report None",
1605        );
1606    }
1607
1608    // Generic marker-parameterized Attrs → Flattrs stamp. Builds
1609    // an `Attrs` with one marked and one unmarked entry; stamps
1610    // via the generic helper with `TEST_GENERIC_MARKER`; asserts
1611    // only the marked entry lands on headers.
1612    #[test]
1613    fn test_stamp_marked_attrs_into_flattrs() {
1614        use crate::flattrs::Flattrs;
1615
1616        let mut attrs = Attrs::new();
1617        attrs.set(TEST_MARKED_ATTR, "marked_value".to_string());
1618        attrs.set(TEST_UNMARKED_ATTR, "unmarked_value".to_string());
1619
1620        let mut headers = Flattrs::new();
1621        stamp_marked_attrs_into_flattrs(&mut headers, &attrs, TEST_GENERIC_MARKER);
1622
1623        assert_eq!(
1624            headers.get(TEST_MARKED_ATTR),
1625            Some("marked_value".to_string()),
1626        );
1627        assert_eq!(headers.get::<String>(TEST_UNMARKED_ATTR), None);
1628    }
1629
1630    // Generic marker-parameterized Flattrs → Flattrs copy. The
1631    // source Flattrs gets populated with both marked and unmarked
1632    // values via typed `set` (by-key-hash stamping); the generic
1633    // copy resolves each key_hash through the inventory, filters by
1634    // marker, and copies only the marked entry.
1635    #[test]
1636    fn test_copy_marked_flattrs() {
1637        use crate::flattrs::Flattrs;
1638
1639        let mut src = Flattrs::new();
1640        src.set(TEST_MARKED_ATTR, "marked".to_string());
1641        src.set(TEST_UNMARKED_ATTR, "unmarked".to_string());
1642
1643        let mut dst = Flattrs::new();
1644        copy_marked_flattrs(&mut dst, &src, TEST_GENERIC_MARKER);
1645
1646        assert_eq!(dst.get(TEST_MARKED_ATTR), Some("marked".to_string()),);
1647        assert_eq!(dst.get::<String>(TEST_UNMARKED_ATTR), None);
1648    }
1649
1650    // Generic marker-parameterized enumeration. The test-local
1651    // `TEST_MARKED_ATTR` is marked, so it must appear; the
1652    // unmarked test attr must not.
1653    #[test]
1654    fn test_marked_attr_names_enumeration() {
1655        let names = marked_attr_names(TEST_GENERIC_MARKER);
1656        assert!(
1657            names.contains(TEST_MARKED_ATTR.name()),
1658            "marked test attr must appear in the vocabulary enumeration",
1659        );
1660        assert!(
1661            !names.contains(TEST_UNMARKED_ATTR.name()),
1662            "unmarked test attr must not appear in the vocabulary enumeration",
1663        );
1664    }
1665}