1use std::any::Any;
98use std::collections::HashMap;
99use std::fmt::Display;
100use std::ops::Index;
101use std::ops::IndexMut;
102use std::str::FromStr;
103use std::sync::LazyLock;
104
105use chrono::DateTime;
106use chrono::Utc;
107use erased_serde::Deserializer as ErasedDeserializer;
108use erased_serde::Serialize as ErasedSerialize;
109use serde::Deserialize;
110use serde::Deserializer;
111use serde::Serialize;
112use serde::Serializer;
113use serde::de::DeserializeOwned;
114use serde::de::MapAccess;
115use serde::de::Visitor;
116use serde::ser::SerializeMap;
117use typeuri::Named;
118
119#[doc(hidden)]
123pub struct AttrKeyInfo {
124 pub name: &'static str,
126 pub typehash: fn() -> u64,
128 pub deserialize_erased:
130 fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
131 pub meta: &'static LazyLock<Attrs>,
133 pub display: fn(&dyn SerializableValue) -> String,
135 pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
137 pub default: Option<&'static dyn SerializableValue>,
139 pub erased: &'static dyn ErasedKey,
142}
143
144inventory::collect!(AttrKeyInfo);
145
146pub struct Key<T: 'static> {
152 name: &'static str,
153 default_value: Option<&'static T>,
154 attrs: &'static LazyLock<Attrs>,
155}
156
157impl<T> Key<T> {
158 pub fn name(&self) -> &'static str {
160 self.name
161 }
162}
163
164impl<T: Named + 'static> Key<T> {
165 pub const fn new(
167 name: &'static str,
168 default_value: Option<&'static T>,
169 attrs: &'static LazyLock<Attrs>,
170 ) -> Self {
171 Self {
172 name,
173 default_value,
174 attrs,
175 }
176 }
177
178 pub fn default(&self) -> Option<&'static T> {
180 self.default_value
181 }
182
183 pub fn has_default(&self) -> bool {
185 self.default_value.is_some()
186 }
187
188 pub fn typehash(&self) -> u64 {
190 T::typehash()
191 }
192
193 pub fn attrs(&self) -> &'static LazyLock<Attrs> {
195 self.attrs
196 }
197}
198
199impl<T: 'static> Clone for Key<T> {
200 fn clone(&self) -> Self {
201 *self
203 }
204}
205
206impl<T: 'static> Copy for Key<T> {}
207
208pub trait ErasedKey: Any + Send + Sync + 'static {
210 fn name(&self) -> &'static str;
212
213 fn typehash(&self) -> u64;
215
216 fn typename(&self) -> &'static str;
218}
219
220impl dyn ErasedKey {
221 pub fn downcast_ref<T: Named + 'static>(&'static self) -> Option<&'static Key<T>> {
223 (self as &dyn Any).downcast_ref::<Key<T>>()
224 }
225}
226
227impl<T: AttrValue> ErasedKey for Key<T> {
228 fn name(&self) -> &'static str {
229 self.name
230 }
231
232 fn typehash(&self) -> u64 {
233 T::typehash()
234 }
235
236 fn typename(&self) -> &'static str {
237 T::typename()
238 }
239}
240
241impl<T: AttrValue> Index<Key<T>> for Attrs {
243 type Output = T;
244
245 fn index(&self, key: Key<T>) -> &Self::Output {
246 self.get(key).unwrap()
247 }
248}
249
250impl<T: AttrValue> IndexMut<Key<T>> for Attrs {
253 fn index_mut(&mut self, key: Key<T>) -> &mut Self::Output {
254 self.get_mut(key).unwrap()
255 }
256}
257
258pub trait AttrValue:
269 Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone + 'static
270{
271 fn display(&self) -> String;
274
275 fn parse(value: &str) -> Result<Self, anyhow::Error>;
277}
278
279#[macro_export]
293macro_rules! impl_attrvalue {
294 ($($ty:ty),+ $(,)?) => {
295 $(
296 impl $crate::attrs::AttrValue for $ty {
297 fn display(&self) -> String {
298 self.to_string()
299 }
300
301 fn parse(value: &str) -> Result<Self, anyhow::Error> {
302 value.parse().map_err(|e| anyhow::anyhow!("failed to parse {}: {}", stringify!($ty), e))
303 }
304 }
305 )+
306 };
307}
308
309impl_attrvalue!(
313 i8,
314 i16,
315 i32,
316 i64,
317 i128,
318 isize,
319 u8,
320 u16,
321 u32,
322 u64,
323 u128,
324 usize,
325 f32,
326 f64,
327 String,
328 std::net::IpAddr,
329 std::net::Ipv4Addr,
330 std::net::Ipv6Addr,
331);
332
333impl AttrValue for std::time::Duration {
334 fn display(&self) -> String {
335 humantime::format_duration(*self).to_string()
336 }
337
338 fn parse(value: &str) -> Result<Self, anyhow::Error> {
339 Ok(humantime::parse_duration(value)?)
340 }
341}
342
343impl AttrValue for std::time::SystemTime {
344 fn display(&self) -> String {
345 let datetime: DateTime<Utc> = (*self).into();
346 datetime.to_rfc3339()
347 }
348
349 fn parse(value: &str) -> Result<Self, anyhow::Error> {
350 let datetime = DateTime::parse_from_rfc3339(value)?;
351 Ok(datetime.into())
352 }
353}
354
355impl<T, E> AttrValue for std::ops::Range<T>
356where
357 T: Named
358 + Display
359 + FromStr<Err = E>
360 + Send
361 + Sync
362 + Serialize
363 + DeserializeOwned
364 + Clone
365 + 'static,
366 E: Into<anyhow::Error> + Send + Sync + 'static,
367{
368 fn display(&self) -> String {
369 format!("{}..{}", self.start, self.end)
370 }
371
372 fn parse(value: &str) -> Result<Self, anyhow::Error> {
373 let (start, end) = value.split_once("..").ok_or_else(|| {
374 anyhow::anyhow!("expected range in format `start..end`, got `{}`", value)
375 })?;
376 let start = start.parse().map_err(|e: E| e.into())?;
377 let end = end.parse().map_err(|e: E| e.into())?;
378 Ok(start..end)
379 }
380}
381
382impl AttrValue for bool {
383 fn display(&self) -> String {
384 if *self { 1.to_string() } else { 0.to_string() }
385 }
386
387 fn parse(value: &str) -> Result<Self, anyhow::Error> {
388 let value = value.to_ascii_lowercase();
389 match value.as_str() {
390 "0" | "false" => Ok(false),
391 "1" | "true" => Ok(true),
392 _ => Err(anyhow::anyhow!(
393 "expected `0`, `1`, `true` or `false`, got `{}`",
394 value
395 )),
396 }
397 }
398}
399
400#[doc(hidden)]
402pub trait SerializableValue: Send + Sync {
403 fn as_any(&self) -> &dyn Any;
405 fn as_any_mut(&mut self) -> &mut dyn Any;
407 fn as_erased_serialize(&self) -> &dyn ErasedSerialize;
409 fn cloned(&self) -> Box<dyn SerializableValue>;
411 fn display(&self) -> String;
413}
414
415impl<T: AttrValue> SerializableValue for T {
416 fn as_any(&self) -> &dyn Any {
417 self
418 }
419
420 fn as_any_mut(&mut self) -> &mut dyn Any {
421 self
422 }
423
424 fn as_erased_serialize(&self) -> &dyn ErasedSerialize {
425 self
426 }
427
428 fn cloned(&self) -> Box<dyn SerializableValue> {
429 Box::new(self.clone())
430 }
431
432 fn display(&self) -> String {
433 self.display()
434 }
435}
436
437pub struct Attrs {
455 values: HashMap<&'static str, Box<dyn SerializableValue>>,
456}
457
458impl Attrs {
459 pub fn new() -> Self {
461 Self {
462 values: HashMap::new(),
463 }
464 }
465
466 pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
468 self.values.insert(key.name, Box::new(value));
469 }
470
471 fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
472 if self.contains_key(key) {
473 return;
474 }
475 let Some(default) = key.default() else { return };
476 self.set(key, default.clone());
477 }
478
479 pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
482 self.values
483 .get(key.name)
484 .and_then(|value| value.as_any().downcast_ref::<T>())
485 .or_else(|| key.default())
486 }
487
488 pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
491 self.maybe_set_from_default(key);
492 self.values
493 .get_mut(key.name)
494 .and_then(|value| value.as_any_mut().downcast_mut::<T>())
495 }
496
497 pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
499 self.values.remove(key.name).is_some()
501 }
502
503 pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
505 self.values.contains_key(key.name)
506 }
507
508 pub fn len(&self) -> usize {
510 self.values.len()
511 }
512
513 pub fn is_empty(&self) -> bool {
515 self.values.is_empty()
516 }
517
518 pub fn clear(&mut self) {
520 self.values.clear();
521 }
522
523 pub fn remove_value<T: 'static>(&mut self, key: Key<T>) -> Option<Box<dyn SerializableValue>> {
526 self.values.remove(key.name)
527 }
528
529 pub fn insert_value<T: 'static>(&mut self, key: Key<T>, value: Box<dyn SerializableValue>) {
531 self.values.insert(key.name, value);
532 }
533
534 pub fn insert_value_by_name_unchecked(
536 &mut self,
537 name: &'static str,
538 value: Box<dyn SerializableValue>,
539 ) {
540 self.values.insert(name, value);
541 }
542
543 pub fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
546 self.values.get(name).map(|b| b.as_ref())
547 }
548
549 pub fn merge(&mut self, other: Attrs) {
555 self.values.extend(other.values);
556 }
557}
558
559impl Clone for Attrs {
560 fn clone(&self) -> Self {
561 let mut values = HashMap::new();
562 for (key, value) in &self.values {
563 values.insert(*key, value.cloned());
564 }
565 Self { values }
566 }
567}
568
569impl std::fmt::Display for Attrs {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 let mut first = true;
572 for (key, value) in &self.values {
573 if first {
574 first = false;
575 } else {
576 write!(f, ",")?;
577 }
578 write!(f, "{}={}", key, value.display())?
579 }
580 Ok(())
581 }
582}
583
584impl std::fmt::Debug for Attrs {
585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 let mut debug_map = std::collections::BTreeMap::new();
588 for (key, value) in &self.values {
589 match serde_json::to_string(value.as_erased_serialize()) {
590 Ok(json) => {
591 debug_map.insert(*key, json);
592 }
593 Err(_) => {
594 debug_map.insert(*key, "<serialization error>".to_string());
595 }
596 }
597 }
598
599 f.debug_struct("Attrs").field("values", &debug_map).finish()
600 }
601}
602
603impl Default for Attrs {
604 fn default() -> Self {
605 Self::new()
606 }
607}
608
609impl Serialize for Attrs {
610 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
611 where
612 S: Serializer,
613 {
614 let mut map = serializer.serialize_map(Some(self.values.len()))?;
615
616 for (key_name, value) in &self.values {
617 map.serialize_entry(key_name, value.as_erased_serialize())?;
618 }
619
620 map.end()
621 }
622}
623
624struct AttrsVisitor;
625
626impl<'de> Visitor<'de> for AttrsVisitor {
627 type Value = Attrs;
628
629 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
630 formatter.write_str("a map of attribute keys to their serialized values")
631 }
632
633 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
634 where
635 M: MapAccess<'de>,
636 {
637 static KEYS_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static AttrKeyInfo>> =
638 std::sync::LazyLock::new(|| {
639 inventory::iter::<AttrKeyInfo>()
640 .map(|info| (info.name, info))
641 .collect()
642 });
643 let keys_by_name = &*KEYS_BY_NAME;
644
645 let exe_name = std::env::current_exe()
646 .ok()
647 .map(|p| p.display().to_string())
648 .unwrap_or_else(|| "<unknown-exe>".to_string());
649
650 let mut attrs = Attrs::new();
651 while let Some(key_name) = access.next_key::<String>()? {
652 let Some(&key) = keys_by_name.get(key_name.as_str()) else {
653 access.next_value::<serde::de::IgnoredAny>().map_err(|_| {
673 serde::de::Error::custom(format!(
674 "unknown attr key '{}' on binary '{}'; \
675 this binary doesn't know this key and cannot skip its value safely under bincode",
676 key_name, exe_name,
677 ))
678 })?;
679 continue;
680 };
681
682 let seed = ValueDeserializeSeed {
684 deserialize_erased: key.deserialize_erased,
685 };
686 match access.next_value_seed(seed) {
687 Ok(value) => {
688 attrs.values.insert(key.name, value);
689 }
690 Err(err) => {
691 return Err(serde::de::Error::custom(format!(
692 "failed to deserialize value for key {}: {}",
693 key_name, err
694 )));
695 }
696 }
697 }
698
699 Ok(attrs)
700 }
701}
702
703struct ValueDeserializeSeed {
705 deserialize_erased:
706 fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
707}
708
709impl<'de> serde::de::DeserializeSeed<'de> for ValueDeserializeSeed {
710 type Value = Box<dyn SerializableValue>;
711
712 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
713 where
714 D: serde::de::Deserializer<'de>,
715 {
716 let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
717 (self.deserialize_erased)(&mut erased).map_err(serde::de::Error::custom)
718 }
719}
720
721impl<'de> Deserialize<'de> for Attrs {
722 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
723 where
724 D: Deserializer<'de>,
725 {
726 deserializer.deserialize_map(AttrsVisitor)
727 }
728}
729
730#[doc(hidden)]
733pub const fn ascii_to_lowercase_const<const N: usize>(input: &str) -> [u8; N] {
734 let bytes = input.as_bytes();
735 let mut result = [0u8; N];
736 let mut i = 0;
737
738 while i < bytes.len() && i < N {
739 let byte = bytes[i];
740 if byte >= b'A' && byte <= b'Z' {
741 result[i] = byte + 32; } else {
743 result[i] = byte;
744 }
745 i += 1;
746 }
747
748 result
749}
750
751#[doc(hidden)]
753#[macro_export]
754macro_rules! const_ascii_lowercase {
755 ($s:expr) => {{
756 const INPUT: &str = $s;
757 const LEN: usize = INPUT.len();
758 const BYTES: [u8; LEN] = $crate::attrs::ascii_to_lowercase_const::<LEN>(INPUT);
759 unsafe { std::str::from_utf8_unchecked(&BYTES) }
761 }};
762}
763
764#[doc(hidden)]
767#[macro_export]
768macro_rules! assert_impl {
769 ($ty:ty, $trait:path) => {
770 const _: fn() = || {
771 fn check<T: $trait>() {}
772 check::<$ty>();
773 };
774 };
775}
776
777#[macro_export]
820macro_rules! declare_attrs {
821 ($(
823 $(#[$attr:meta])*
824 $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))*
825 $vis:vis attr $name:ident: $type:ty $(= $default:expr)?;
826 )*) => {
827 $(
828 $crate::declare_attrs! {
829 @single
830 $(@meta($($meta_key = $meta_value),*))*
831 $(#[$attr])* ;
832 $vis attr $name: $type $(= $default)?;
833 }
834 )*
835 };
836
837 (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
839 $crate::assert_impl!($type, $crate::attrs::AttrValue);
840
841 $crate::paste! {
843 static [<$name _DEFAULT>]: $type = $default;
844 static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
845 std::sync::LazyLock::new(|| {
846 #[allow(unused_mut)]
847 let mut attrs = $crate::attrs::Attrs::new();
848 $($(
849 attrs.set($meta_key, $meta_value);
850 )*)*
851 attrs
852 });
853 }
854
855 $(#[$attr])*
856 $vis static $name: $crate::attrs::Key<$type> = {
857 $crate::assert_impl!($type, $crate::attrs::AttrValue);
858
859 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
860 const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
861 $crate::paste! {
862 $crate::attrs::Key::new(
863 LOWER_NAME,
864 Some(&[<$name _DEFAULT>]),
865 $crate::paste! { &[<$name _META_ATTRS>] },
866 )
867 }
868 };
869
870 $crate::submit! {
872 $crate::attrs::AttrKeyInfo {
873 name: {
874 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
875 $crate::const_ascii_lowercase!(FULL_NAME)
876 },
877 typehash: <$type as $crate::typeuri::Named>::typehash,
878 deserialize_erased: |deserializer| {
879 let value: $type = erased_serde::deserialize(deserializer)?;
880 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
881 },
882 meta: $crate::paste! { &[<$name _META_ATTRS>] },
883 display: |value: &dyn $crate::attrs::SerializableValue| {
884 let value = value.as_any().downcast_ref::<$type>().unwrap();
885 $crate::attrs::AttrValue::display(value)
886 },
887 parse: |value: &str| {
888 let value: $type = $crate::attrs::AttrValue::parse(value)?;
889 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
890 },
891 default: Some($crate::paste! { &[<$name _DEFAULT>] }),
892 erased: &$name,
893 }
894 }
895 };
896
897 (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
899 $crate::assert_impl!($type, $crate::attrs::AttrValue);
900
901 $crate::paste! {
902 static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
903 std::sync::LazyLock::new(|| {
904 #[allow(unused_mut)]
905 let mut attrs = $crate::attrs::Attrs::new();
906 $($(
907 attrs.set($meta_key, $meta_value);
910 )*)*
911 attrs
912 });
913 }
914
915 $(#[$attr])*
916 $vis static $name: $crate::attrs::Key<$type> = {
917 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
918 const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
919 $crate::attrs::Key::new(LOWER_NAME, None, $crate::paste! { &[<$name _META_ATTRS>] })
920 };
921
922
923 $crate::submit! {
925 $crate::attrs::AttrKeyInfo {
926 name: {
927 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
928 $crate::const_ascii_lowercase!(FULL_NAME)
929 },
930 typehash: <$type as $crate::typeuri::Named>::typehash,
931 deserialize_erased: |deserializer| {
932 let value: $type = erased_serde::deserialize(deserializer)?;
933 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
934 },
935 meta: $crate::paste! { &[<$name _META_ATTRS>] },
936 display: |value: &dyn $crate::attrs::SerializableValue| {
937 let value = value.as_any().downcast_ref::<$type>().unwrap();
938 $crate::attrs::AttrValue::display(value)
939 },
940 parse: |value: &str| {
941 let value: $type = $crate::attrs::AttrValue::parse(value)?;
942 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
943 },
944 default: None,
945 erased: &$name,
946 }
947 }
948 };
949}
950
951pub use declare_attrs;
952
953#[cfg(test)]
954mod tests {
955 use std::time::Duration;
956
957 use super::*;
958
959 declare_attrs! {
960 attr TEST_TIMEOUT: Duration;
961 attr TEST_COUNT: u32;
962 @meta(TEST_COUNT = 42)
963 pub attr TEST_NAME: String;
964 }
965
966 #[test]
967 fn test_basic_operations() {
968 let mut attrs = Attrs::new();
969
970 attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
972 attrs.set(TEST_COUNT, 42u32);
973 attrs.set(TEST_NAME, "test".to_string());
974
975 assert_eq!(attrs.get(TEST_TIMEOUT), Some(&Duration::from_secs(5)));
976 assert_eq!(attrs.get(TEST_COUNT), Some(&42u32));
977 assert_eq!(attrs.get(TEST_NAME), Some(&"test".to_string()));
978
979 assert!(attrs.contains_key(TEST_TIMEOUT));
981 assert!(attrs.contains_key(TEST_COUNT));
982 assert!(attrs.contains_key(TEST_NAME));
983
984 assert_eq!(attrs.len(), 3);
986 assert!(!attrs.is_empty());
987
988 assert_eq!(TEST_NAME.attrs().get(TEST_COUNT).unwrap(), &42u32);
990 }
991
992 #[test]
993 fn test_get_mut() {
994 let mut attrs = Attrs::new();
995 attrs.set(TEST_COUNT, 10u32);
996
997 if let Some(count) = attrs.get_mut(TEST_COUNT) {
998 *count += 5;
999 }
1000
1001 assert_eq!(attrs.get(TEST_COUNT), Some(&15u32));
1002 }
1003
1004 #[test]
1005 fn test_remove() {
1006 let mut attrs = Attrs::new();
1007 attrs.set(TEST_COUNT, 42u32);
1008
1009 let removed = attrs.remove(TEST_COUNT);
1010 assert!(removed);
1011 assert_eq!(attrs.get(TEST_COUNT), None);
1012 assert!(!attrs.contains_key(TEST_COUNT));
1013 }
1014
1015 #[test]
1016 fn test_clear() {
1017 let mut attrs = Attrs::new();
1018 attrs.set(TEST_TIMEOUT, Duration::from_secs(1));
1019 attrs.set(TEST_COUNT, 42u32);
1020
1021 attrs.clear();
1022 assert!(attrs.is_empty());
1023 assert_eq!(attrs.len(), 0);
1024 }
1025
1026 #[test]
1027 fn test_key_properties() {
1028 assert_eq!(
1029 TEST_TIMEOUT.name(),
1030 "hyperactor_config::attrs::tests::test_timeout"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_serialization() {
1036 let mut attrs = Attrs::new();
1037 attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1038 attrs.set(TEST_COUNT, 42u32);
1039 attrs.set(TEST_NAME, "test".to_string());
1040
1041 let serialized = serde_json::to_string(&attrs).expect("Failed to serialize");
1043
1044 assert!(serialized.contains("hyperactor_config::attrs::tests::test_timeout"));
1046 assert!(serialized.contains("hyperactor_config::attrs::tests::test_count"));
1047 assert!(serialized.contains("hyperactor_config::attrs::tests::test_name"));
1048 }
1049
1050 #[test]
1051 fn test_deserialization() {
1052 let mut original_attrs = Attrs::new();
1054 original_attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1055 original_attrs.set(TEST_COUNT, 42u32);
1056 original_attrs.set(TEST_NAME, "test".to_string());
1057
1058 let serialized = serde_json::to_string(&original_attrs).expect("Failed to serialize");
1060
1061 let deserialized_attrs: Attrs =
1063 serde_json::from_str(&serialized).expect("Failed to deserialize");
1064
1065 assert_eq!(
1067 deserialized_attrs.get(TEST_TIMEOUT),
1068 Some(&Duration::from_secs(5))
1069 );
1070 assert_eq!(deserialized_attrs.get(TEST_COUNT), Some(&42u32));
1071 assert_eq!(deserialized_attrs.get(TEST_NAME), Some(&"test".to_string()));
1072 }
1073
1074 #[test]
1075 fn test_roundtrip_serialization() {
1076 let mut original = Attrs::new();
1078 original.set(TEST_TIMEOUT, Duration::from_secs(10));
1079 original.set(TEST_COUNT, 5u32);
1080 original.set(TEST_NAME, "test-service".to_string());
1081
1082 let serialized = serde_json::to_string(&original).unwrap();
1084
1085 let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1087
1088 assert_eq!(
1090 deserialized.get(TEST_TIMEOUT),
1091 Some(&Duration::from_secs(10))
1092 );
1093 assert_eq!(deserialized.get(TEST_COUNT), Some(&5u32));
1094 assert_eq!(
1095 deserialized.get(TEST_NAME),
1096 Some(&"test-service".to_string())
1097 );
1098 }
1099
1100 #[test]
1101 fn test_empty_attrs_serialization() {
1102 let attrs = Attrs::new();
1103 let serialized = serde_json::to_string(&attrs).unwrap();
1104
1105 assert_eq!(serialized, "{}");
1107
1108 let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1109
1110 assert!(deserialized.is_empty());
1111 }
1112
1113 #[test]
1114 fn test_format_independence() {
1115 let mut attrs = Attrs::new();
1117 attrs.set(TEST_COUNT, 42u32);
1118 attrs.set(TEST_NAME, "test".to_string());
1119
1120 let json_output = serde_json::to_string(&attrs).unwrap();
1122 let yaml_output = serde_yaml::to_string(&attrs).unwrap();
1123
1124 assert!(json_output.contains(":"));
1126 assert!(json_output.contains("\""));
1127
1128 assert!(json_output.contains("42"));
1130 assert!(!json_output.contains("\"42\""));
1131
1132 assert!(yaml_output.contains(":"));
1134 assert!(yaml_output.contains("42"));
1135
1136 assert!(!yaml_output.contains("\"42\""));
1138
1139 assert_ne!(json_output, yaml_output);
1141
1142 let from_json: Attrs = serde_json::from_str(&json_output).unwrap();
1144 let from_yaml: Attrs = serde_yaml::from_str(&yaml_output).unwrap();
1145
1146 assert_eq!(from_json.get(TEST_COUNT), Some(&42u32));
1147 assert_eq!(from_yaml.get(TEST_COUNT), Some(&42u32));
1148 assert_eq!(from_json.get(TEST_NAME), Some(&"test".to_string()));
1149 assert_eq!(from_yaml.get(TEST_NAME), Some(&"test".to_string()));
1150 }
1151
1152 #[test]
1153 fn test_clone() {
1154 let mut original = Attrs::new();
1156 original.set(TEST_COUNT, 42u32);
1157 original.set(TEST_NAME, "test".to_string());
1158 original.set(TEST_TIMEOUT, std::time::Duration::from_secs(10));
1159
1160 let cloned = original.clone();
1162
1163 assert_eq!(cloned.get(TEST_COUNT), Some(&42u32));
1165 assert_eq!(cloned.get(TEST_NAME), Some(&"test".to_string()));
1166 assert_eq!(
1167 cloned.get(TEST_TIMEOUT),
1168 Some(&std::time::Duration::from_secs(10))
1169 );
1170
1171 original.set(TEST_COUNT, 100u32);
1173 assert_eq!(original.get(TEST_COUNT), Some(&100u32));
1174 assert_eq!(cloned.get(TEST_COUNT), Some(&42u32)); let mut cloned_mut = cloned.clone();
1178 cloned_mut.set(TEST_NAME, "modified".to_string());
1179 assert_eq!(cloned_mut.get(TEST_NAME), Some(&"modified".to_string()));
1180 assert_eq!(original.get(TEST_NAME), Some(&"test".to_string())); }
1182
1183 #[test]
1184 fn test_debug_with_json() {
1185 let mut attrs = Attrs::new();
1186 attrs.set(TEST_COUNT, 42u32);
1187 attrs.set(TEST_NAME, "test".to_string());
1188
1189 let debug_output = format!("{:?}", attrs);
1191
1192 assert!(debug_output.contains("Attrs"));
1194
1195 assert!(debug_output.contains("42"));
1197
1198 assert!(debug_output.contains("hyperactor_config::attrs::tests::test_count"));
1200 assert!(debug_output.contains("hyperactor_config::attrs::tests::test_name"));
1201
1202 assert!(debug_output.contains("test"));
1205 }
1206
1207 declare_attrs! {
1208 attr TIMEOUT_WITH_DEFAULT: Duration = Duration::from_secs(10);
1210
1211 pub(crate) attr CRATE_LOCAL_ATTR: String;
1213 }
1214
1215 #[test]
1216 fn test_defaults() {
1217 assert!(TIMEOUT_WITH_DEFAULT.has_default());
1218 assert!(!CRATE_LOCAL_ATTR.has_default());
1219
1220 assert_eq!(
1221 Attrs::new().get(TIMEOUT_WITH_DEFAULT),
1222 Some(&Duration::from_secs(10))
1223 );
1224 }
1225
1226 #[test]
1227 fn test_indexing() {
1228 let mut attrs = Attrs::new();
1229
1230 assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(10));
1231 attrs[TIMEOUT_WITH_DEFAULT] = Duration::from_secs(100);
1232 assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(100));
1233
1234 attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
1235 assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
1236 }
1237
1238 #[test]
1239 fn attrs_deserialize_unknown_key_is_error() {
1240 let bad_key: &'static str = "monarch_hyperactor::pytokio::unawaited_pytokio_traceback";
1265
1266 let mut attrs = Attrs::new();
1270 attrs.insert_value_by_name_unchecked(bad_key, Box::new(0u32));
1271
1272 let wire_bytes = bincode::serialize(&attrs).unwrap();
1275
1276 let err = bincode::deserialize::<Attrs>(&wire_bytes)
1279 .expect_err("should error on unknown attr key");
1280
1281 let exe_str = std::env::current_exe()
1282 .ok()
1283 .map(|p| p.display().to_string())
1284 .unwrap_or_else(|| "<unknown-exe>".to_string());
1285 let msg = format!("{err}");
1286
1287 assert!(msg.contains("unknown attr key"), "got: {msg}");
1288 assert!(msg.contains(bad_key), "got: {msg}");
1289 assert!(msg.contains(&exe_str), "got: {msg}");
1290 }
1291}