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