1use std::collections::BTreeMap;
4use std::fmt::{self, Debug, Display};
5use std::ops::Range;
6use std::result::Result as StdResult;
7use std::str::FromStr;
8
9use derive_more::From;
10use safelog::DisplayRedacted as _;
11use thiserror::Error;
12use tor_error::{internal, into_internal, Bug};
13use tor_hscrypto::pk::{HsId, HsIdParseError, HSID_ONION_SUFFIX};
14use tor_hscrypto::time::TimePeriod;
15use tor_persist::hsnickname::HsNickname;
16use tor_persist::slug::Slug;
17
18use crate::{ArtiPath, ArtiPathSyntaxError};
19
20#[macro_use]
22pub mod derive;
23
24#[derive(Clone, Debug, PartialEq, Eq, Hash, From, derive_more::Display)]
26#[non_exhaustive]
27pub enum KeyPath {
28 Arti(ArtiPath),
30 CTor(CTorPath),
32}
33
34#[derive(Clone, Debug, PartialEq, Eq, Hash, From)]
36pub struct ArtiPathRange(pub(crate) Range<usize>);
37
38impl ArtiPath {
39 pub fn matches(&self, pat: &KeyPathPattern) -> Option<Vec<ArtiPathRange>> {
60 use KeyPathPattern::*;
61
62 let pattern: &str = match pat {
63 Arti(pat) => pat.as_ref(),
64 _ => return None,
65 };
66
67 glob_match::glob_match_with_captures(pattern, self.as_ref())
68 .map(|res| res.into_iter().map(|r| r.into()).collect())
69 }
70}
71
72impl KeyPath {
73 pub fn matches(&self, pat: &KeyPathPattern) -> bool {
90 use KeyPathPattern::*;
91
92 match (self, pat) {
93 (KeyPath::Arti(p), Arti(_)) => p.matches(pat).is_some(),
94 (KeyPath::CTor(p), CTor(pat)) if p == pat => true,
95 _ => false,
96 }
97 }
98
99 pub fn arti(&self) -> Option<&ArtiPath> {
103 match self {
104 KeyPath::Arti(ref arti) => Some(arti),
105 KeyPath::CTor(_) => None,
106 }
107 }
108
109 pub fn ctor(&self) -> Option<&CTorPath> {
111 match self {
112 KeyPath::Arti(_) => None,
113 KeyPath::CTor(ref ctor) => Some(ctor),
114 }
115 }
116}
117
118pub trait KeySpecifierPattern {
125 fn new_any() -> Self
127 where
128 Self: Sized;
129
130 fn arti_pattern(&self) -> Result<KeyPathPattern, Bug>;
133}
134
135#[derive(Debug, Clone, thiserror::Error)]
152#[non_exhaustive]
153pub enum KeyPathError {
154 #[error("Path does not match expected pattern")]
156 PatternNotMatched(ArtiPath),
157
158 #[error("Unrecognized path: {0}")]
163 Unrecognized(KeyPath),
164
165 #[error("ArtiPath {path} is invalid")]
167 InvalidArtiPath {
168 #[source]
170 error: ArtiPathSyntaxError,
171 path: ArtiPath,
173 },
174
175 #[error("invalid string value for element of key path")]
183 InvalidKeyPathComponentValue {
184 #[source]
186 error: InvalidKeyPathComponentValue,
187 key: String,
191 path: ArtiPath,
193 value: Slug,
195 },
196
197 #[error("Internal error")]
199 Bug(#[from] tor_error::Bug),
200}
201
202#[derive(Error, Clone, Debug)]
208#[non_exhaustive]
209pub enum InvalidKeyPathComponentValue {
210 #[error("{0}")]
219 Slug(String),
220
221 #[error("Internal error")]
226 Bug(#[from] tor_error::Bug),
227}
228
229#[derive(Debug, Clone, PartialEq, derive_builder::Builder, amplify::Getters)]
236pub struct KeyPathInfo {
237 summary: String,
241 role: String,
247 #[builder(default, setter(custom))]
254 extra_info: BTreeMap<String, String>,
255}
256
257impl KeyPathInfo {
258 pub fn builder() -> KeyPathInfoBuilder {
260 KeyPathInfoBuilder::default()
261 }
262}
263
264impl KeyPathInfoBuilder {
265 pub fn set_all_extra_info(
269 &mut self,
270 all_extra_info: impl Iterator<Item = (String, String)>,
271 ) -> &mut Self {
272 self.extra_info = Some(all_extra_info.collect());
273 self
274 }
275
276 pub fn extra_info(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
280 let extra_info = self.extra_info.get_or_insert(Default::default());
281 extra_info.insert(key.into(), value.into());
282 self
283 }
284}
285
286pub trait KeyPathInfoExtractor: Send + Sync {
291 fn describe(&self, path: &KeyPath) -> StdResult<KeyPathInfo, KeyPathError>;
293}
294
295#[macro_export]
297macro_rules! register_key_info_extractor {
298 ($kv:expr) => {{
299 $crate::inventory::submit!(&$kv as &dyn $crate::KeyPathInfoExtractor);
300 }};
301}
302
303#[derive(Clone, Debug, PartialEq, Eq, Hash)]
323#[non_exhaustive]
324pub enum KeyPathPattern {
325 Arti(String),
327 CTor(CTorPath),
329}
330
331#[derive(Clone, Debug, PartialEq, Eq, Hash, derive_more::Display)] #[non_exhaustive]
334pub enum CTorPath {
335 #[display("ClientHsDescEncKey({})", _0.display_unredacted())]
344 ClientHsDescEncKey(HsId),
345 #[display("{path}")]
347 Service {
348 nickname: HsNickname,
350 path: CTorServicePath,
352 },
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Hash, derive_more::Display)] #[non_exhaustive]
358pub enum CTorServicePath {
359 #[display("hs_ed25519_public_key")]
361 PublicKey,
362 #[display("hs_ed25519_secret_key")]
364 PrivateKey,
365}
366
367impl CTorPath {
368 pub fn service(nickname: HsNickname, path: CTorServicePath) -> Self {
370 Self::Service { nickname, path }
371 }
372
373 pub fn client(hsid: HsId) -> Self {
375 Self::ClientHsDescEncKey(hsid)
376 }
377}
378
379pub trait KeySpecifier {
383 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError>;
387
388 fn ctor_path(&self) -> Option<CTorPath>;
393
394 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>>;
397}
398
399pub trait KeySpecifierComponent {
412 fn to_slug(&self) -> Result<Slug, Bug>;
414 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
416 where
417 Self: Sized;
418 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result;
422}
423
424#[derive(Error, Debug, Clone)]
429#[non_exhaustive]
430pub enum ArtiPathUnavailableError {
431 #[error("Internal error")]
433 Bug(#[from] tor_error::Bug),
434
435 #[error("ArtiPath unavailable")]
440 ArtiPathUnavailable,
441}
442
443impl KeySpecifier for ArtiPath {
444 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
445 Ok(self.clone())
446 }
447
448 fn ctor_path(&self) -> Option<CTorPath> {
449 None
450 }
451
452 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
453 None
454 }
455}
456
457impl KeySpecifier for CTorPath {
458 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
459 Err(ArtiPathUnavailableError::ArtiPathUnavailable)
460 }
461
462 fn ctor_path(&self) -> Option<CTorPath> {
463 Some(self.clone())
464 }
465
466 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
467 None
468 }
469}
470
471impl KeySpecifier for KeyPath {
472 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
473 match self {
474 KeyPath::Arti(p) => p.arti_path(),
475 KeyPath::CTor(p) => p.arti_path(),
476 }
477 }
478
479 fn ctor_path(&self) -> Option<CTorPath> {
480 match self {
481 KeyPath::Arti(p) => p.ctor_path(),
482 KeyPath::CTor(p) => p.ctor_path(),
483 }
484 }
485
486 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
487 None
488 }
489}
490
491impl KeySpecifierComponent for TimePeriod {
492 fn to_slug(&self) -> Result<Slug, Bug> {
493 Slug::new(format!(
494 "{}_{}_{}",
495 self.interval_num(),
496 self.length(),
497 self.epoch_offset_in_sec()
498 ))
499 .map_err(into_internal!("TP formatting went wrong"))
500 }
501
502 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
503 where
504 Self: Sized,
505 {
506 use itertools::Itertools;
507
508 let s = s.to_string();
509 #[allow(clippy::redundant_closure)] let err_ctx = |e: &str| InvalidKeyPathComponentValue::Slug(e.to_string());
511 let (interval, len, offset) = s
512 .split('_')
513 .collect_tuple()
514 .ok_or_else(|| err_ctx("invalid number of subcomponents"))?;
515
516 let length = len.parse().map_err(|_| err_ctx("invalid length"))?;
517 let interval_num = interval
518 .parse()
519 .map_err(|_| err_ctx("invalid interval_num"))?;
520 let offset_in_sec = offset
521 .parse()
522 .map_err(|_| err_ctx("invalid offset_in_sec"))?;
523
524 Ok(TimePeriod::from_parts(length, interval_num, offset_in_sec))
525 }
526
527 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
528 Display::fmt(&self, f)
529 }
530}
531
532pub trait KeySpecifierComponentViaDisplayFromStr: Display + FromStr {}
540impl<T: KeySpecifierComponentViaDisplayFromStr> KeySpecifierComponent for T {
541 fn to_slug(&self) -> Result<Slug, Bug> {
542 self.to_string()
543 .try_into()
544 .map_err(into_internal!("Display generated bad Slug"))
545 }
546 fn from_slug(s: &Slug) -> Result<Self, InvalidKeyPathComponentValue>
547 where
548 Self: Sized,
549 {
550 s.as_str()
551 .parse()
552 .map_err(|_| internal!("slug cannot be parsed as component").into())
553 }
554 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
555 Display::fmt(self, f)
556 }
557}
558
559impl KeySpecifierComponentViaDisplayFromStr for HsNickname {}
560
561impl KeySpecifierComponent for HsId {
562 fn to_slug(&self) -> StdResult<Slug, Bug> {
563 let hsid = self.display_unredacted().to_string();
567 let hsid_slug = hsid
568 .strip_suffix(HSID_ONION_SUFFIX)
569 .ok_or_else(|| internal!("HsId Display impl missing .onion suffix?!"))?;
570 hsid_slug
571 .to_owned()
572 .try_into()
573 .map_err(into_internal!("Display generated bad Slug"))
574 }
575
576 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
577 where
578 Self: Sized,
579 {
580 let onion = format!("{}{HSID_ONION_SUFFIX}", s.as_str());
589
590 onion
591 .parse()
592 .map_err(|e: HsIdParseError| InvalidKeyPathComponentValue::Slug(e.to_string()))
593 }
594
595 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
596 Display::fmt(&self.display_redacted(), f)
597 }
598}
599
600struct KeySpecifierComponentPrettyHelper<'c>(&'c dyn KeySpecifierComponent);
602
603impl Display for KeySpecifierComponentPrettyHelper<'_> {
604 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
605 KeySpecifierComponent::fmt_pretty(self.0, f)
606 }
607}
608
609pub trait KeyCertificateSpecifier {
615 fn cert_denotators(&self) -> Vec<&dyn KeySpecifierComponent>;
624 fn signing_key_specifier(&self) -> Option<&dyn KeySpecifier>;
632 fn subject_key_specifier(&self) -> &dyn KeySpecifier;
634}
635
636#[cfg(test)]
637mod test {
638 #![allow(clippy::bool_assert_comparison)]
640 #![allow(clippy::clone_on_copy)]
641 #![allow(clippy::dbg_macro)]
642 #![allow(clippy::mixed_attributes_style)]
643 #![allow(clippy::print_stderr)]
644 #![allow(clippy::print_stdout)]
645 #![allow(clippy::single_char_pattern)]
646 #![allow(clippy::unwrap_used)]
647 #![allow(clippy::unchecked_duration_subtraction)]
648 #![allow(clippy::useless_vec)]
649 #![allow(clippy::needless_pass_by_value)]
650 use super::*;
652
653 use crate::test_utils::check_key_specifier;
654 use derive_deftly::Deftly;
655 use humantime::parse_rfc3339;
656 use itertools::Itertools;
657 use serde::{Deserialize, Serialize};
658 use std::fmt::Debug;
659 use std::time::Duration;
660
661 impl KeySpecifierComponentViaDisplayFromStr for usize {}
662 impl KeySpecifierComponentViaDisplayFromStr for String {}
663
664 impl KeySpecifierComponentViaDisplayFromStr for bool {}
667
668 fn test_time_period() -> TimePeriod {
669 TimePeriod::new(
670 Duration::from_secs(86400),
671 parse_rfc3339("2020-09-15T00:00:00Z").unwrap(),
672 Duration::from_secs(3600),
673 )
674 .unwrap()
675 }
676
677 #[test]
678 fn pretty_time_period() {
679 let tp = test_time_period();
680 assert_eq!(
681 KeySpecifierComponentPrettyHelper(&tp).to_string(),
682 "#18519 2020-09-14T01:00:00Z..+24:00",
683 );
684 }
685
686 #[test]
687 fn serde() {
688 #[derive(Serialize, Deserialize, Debug)]
692 struct T {
693 n: Slug,
694 }
695 let j = serde_json::from_str(r#"{ "n": "x" }"#).unwrap();
696 let t: T = serde_json::from_value(j).unwrap();
697 assert_eq!(&t.n.to_string(), "x");
698
699 assert_eq!(&serde_json::to_string(&t).unwrap(), r#"{"n":"x"}"#);
700
701 let j = serde_json::from_str(r#"{ "n": "!" }"#).unwrap();
702 let e = serde_json::from_value::<T>(j).unwrap_err();
703 assert!(
704 e.to_string()
705 .contains("character '!' (U+0021) is not allowed"),
706 "wrong msg {e:?}"
707 );
708 }
709
710 #[test]
711 fn define_key_specifier_with_fields_and_denotator() {
712 let tp = test_time_period();
713
714 #[derive(Deftly, Debug, PartialEq)]
715 #[derive_deftly(KeySpecifier)]
716 #[deftly(prefix = "encabulator")]
717 #[deftly(role = "marzlevane")]
718 #[deftly(summary = "test key")]
719 struct TestSpecifier {
720 kind: String,
722 base: String,
723 casing: String,
724 #[deftly(denotator)]
725 count: usize,
726 #[deftly(denotator)]
727 tp: TimePeriod,
728 }
729
730 let key_spec = TestSpecifier {
731 kind: "hydrocoptic".into(),
732 base: "waneshaft".into(),
733 casing: "logarithmic".into(),
734 count: 6,
735 tp,
736 };
737
738 check_key_specifier(
739 &key_spec,
740 "encabulator/hydrocoptic/waneshaft/logarithmic/marzlevane+6+18519_1440_3600",
741 );
742
743 let info = TestSpecifierInfoExtractor
744 .describe(&KeyPath::Arti(key_spec.arti_path().unwrap()))
745 .unwrap();
746
747 assert_eq!(
748 format!("{info:#?}"),
749 r##"
750KeyPathInfo {
751 summary: "test key",
752 role: "marzlevane",
753 extra_info: {
754 "base": "waneshaft",
755 "casing": "logarithmic",
756 "count": "6",
757 "kind": "hydrocoptic",
758 "tp": "#18519 2020-09-14T01:00:00Z..+24:00",
759 },
760}
761 "##
762 .trim()
763 );
764 }
765
766 #[test]
767 fn define_key_specifier_no_fields() {
768 #[derive(Deftly, Debug, PartialEq)]
769 #[derive_deftly(KeySpecifier)]
770 #[deftly(prefix = "encabulator")]
771 #[deftly(role = "marzlevane")]
772 #[deftly(summary = "test key")]
773 struct TestSpecifier {}
774
775 let key_spec = TestSpecifier {};
776
777 check_key_specifier(&key_spec, "encabulator/marzlevane");
778
779 assert_eq!(
780 TestSpecifierPattern {}.arti_pattern().unwrap(),
781 KeyPathPattern::Arti("encabulator/marzlevane".into())
782 );
783 }
784
785 #[test]
786 fn define_key_specifier_with_denotator() {
787 #[derive(Deftly, Debug, PartialEq)]
788 #[derive_deftly(KeySpecifier)]
789 #[deftly(prefix = "encabulator")]
790 #[deftly(role = "marzlevane")]
791 #[deftly(summary = "test key")]
792 struct TestSpecifier {
793 #[deftly(denotator)]
794 count: usize,
795 }
796
797 let key_spec = TestSpecifier { count: 6 };
798
799 check_key_specifier(&key_spec, "encabulator/marzlevane+6");
800
801 assert_eq!(
802 TestSpecifierPattern { count: None }.arti_pattern().unwrap(),
803 KeyPathPattern::Arti("encabulator/marzlevane+*".into())
804 );
805 }
806
807 #[test]
808 fn define_key_specifier_with_fields() {
809 #[derive(Deftly, Debug, PartialEq)]
810 #[derive_deftly(KeySpecifier)]
811 #[deftly(prefix = "encabulator")]
812 #[deftly(role = "fan")]
813 #[deftly(summary = "test key")]
814 struct TestSpecifier {
815 casing: String,
816 bearings: String,
818 }
819
820 let key_spec = TestSpecifier {
821 casing: "logarithmic".into(),
822 bearings: "spurving".into(),
823 };
824
825 check_key_specifier(&key_spec, "encabulator/logarithmic/spurving/fan");
826
827 assert_eq!(
828 TestSpecifierPattern {
829 casing: Some("logarithmic".into()),
830 bearings: Some("prefabulating".into()),
831 }
832 .arti_pattern()
833 .unwrap(),
834 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan".into())
835 );
836
837 assert_eq!(key_spec.ctor_path(), None);
838 }
839
840 #[test]
841 fn define_key_specifier_with_multiple_denotators() {
842 #[derive(Deftly, Debug, PartialEq)]
843 #[derive_deftly(KeySpecifier)]
844 #[deftly(prefix = "encabulator")]
845 #[deftly(role = "fan")]
846 #[deftly(summary = "test key")]
847 struct TestSpecifier {
848 casing: String,
849 bearings: String,
851
852 #[deftly(denotator)]
853 count: usize,
854
855 #[deftly(denotator)]
856 length: usize,
857
858 #[deftly(denotator)]
859 kind: String,
860 }
861
862 let key_spec = TestSpecifier {
863 casing: "logarithmic".into(),
864 bearings: "spurving".into(),
865 count: 8,
866 length: 2000,
867 kind: "lunar".into(),
868 };
869
870 check_key_specifier(
871 &key_spec,
872 "encabulator/logarithmic/spurving/fan+8+2000+lunar",
873 );
874
875 assert_eq!(
876 TestSpecifierPattern {
877 casing: Some("logarithmic".into()),
878 bearings: Some("prefabulating".into()),
879 ..TestSpecifierPattern::new_any()
880 }
881 .arti_pattern()
882 .unwrap(),
883 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan+*+*+*".into())
884 );
885 }
886
887 #[test]
888 fn define_key_specifier_role_field() {
889 #[derive(Deftly, Debug, Eq, PartialEq)]
890 #[derive_deftly(KeySpecifier)]
891 #[deftly(prefix = "prefix")]
892 #[deftly(summary = "test key")]
893 struct TestSpecifier {
894 #[deftly(role)]
895 role: String,
896 i: usize,
897 #[deftly(denotator)]
898 den: bool,
899 }
900
901 check_key_specifier(
902 &TestSpecifier {
903 i: 1,
904 role: "role".to_string(),
905 den: true,
906 },
907 "prefix/1/role+true",
908 );
909 }
910
911 #[test]
912 fn define_key_specifier_ctor_path() {
913 #[derive(Deftly, Debug, Eq, PartialEq)]
914 #[derive_deftly(KeySpecifier)]
915 #[deftly(prefix = "p")]
916 #[deftly(role = "r")]
917 #[deftly(ctor_path = "Self::ctp")]
918 #[deftly(summary = "test key")]
919 struct TestSpecifier {
920 i: usize,
921 }
922
923 impl TestSpecifier {
924 fn ctp(&self) -> CTorPath {
925 CTorPath::Service {
926 nickname: HsNickname::from_str("allium-cepa").unwrap(),
927 path: CTorServicePath::PublicKey,
928 }
929 }
930 }
931
932 let spec = TestSpecifier { i: 42 };
933
934 check_key_specifier(&spec, "p/42/r");
935
936 assert_eq!(
937 spec.ctor_path(),
938 Some(CTorPath::Service {
939 nickname: HsNickname::from_str("allium-cepa").unwrap(),
940 path: CTorServicePath::PublicKey,
941 }),
942 );
943 }
944
945 #[test]
946 fn define_key_specifier_fixed_path_component() {
947 #[derive(Deftly, Debug, Eq, PartialEq)]
948 #[derive_deftly(KeySpecifier)]
949 #[deftly(prefix = "prefix")]
950 #[deftly(role = "role")]
951 #[deftly(summary = "test key")]
952 struct TestSpecifier {
953 x: usize,
954 #[deftly(fixed_path_component = "fixed")]
955 z: bool,
956 }
957
958 check_key_specifier(&TestSpecifier { x: 1, z: true }, "prefix/1/fixed/true/role");
959 }
960
961 #[test]
962 fn encode_time_period() {
963 let period = TimePeriod::from_parts(1, 2, 3);
964 let encoded_period = period.to_slug().unwrap();
965
966 assert_eq!(encoded_period.to_string(), "2_1_3");
967 assert_eq!(period, TimePeriod::from_slug(&encoded_period).unwrap());
968
969 assert!(TimePeriod::from_slug(&Slug::new("invalid_tp".to_string()).unwrap()).is_err());
970 assert!(TimePeriod::from_slug(&Slug::new("2_1_3_4".to_string()).unwrap()).is_err());
971 }
972
973 #[test]
974 fn encode_hsid() {
975 let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
976 let onion = format!("{b32}.onion");
977 let hsid = HsId::from_str(&onion).unwrap();
978 let hsid_slug = hsid.to_slug().unwrap();
979
980 assert_eq!(hsid_slug.to_string(), b32);
981 assert_eq!(hsid, HsId::from_slug(&hsid_slug).unwrap());
982 }
983
984 #[test]
985 fn key_info_builder() {
986 macro_rules! assert_extra_info_eq {
988 ($key_info:expr, [$(($k:expr, $v:expr),)*]) => {{
989 assert_eq!(
990 $key_info.extra_info.into_iter().collect_vec(),
991 vec![
992 $(($k.into(), $v.into()),)*
993 ]
994 );
995 }}
996 }
997 let extra_info = vec![("nickname".into(), "bar".into())];
998
999 let key_info = KeyPathInfo::builder()
1000 .summary("test summary".into())
1001 .role("KS_vote".to_string())
1002 .set_all_extra_info(extra_info.clone().into_iter())
1003 .build()
1004 .unwrap();
1005
1006 assert_eq!(key_info.extra_info.into_iter().collect_vec(), extra_info);
1007
1008 let key_info = KeyPathInfo::builder()
1009 .summary("test summary".into())
1010 .role("KS_vote".to_string())
1011 .set_all_extra_info(extra_info.clone().into_iter())
1012 .extra_info("type", "service")
1013 .extra_info("time period", "100")
1014 .build()
1015 .unwrap();
1016
1017 assert_extra_info_eq!(
1018 key_info,
1019 [
1020 ("nickname", "bar"),
1021 ("time period", "100"),
1022 ("type", "service"),
1023 ]
1024 );
1025
1026 let key_info = KeyPathInfo::builder()
1027 .summary("test summary".into())
1028 .role("KS_vote".to_string())
1029 .extra_info("type", "service")
1030 .extra_info("time period", "100")
1031 .set_all_extra_info(extra_info.clone().into_iter())
1032 .build()
1033 .unwrap();
1034
1035 assert_extra_info_eq!(key_info, [("nickname", "bar"),]);
1036
1037 let key_info = KeyPathInfo::builder()
1038 .summary("test summary".into())
1039 .role("KS_vote".to_string())
1040 .extra_info("type", "service")
1041 .extra_info("time period", "100")
1042 .build()
1043 .unwrap();
1044
1045 assert_extra_info_eq!(key_info, [("time period", "100"), ("type", "service"),]);
1046 }
1047}