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