hyperactor/
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::attrs::Attrs;
24//! use hyperactor::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::attrs::Attrs;
50//! use hyperactor::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::attrs::Attrs;
79//! use hyperactor::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::ops::Index;
100use std::ops::IndexMut;
101use std::sync::LazyLock;
102
103use chrono::DateTime;
104use chrono::Utc;
105use erased_serde::Deserializer as ErasedDeserializer;
106use erased_serde::Serialize as ErasedSerialize;
107use serde::Deserialize;
108use serde::Deserializer;
109use serde::Serialize;
110use serde::Serializer;
111use serde::de::DeserializeOwned;
112use serde::de::MapAccess;
113use serde::de::Visitor;
114use serde::ser::SerializeMap;
115
116use crate::data::Named;
117
118// Information about an attribute key, used for automatic registration.
119// This needs to be public to be accessible from other crates, but it is
120// not part of the public API.
121#[doc(hidden)]
122pub struct AttrKeyInfo {
123    /// Name of the key
124    pub name: &'static str,
125    /// Function to get the type hash of the associated value type
126    pub typehash: fn() -> u64,
127    /// Deserializer function that deserializes directly from any deserializer
128    pub deserialize_erased:
129        fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
130    /// Meta-attributes.
131    pub meta: &'static LazyLock<Attrs>,
132    /// Display an attribute value using AttrValue::display.
133    pub display: fn(&dyn SerializableValue) -> String,
134    /// Parse an attribute value using AttrValue::parse.
135    pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
136    /// Default value for the attribute, if any.
137    pub default: Option<&'static dyn SerializableValue>,
138    /// A reference to the relevant key object with the associated
139    /// type parameter erased. Can be downcast to a concrete Key<T>.
140    pub erased: &'static dyn ErasedKey,
141}
142
143inventory::collect!(AttrKeyInfo);
144
145/// A typed key for the attribute dictionary.
146///
147/// Each key is associated with a specific type T and has a unique name.
148/// Keys are typically created using the `declare_attrs!` macro which ensures they have
149/// static lifetime and automatically registers them for serialization.
150pub struct Key<T: 'static> {
151    name: &'static str,
152    default_value: Option<&'static T>,
153    attrs: &'static LazyLock<Attrs>,
154}
155
156impl<T: Named + 'static> Key<T> {
157    /// Creates a new key with the given name.
158    pub const fn new(
159        name: &'static str,
160        default_value: Option<&'static T>,
161        attrs: &'static LazyLock<Attrs>,
162    ) -> Self {
163        Self {
164            name,
165            default_value,
166            attrs,
167        }
168    }
169
170    /// Returns the name of this key.
171    pub fn name(&self) -> &'static str {
172        self.name
173    }
174
175    /// Returns a reference to the default value for this key, if one exists.
176    pub fn default(&self) -> Option<&'static T> {
177        self.default_value
178    }
179
180    /// Returns whether this key has a default value.
181    pub fn has_default(&self) -> bool {
182        self.default_value.is_some()
183    }
184
185    /// Returns the type hash of the associated value type.
186    pub fn typehash(&self) -> u64 {
187        T::typehash()
188    }
189
190    /// The attributes associated with this key.
191    pub fn attrs(&self) -> &'static LazyLock<Attrs> {
192        self.attrs
193    }
194}
195
196impl<T: 'static> Clone for Key<T> {
197    fn clone(&self) -> Self {
198        // Use Copy.
199        *self
200    }
201}
202
203impl<T: 'static> Copy for Key<T> {}
204
205/// A trait for type-erased keys.
206pub trait ErasedKey: Any + Send + Sync + 'static {
207    /// The name of the key.
208    fn name(&self) -> &'static str;
209
210    /// The typehash of the key's associated type.
211    fn typehash(&self) -> u64;
212
213    /// The typename of the key's associated type.
214    fn typename(&self) -> &'static str;
215}
216
217impl dyn ErasedKey {
218    /// Downcast a type-erased key to a specific key type.
219    pub fn downcast_ref<T: Named + 'static>(&'static self) -> Option<&'static Key<T>> {
220        (self as &dyn Any).downcast_ref::<Key<T>>()
221    }
222}
223
224impl<T: AttrValue> ErasedKey for Key<T> {
225    fn name(&self) -> &'static str {
226        self.name
227    }
228
229    fn typehash(&self) -> u64 {
230        T::typehash()
231    }
232
233    fn typename(&self) -> &'static str {
234        T::typename()
235    }
236}
237
238// Enable attr[key] syntax.
239impl<T: AttrValue> Index<Key<T>> for Attrs {
240    type Output = T;
241
242    fn index(&self, key: Key<T>) -> &Self::Output {
243        self.get(key).unwrap()
244    }
245}
246
247// TODO: separately type keys with defaults, so that we can statically enforce that indexmut is only
248// called on keys with defaults.
249impl<T: AttrValue> IndexMut<Key<T>> for Attrs {
250    fn index_mut(&mut self, key: Key<T>) -> &mut Self::Output {
251        self.get_mut(key).unwrap()
252    }
253}
254
255/// This trait must be implemented by all attribute values. In addition to enforcing
256/// the supertrait `Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone`,
257/// `AttrValue` requires that the type be representable in "display" format.
258///
259/// `AttrValue` includes its own `display` and `parse` so that behavior can be tailored
260/// for attribute purposes specifically, allowing common types like `Duration` to be used
261/// without modification.
262///
263/// This crate includes a derive macro for AttrValue, which uses the type's
264/// `std::string::ToString` for display, and `std::str::FromStr` for parsing.
265pub trait AttrValue:
266    Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone + 'static
267{
268    /// Display the value, typically using [`std::fmt::Display`].
269    /// This is called to show the output in human-readable form.
270    fn display(&self) -> String;
271
272    /// Parse a value from a string, typically using [`std::str::FromStr`].
273    fn parse(value: &str) -> Result<Self, anyhow::Error>;
274}
275
276/// Macro to implement AttrValue for types that implement ToString and FromStr.
277///
278/// This macro provides a convenient way to implement AttrValue for types that already
279/// have string conversion capabilities through the standard ToString and FromStr traits.
280///
281/// # Usage
282///
283/// ```ignore
284/// impl_attrvalue!(i32, u64, f64);
285/// ```
286///
287/// This will generate AttrValue implementations for i32, u64, and f64 that use
288/// their ToString and FromStr implementations for display and parsing.
289#[macro_export]
290macro_rules! impl_attrvalue {
291    ($($ty:ty),+ $(,)?) => {
292        $(
293            impl $crate::attrs::AttrValue for $ty {
294                fn display(&self) -> String {
295                    self.to_string()
296                }
297
298                fn parse(value: &str) -> Result<Self, anyhow::Error> {
299                    value.parse().map_err(|e| anyhow::anyhow!("failed to parse {}: {}", stringify!($ty), e))
300                }
301            }
302        )+
303    };
304}
305
306// pub use impl_attrvalue;
307
308// Implement AttrValue for common standard library types
309impl_attrvalue!(
310    bool,
311    i8,
312    i16,
313    i32,
314    i64,
315    i128,
316    isize,
317    u8,
318    u16,
319    u32,
320    u64,
321    u128,
322    usize,
323    f32,
324    f64,
325    String,
326    std::net::IpAddr,
327    std::net::Ipv4Addr,
328    std::net::Ipv6Addr,
329    crate::ActorId,
330    ndslice::Shape,
331    ndslice::Point,
332);
333
334impl AttrValue for std::time::Duration {
335    fn display(&self) -> String {
336        humantime::format_duration(*self).to_string()
337    }
338
339    fn parse(value: &str) -> Result<Self, anyhow::Error> {
340        Ok(humantime::parse_duration(value)?)
341    }
342}
343
344impl AttrValue for std::time::SystemTime {
345    fn display(&self) -> String {
346        let datetime: DateTime<Utc> = (*self).into();
347        datetime.to_rfc3339()
348    }
349
350    fn parse(value: &str) -> Result<Self, anyhow::Error> {
351        let datetime = DateTime::parse_from_rfc3339(value)?;
352        Ok(datetime.into())
353    }
354}
355
356// Internal trait for type-erased serialization
357#[doc(hidden)]
358pub trait SerializableValue: Send + Sync {
359    /// Get a reference to this value as Any for downcasting
360    fn as_any(&self) -> &dyn Any;
361    /// Get a mutable reference to this value as Any for downcasting
362    fn as_any_mut(&mut self) -> &mut dyn Any;
363    /// Get a reference to this value as an erased serializable trait object
364    fn as_erased_serialize(&self) -> &dyn ErasedSerialize;
365    /// Clone the underlying value, retaining dyn compatibility.
366    fn cloned(&self) -> Box<dyn SerializableValue>;
367    /// Display the value
368    fn display(&self) -> String;
369}
370
371impl<T: AttrValue> SerializableValue for T {
372    fn as_any(&self) -> &dyn Any {
373        self
374    }
375
376    fn as_any_mut(&mut self) -> &mut dyn Any {
377        self
378    }
379
380    fn as_erased_serialize(&self) -> &dyn ErasedSerialize {
381        self
382    }
383
384    fn cloned(&self) -> Box<dyn SerializableValue> {
385        Box::new(self.clone())
386    }
387
388    fn display(&self) -> String {
389        self.display()
390    }
391}
392
393/// A heterogeneous, strongly-typed attribute dictionary with serialization support.
394///
395/// This dictionary stores key-value pairs where:
396/// - Keys are type-safe and must be predefined with their associated types
397/// - Values must implement [`AttrValue`]
398/// - The entire dictionary can be serialized to/from JSON automatically
399///
400/// # Type Safety
401///
402/// The dictionary enforces type safety at compile time. You cannot retrieve a value
403/// with the wrong type, and the compiler will catch such errors.
404///
405/// # Serialization
406///
407/// The dictionary can be serialized using serde. During serialization, each value
408/// is serialized with its key name. During deserialization, the automatically registered
409/// key information is used to determine the correct type for each value.
410pub struct Attrs {
411    values: HashMap<&'static str, Box<dyn SerializableValue>>,
412}
413
414impl Attrs {
415    /// Create a new empty attribute dictionary.
416    pub fn new() -> Self {
417        Self {
418            values: HashMap::new(),
419        }
420    }
421
422    /// Set a value for the given key.
423    pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
424        self.values.insert(key.name, Box::new(value));
425    }
426
427    fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
428        if self.contains_key(key) {
429            return;
430        }
431        let Some(default) = key.default() else { return };
432        self.set(key, default.clone());
433    }
434
435    /// Get a value for the given key, returning None if not present. If the key has a default value,
436    /// that is returned instead.
437    pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
438        self.values
439            .get(key.name)
440            .and_then(|value| value.as_any().downcast_ref::<T>())
441            .or_else(|| key.default())
442    }
443
444    /// Get a mutable reference to a value for the given key. If the key has a default value, it is
445    /// first set, and then returned as a mutable reference.
446    pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
447        self.maybe_set_from_default(key);
448        self.values
449            .get_mut(key.name)
450            .and_then(|value| value.as_any_mut().downcast_mut::<T>())
451    }
452
453    /// Remove a value for the given key, returning it if present.
454    pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
455        // TODO: return value (this is tricky because of the type erasure)
456        self.values.remove(key.name).is_some()
457    }
458
459    /// Checks if the given key exists in the dictionary.
460    pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
461        self.values.contains_key(key.name)
462    }
463
464    /// Returns the number of key-value pairs in the dictionary.
465    pub fn len(&self) -> usize {
466        self.values.len()
467    }
468
469    /// Returns true if the dictionary is empty.
470    pub fn is_empty(&self) -> bool {
471        self.values.is_empty()
472    }
473
474    /// Clear all key-value pairs from the dictionary.
475    pub fn clear(&mut self) {
476        self.values.clear();
477    }
478
479    // Internal methods for config guard support
480    /// Take a value by key name, returning the boxed value if present
481    pub(crate) fn remove_value<T: 'static>(
482        &mut self,
483        key: Key<T>,
484    ) -> Option<Box<dyn SerializableValue>> {
485        self.values.remove(key.name)
486    }
487
488    /// Restore a value by key name
489    pub(crate) fn insert_value<T: 'static>(
490        &mut self,
491        key: Key<T>,
492        value: Box<dyn SerializableValue>,
493    ) {
494        self.values.insert(key.name, value);
495    }
496
497    /// Restore a value by key name
498    pub(crate) fn insert_value_by_name_unchecked(
499        &mut self,
500        name: &'static str,
501        value: Box<dyn SerializableValue>,
502    ) {
503        self.values.insert(name, value);
504    }
505
506    /// Internal getter by key name for explicitly-set values (no
507    /// defaults).
508    pub(crate) fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
509        self.values.get(name).map(|b| b.as_ref())
510    }
511}
512
513impl Clone for Attrs {
514    fn clone(&self) -> Self {
515        let mut values = HashMap::new();
516        for (key, value) in &self.values {
517            values.insert(*key, value.cloned());
518        }
519        Self { values }
520    }
521}
522
523impl std::fmt::Display for Attrs {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        let mut first = true;
526        for (key, value) in &self.values {
527            if first {
528                first = false;
529            } else {
530                write!(f, ",")?;
531            }
532            write!(f, "{}={}", key, value.display())?
533        }
534        Ok(())
535    }
536}
537
538impl std::fmt::Debug for Attrs {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        // Create a map of key names to their JSON representation for debugging
541        let mut debug_map = std::collections::BTreeMap::new();
542        for (key, value) in &self.values {
543            match serde_json::to_string(value.as_erased_serialize()) {
544                Ok(json) => {
545                    debug_map.insert(*key, json);
546                }
547                Err(_) => {
548                    debug_map.insert(*key, "<serialization error>".to_string());
549                }
550            }
551        }
552
553        f.debug_struct("Attrs").field("values", &debug_map).finish()
554    }
555}
556
557impl Default for Attrs {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563impl Serialize for Attrs {
564    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
565    where
566        S: Serializer,
567    {
568        let mut map = serializer.serialize_map(Some(self.values.len()))?;
569
570        for (key_name, value) in &self.values {
571            map.serialize_entry(key_name, value.as_erased_serialize())?;
572        }
573
574        map.end()
575    }
576}
577
578struct AttrsVisitor;
579
580impl<'de> Visitor<'de> for AttrsVisitor {
581    type Value = Attrs;
582
583    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
584        formatter.write_str("a map of attribute keys to their serialized values")
585    }
586
587    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
588    where
589        M: MapAccess<'de>,
590    {
591        static KEYS_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static AttrKeyInfo>> =
592            std::sync::LazyLock::new(|| {
593                inventory::iter::<AttrKeyInfo>()
594                    .map(|info| (info.name, info))
595                    .collect()
596            });
597        let keys_by_name = &*KEYS_BY_NAME;
598
599        let mut attrs = Attrs::new();
600        while let Some(key_name) = access.next_key::<String>()? {
601            let Some(&key) = keys_by_name.get(key_name.as_str()) else {
602                // Silently ignore unknown keys
603                access.next_value::<serde::de::IgnoredAny>()?;
604                continue;
605            };
606
607            // Create a seed to deserialize the value using erased_serde
608            let seed = ValueDeserializeSeed {
609                deserialize_erased: key.deserialize_erased,
610            };
611            match access.next_value_seed(seed) {
612                Ok(value) => {
613                    attrs.values.insert(key.name, value);
614                }
615                Err(err) => {
616                    return Err(serde::de::Error::custom(format!(
617                        "failed to deserialize value for key {}: {}",
618                        key_name, err
619                    )));
620                }
621            }
622        }
623
624        Ok(attrs)
625    }
626}
627
628/// Helper struct to deserialize values using erased_serde
629struct ValueDeserializeSeed {
630    deserialize_erased:
631        fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
632}
633
634impl<'de> serde::de::DeserializeSeed<'de> for ValueDeserializeSeed {
635    type Value = Box<dyn SerializableValue>;
636
637    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
638    where
639        D: serde::de::Deserializer<'de>,
640    {
641        let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
642        (self.deserialize_erased)(&mut erased).map_err(serde::de::Error::custom)
643    }
644}
645
646impl<'de> Deserialize<'de> for Attrs {
647    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
648    where
649        D: Deserializer<'de>,
650    {
651        deserializer.deserialize_map(AttrsVisitor)
652    }
653}
654
655// Converts an ASCII string to lowercase at compile time.
656// Returns a const string with lowercase ASCII characters.
657#[doc(hidden)]
658pub const fn ascii_to_lowercase_const<const N: usize>(input: &str) -> [u8; N] {
659    let bytes = input.as_bytes();
660    let mut result = [0u8; N];
661    let mut i = 0;
662
663    while i < bytes.len() && i < N {
664        let byte = bytes[i];
665        if byte >= b'A' && byte <= b'Z' {
666            result[i] = byte + 32; // Convert to lowercase
667        } else {
668            result[i] = byte;
669        }
670        i += 1;
671    }
672
673    result
674}
675
676// Macro to generate a const lowercase string at compile time
677#[doc(hidden)]
678#[macro_export]
679macro_rules! const_ascii_lowercase {
680    ($s:expr) => {{
681        const INPUT: &str = $s;
682        const LEN: usize = INPUT.len();
683        const BYTES: [u8; LEN] = $crate::attrs::ascii_to_lowercase_const::<LEN>(INPUT);
684        // Safety: We're converting ASCII to ASCII, so it's valid UTF-8
685        unsafe { std::str::from_utf8_unchecked(&BYTES) }
686    }};
687}
688
689/// Macro to check check that a trait is implemented, generating a
690/// nice error message if it isn't.
691#[doc(hidden)]
692#[macro_export]
693macro_rules! assert_impl {
694    ($ty:ty, $trait:path) => {
695        const _: fn() = || {
696            fn check<T: $trait>() {}
697            check::<$ty>();
698        };
699    };
700}
701
702/// Declares attribute keys using a lazy_static! style syntax.
703///
704/// # Syntax
705///
706/// ```ignore
707/// declare_attrs! {
708///     /// Documentation for the key (default visibility).
709///     attr KEY_NAME: Type = default_value;
710///
711///     /// Another key (default value is optional)
712///     pub attr ANOTHER_KEY: AnotherType;
713/// }
714/// ```
715///
716/// # Arguments
717///
718/// * Optional visibility modifier (`pub`, `pub(crate)`, etc.)
719/// * `attr` keyword (required)
720/// * Key name (identifier)
721/// * Type of values this key can store
722/// * Optional default value
723///
724/// # Example
725///
726/// ```
727/// use std::time::Duration;
728///
729/// use hyperactor::attrs::Attrs;
730/// use hyperactor::attrs::declare_attrs;
731///
732/// declare_attrs! {
733///     /// Timeout for RPC operations
734///     pub attr TIMEOUT: Duration = Duration::from_secs(30);
735///
736///     /// Maximum number of retry attempts (no default specified)
737///     attr MAX_RETRIES: u32;
738/// }
739///
740/// let mut attrs = Attrs::new();
741/// assert_eq!(attrs.get(TIMEOUT), Some(&Duration::from_secs(30)));
742/// attrs.set(MAX_RETRIES, 5);
743/// ```
744#[macro_export]
745macro_rules! declare_attrs {
746    // Handle multiple attribute keys with optional default values and optional meta attributes
747    ($(
748        $(#[$attr:meta])*
749        $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))*
750        $vis:vis attr $name:ident: $type:ty $(= $default:expr)?;
751    )*) => {
752        $(
753            $crate::declare_attrs! {
754                @single
755                $(@meta($($meta_key = $meta_value),*))*
756                $(#[$attr])* ;
757                $vis attr $name: $type $(= $default)?;
758            }
759        )*
760    };
761
762    // Handle single attribute key with default value and meta attributes
763    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
764        $crate::assert_impl!($type, $crate::attrs::AttrValue);
765
766        // Create a static default value
767        $crate::paste! {
768            static [<$name _DEFAULT>]: $type = $default;
769            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
770                std::sync::LazyLock::new(|| {
771                    #[allow(unused_mut)]
772                    let mut attrs = $crate::attrs::Attrs::new();
773                    $($(
774                        attrs.set($meta_key, $meta_value);
775                    )*)*
776                    attrs
777                });
778        }
779
780        $(#[$attr])*
781        $vis static $name: $crate::attrs::Key<$type> = {
782            $crate::assert_impl!($type, $crate::attrs::AttrValue);
783
784            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
785            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
786            $crate::paste! {
787                $crate::attrs::Key::new(
788                    LOWER_NAME,
789                    Some(&[<$name _DEFAULT>]),
790                    $crate::paste! { &[<$name _META_ATTRS>] },
791                )
792            }
793        };
794
795        // Register the key for serialization
796        $crate::submit! {
797            $crate::attrs::AttrKeyInfo {
798                name: {
799                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
800                    $crate::const_ascii_lowercase!(FULL_NAME)
801                },
802                typehash: <$type as $crate::data::Named>::typehash,
803                deserialize_erased: |deserializer| {
804                    let value: $type = erased_serde::deserialize(deserializer)?;
805                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
806                },
807                meta: $crate::paste! { &[<$name _META_ATTRS>] },
808                display: |value: &dyn $crate::attrs::SerializableValue| {
809                    let value = value.as_any().downcast_ref::<$type>().unwrap();
810                    $crate::attrs::AttrValue::display(value)
811                },
812                parse: |value: &str| {
813                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
814                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
815                },
816                default: Some($crate::paste! { &[<$name _DEFAULT>] }),
817                erased: &$name,
818            }
819        }
820    };
821
822    // Handle single attribute key without default value but with meta attributes
823    (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
824        $crate::assert_impl!($type, $crate::attrs::AttrValue);
825
826        $crate::paste! {
827            static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
828            std::sync::LazyLock::new(|| {
829                #[allow(unused_mut)]
830                let mut attrs = $crate::attrs::Attrs::new();
831                $($(
832                    // Note: This assumes meta keys are already declared somewhere
833                    // The user needs to ensure the meta keys exist and are in scope
834                    attrs.set($meta_key, $meta_value);
835                )*)*
836                attrs
837            });
838        }
839
840        $(#[$attr])*
841        $vis static $name: $crate::attrs::Key<$type> = {
842            const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
843            const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
844            $crate::attrs::Key::new(LOWER_NAME, None, $crate::paste! { &[<$name _META_ATTRS>] })
845        };
846
847
848        // Register the key for serialization
849        $crate::submit! {
850            $crate::attrs::AttrKeyInfo {
851                name: {
852                    const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
853                    $crate::const_ascii_lowercase!(FULL_NAME)
854                },
855                typehash: <$type as $crate::data::Named>::typehash,
856                deserialize_erased: |deserializer| {
857                    let value: $type = erased_serde::deserialize(deserializer)?;
858                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
859                },
860                meta: $crate::paste! { &[<$name _META_ATTRS>] },
861                display: |value: &dyn $crate::attrs::SerializableValue| {
862                    let value = value.as_any().downcast_ref::<$type>().unwrap();
863                    $crate::attrs::AttrValue::display(value)
864                },
865                parse: |value: &str| {
866                    let value: $type = $crate::attrs::AttrValue::parse(value)?;
867                    Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
868                },
869                default: None,
870                erased: &$name,
871            }
872        }
873    };
874}
875
876pub use declare_attrs;
877
878#[cfg(test)]
879mod tests {
880    use std::time::Duration;
881
882    use super::*;
883
884    declare_attrs! {
885        attr TEST_TIMEOUT: Duration;
886        attr TEST_COUNT: u32;
887        @meta(TEST_COUNT = 42)
888        pub attr TEST_NAME: String;
889    }
890
891    #[test]
892    fn test_basic_operations() {
893        let mut attrs = Attrs::new();
894
895        // Test setting and getting values
896        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
897        attrs.set(TEST_COUNT, 42u32);
898        attrs.set(TEST_NAME, "test".to_string());
899
900        assert_eq!(attrs.get(TEST_TIMEOUT), Some(&Duration::from_secs(5)));
901        assert_eq!(attrs.get(TEST_COUNT), Some(&42u32));
902        assert_eq!(attrs.get(TEST_NAME), Some(&"test".to_string()));
903
904        // Test contains_key
905        assert!(attrs.contains_key(TEST_TIMEOUT));
906        assert!(attrs.contains_key(TEST_COUNT));
907        assert!(attrs.contains_key(TEST_NAME));
908
909        // Test len
910        assert_eq!(attrs.len(), 3);
911        assert!(!attrs.is_empty());
912
913        // Meta attribute:
914        assert_eq!(TEST_NAME.attrs().get(TEST_COUNT).unwrap(), &42u32);
915    }
916
917    #[test]
918    fn test_get_mut() {
919        let mut attrs = Attrs::new();
920        attrs.set(TEST_COUNT, 10u32);
921
922        if let Some(count) = attrs.get_mut(TEST_COUNT) {
923            *count += 5;
924        }
925
926        assert_eq!(attrs.get(TEST_COUNT), Some(&15u32));
927    }
928
929    #[test]
930    fn test_remove() {
931        let mut attrs = Attrs::new();
932        attrs.set(TEST_COUNT, 42u32);
933
934        let removed = attrs.remove(TEST_COUNT);
935        assert!(removed);
936        assert_eq!(attrs.get(TEST_COUNT), None);
937        assert!(!attrs.contains_key(TEST_COUNT));
938    }
939
940    #[test]
941    fn test_clear() {
942        let mut attrs = Attrs::new();
943        attrs.set(TEST_TIMEOUT, Duration::from_secs(1));
944        attrs.set(TEST_COUNT, 42u32);
945
946        attrs.clear();
947        assert!(attrs.is_empty());
948        assert_eq!(attrs.len(), 0);
949    }
950
951    #[test]
952    fn test_key_properties() {
953        assert_eq!(
954            TEST_TIMEOUT.name(),
955            "hyperactor::attrs::tests::test_timeout"
956        );
957    }
958
959    #[test]
960    fn test_serialization() {
961        let mut attrs = Attrs::new();
962        attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
963        attrs.set(TEST_COUNT, 42u32);
964        attrs.set(TEST_NAME, "test".to_string());
965
966        // Test serialization
967        let serialized = serde_json::to_string(&attrs).expect("Failed to serialize");
968
969        // The serialized string should contain the key names and their values
970        assert!(serialized.contains("hyperactor::attrs::tests::test_timeout"));
971        assert!(serialized.contains("hyperactor::attrs::tests::test_count"));
972        assert!(serialized.contains("hyperactor::attrs::tests::test_name"));
973    }
974
975    #[test]
976    fn test_deserialization() {
977        // Create original attrs
978        let mut original_attrs = Attrs::new();
979        original_attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
980        original_attrs.set(TEST_COUNT, 42u32);
981        original_attrs.set(TEST_NAME, "test".to_string());
982
983        // Serialize
984        let serialized = serde_json::to_string(&original_attrs).expect("Failed to serialize");
985
986        // Deserialize (no manual registry needed!)
987        let deserialized_attrs: Attrs =
988            serde_json::from_str(&serialized).expect("Failed to deserialize");
989
990        // Verify the deserialized values
991        assert_eq!(
992            deserialized_attrs.get(TEST_TIMEOUT),
993            Some(&Duration::from_secs(5))
994        );
995        assert_eq!(deserialized_attrs.get(TEST_COUNT), Some(&42u32));
996        assert_eq!(deserialized_attrs.get(TEST_NAME), Some(&"test".to_string()));
997    }
998
999    #[test]
1000    fn test_roundtrip_serialization() {
1001        // Create original attrs
1002        let mut original = Attrs::new();
1003        original.set(TEST_TIMEOUT, Duration::from_secs(10));
1004        original.set(TEST_COUNT, 5u32);
1005        original.set(TEST_NAME, "test-service".to_string());
1006
1007        // Serialize
1008        let serialized = serde_json::to_string(&original).unwrap();
1009
1010        // Deserialize
1011        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1012
1013        // Verify round-trip worked
1014        assert_eq!(
1015            deserialized.get(TEST_TIMEOUT),
1016            Some(&Duration::from_secs(10))
1017        );
1018        assert_eq!(deserialized.get(TEST_COUNT), Some(&5u32));
1019        assert_eq!(
1020            deserialized.get(TEST_NAME),
1021            Some(&"test-service".to_string())
1022        );
1023    }
1024
1025    #[test]
1026    fn test_empty_attrs_serialization() {
1027        let attrs = Attrs::new();
1028        let serialized = serde_json::to_string(&attrs).unwrap();
1029
1030        // Empty attrs should serialize to empty JSON object
1031        assert_eq!(serialized, "{}");
1032
1033        let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1034
1035        assert!(deserialized.is_empty());
1036    }
1037
1038    #[test]
1039    fn test_format_independence() {
1040        // Test that proves we're using the serializer directly, not JSON internally
1041        let mut attrs = Attrs::new();
1042        attrs.set(TEST_COUNT, 42u32);
1043        attrs.set(TEST_NAME, "test".to_string());
1044
1045        // Serialize to different formats
1046        let json_output = serde_json::to_string(&attrs).unwrap();
1047        let yaml_output = serde_yaml::to_string(&attrs).unwrap();
1048
1049        // JSON should have colons and quotes
1050        assert!(json_output.contains(":"));
1051        assert!(json_output.contains("\""));
1052
1053        // JSON should serialize numbers as numbers, not strings
1054        assert!(json_output.contains("42"));
1055        assert!(!json_output.contains("\"42\""));
1056
1057        // YAML should have colons but different formatting
1058        assert!(yaml_output.contains(":"));
1059        assert!(yaml_output.contains("42"));
1060
1061        // YAML shouldn't quote simple strings or numbers
1062        assert!(!yaml_output.contains("\"42\""));
1063
1064        // The outputs should be different (proving different serializers were used)
1065        assert_ne!(json_output, yaml_output);
1066
1067        // Verify that both can be deserialized correctly
1068        let from_json: Attrs = serde_json::from_str(&json_output).unwrap();
1069        let from_yaml: Attrs = serde_yaml::from_str(&yaml_output).unwrap();
1070
1071        assert_eq!(from_json.get(TEST_COUNT), Some(&42u32));
1072        assert_eq!(from_yaml.get(TEST_COUNT), Some(&42u32));
1073        assert_eq!(from_json.get(TEST_NAME), Some(&"test".to_string()));
1074        assert_eq!(from_yaml.get(TEST_NAME), Some(&"test".to_string()));
1075    }
1076
1077    #[test]
1078    fn test_clone() {
1079        // Create original attrs with multiple types
1080        let mut original = Attrs::new();
1081        original.set(TEST_COUNT, 42u32);
1082        original.set(TEST_NAME, "test".to_string());
1083        original.set(TEST_TIMEOUT, std::time::Duration::from_secs(10));
1084
1085        // Clone the attrs
1086        let cloned = original.clone();
1087
1088        // Verify that the clone has the same values
1089        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32));
1090        assert_eq!(cloned.get(TEST_NAME), Some(&"test".to_string()));
1091        assert_eq!(
1092            cloned.get(TEST_TIMEOUT),
1093            Some(&std::time::Duration::from_secs(10))
1094        );
1095
1096        // Verify that modifications to the original don't affect the clone
1097        original.set(TEST_COUNT, 100u32);
1098        assert_eq!(original.get(TEST_COUNT), Some(&100u32));
1099        assert_eq!(cloned.get(TEST_COUNT), Some(&42u32)); // Clone should be unchanged
1100
1101        // Verify that modifications to the clone don't affect the original
1102        let mut cloned_mut = cloned.clone();
1103        cloned_mut.set(TEST_NAME, "modified".to_string());
1104        assert_eq!(cloned_mut.get(TEST_NAME), Some(&"modified".to_string()));
1105        assert_eq!(original.get(TEST_NAME), Some(&"test".to_string())); // Original should be unchanged
1106    }
1107
1108    #[test]
1109    fn test_debug_with_json() {
1110        let mut attrs = Attrs::new();
1111        attrs.set(TEST_COUNT, 42u32);
1112        attrs.set(TEST_NAME, "test".to_string());
1113
1114        // Test that Debug implementation works and contains JSON representations
1115        let debug_output = format!("{:?}", attrs);
1116
1117        // Should contain the struct name
1118        assert!(debug_output.contains("Attrs"));
1119
1120        // Should contain JSON representations of the values
1121        assert!(debug_output.contains("42"));
1122
1123        // Should contain the key names
1124        assert!(debug_output.contains("hyperactor::attrs::tests::test_count"));
1125        assert!(debug_output.contains("hyperactor::attrs::tests::test_name"));
1126
1127        // For strings, the JSON representation should be the escaped version
1128        // Let's check that the test string is actually present in some form
1129        assert!(debug_output.contains("test"));
1130    }
1131
1132    declare_attrs! {
1133        /// With default...
1134        attr TIMEOUT_WITH_DEFAULT: Duration = Duration::from_secs(10);
1135
1136        /// Just to ensure visibilty is parsed.
1137        pub(crate) attr CRATE_LOCAL_ATTR: String;
1138    }
1139
1140    #[test]
1141    fn test_defaults() {
1142        assert!(TIMEOUT_WITH_DEFAULT.has_default());
1143        assert!(!CRATE_LOCAL_ATTR.has_default());
1144
1145        assert_eq!(
1146            Attrs::new().get(TIMEOUT_WITH_DEFAULT),
1147            Some(&Duration::from_secs(10))
1148        );
1149    }
1150
1151    #[test]
1152    fn test_indexing() {
1153        let mut attrs = Attrs::new();
1154
1155        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(10));
1156        attrs[TIMEOUT_WITH_DEFAULT] = Duration::from_secs(100);
1157        assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(100));
1158
1159        attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
1160        assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
1161    }
1162}