1use std::collections::{BTreeMap, BTreeSet};
27
28use serde::{Deserialize, Serialize, de};
29
30use crate::name_type;
31use crate::names::NameError;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum Tier {
45 Label,
47 Tag,
49 Port,
51}
52
53#[derive(Debug, thiserror::Error, PartialEq, Eq)]
55pub enum AttributeError {
56 #[error("attribute key '{key}' appears in multiple tiers: {tiers:?}")]
58 DuplicateKey {
59 key: String,
61 tiers: Vec<Tier>,
63 },
64
65 #[error("port '{port}' has an empty facet set; at least one facet is required")]
67 EmptyFacetSet {
68 port: String,
70 },
71
72 #[error("port name '{name}' is used more than once across plugs and sockets")]
74 DuplicatePortName {
75 name: String,
77 },
78}
79
80const ATTRIBUTE_VALUE_MAX_LEN: usize = 256;
87
88fn validate_attribute_value(s: &str) -> Result<(), NameError> {
89 if s.is_empty() {
90 return Err(NameError::Empty);
91 }
92 if s.len() > ATTRIBUTE_VALUE_MAX_LEN {
93 return Err(NameError::TooLong {
94 length: s.len(),
95 max: ATTRIBUTE_VALUE_MAX_LEN,
96 });
97 }
98 for (offset, ch) in s.char_indices() {
99 if ch.is_control() {
100 return Err(NameError::InvalidChar { ch, offset });
101 }
102 }
103 Ok(())
104}
105
106macro_rules! attribute_value_type {
109 ($Name:ident, $kind:literal) => {
110 #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
111 pub struct $Name(String);
112
113 impl $Name {
114 pub fn new(candidate: impl Into<String>) -> std::result::Result<Self, NameError> {
116 let s = candidate.into();
117 validate_attribute_value(&s)?;
118 Ok(Self(s))
119 }
120
121 #[must_use]
123 pub fn as_str(&self) -> &str {
124 &self.0
125 }
126
127 #[must_use]
129 pub fn into_inner(self) -> String {
130 self.0
131 }
132 }
133
134 impl AsRef<str> for $Name {
135 fn as_ref(&self) -> &str {
136 &self.0
137 }
138 }
139
140 impl std::fmt::Display for $Name {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.write_str(&self.0)
143 }
144 }
145
146 impl std::fmt::Debug for $Name {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "{}({:?})", $kind, self.0)
149 }
150 }
151
152 impl Serialize for $Name {
153 fn serialize<S: serde::Serializer>(
154 &self,
155 s: S,
156 ) -> std::result::Result<S::Ok, S::Error> {
157 s.serialize_str(&self.0)
158 }
159 }
160
161 impl<'de> Deserialize<'de> for $Name {
162 fn deserialize<D: serde::Deserializer<'de>>(
163 d: D,
164 ) -> std::result::Result<Self, D::Error> {
165 let s = String::deserialize(d)?;
166 Self::new(s).map_err(de::Error::custom)
167 }
168 }
169 };
170}
171
172attribute_value_type!(LabelValue, "LabelValue");
173attribute_value_type!(TagValue, "TagValue");
174
175name_type! {
180 pub struct LabelKey { kind: "LabelKey" }
183}
184
185name_type! {
186 pub struct TagKey { kind: "TagKey" }
189}
190
191name_type! {
192 pub struct FacetKey { kind: "FacetKey" }
194}
195
196name_type! {
197 pub struct FacetValue { kind: "FacetValue" }
199}
200
201name_type! {
202 pub struct PortName { kind: "PortName" }
206}
207
208pub mod label {
215 use super::LabelKey;
216
217 #[must_use]
219 pub fn name() -> LabelKey {
220 LabelKey::new("name").expect("`name` is a valid label key")
221 }
222
223 #[must_use]
225 pub fn r#type() -> LabelKey {
226 LabelKey::new("type").expect("`type` is a valid label key")
227 }
228
229 #[must_use]
231 pub fn description() -> LabelKey {
232 LabelKey::new("description").expect("`description` is a valid label key")
233 }
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(transparent)]
243pub struct Labels(BTreeMap<LabelKey, LabelValue>);
244
245impl Labels {
246 #[must_use]
248 pub fn new() -> Self {
249 Self::default()
250 }
251
252 pub fn insert(&mut self, key: LabelKey, value: LabelValue) -> Option<LabelValue> {
254 self.0.insert(key, value)
255 }
256
257 #[must_use]
259 pub fn get(&self, key: &LabelKey) -> Option<&LabelValue> {
260 self.0.get(key)
261 }
262
263 pub fn iter(&self) -> impl Iterator<Item = (&LabelKey, &LabelValue)> {
265 self.0.iter()
266 }
267
268 pub fn keys(&self) -> impl Iterator<Item = &LabelKey> {
270 self.0.keys()
271 }
272
273 #[must_use]
275 pub fn len(&self) -> usize {
276 self.0.len()
277 }
278
279 #[must_use]
281 pub fn is_empty(&self) -> bool {
282 self.0.is_empty()
283 }
284
285 #[must_use]
287 pub fn contains_key(&self, key: &LabelKey) -> bool {
288 self.0.contains_key(key)
289 }
290}
291
292impl FromIterator<(LabelKey, LabelValue)> for Labels {
293 fn from_iter<I: IntoIterator<Item = (LabelKey, LabelValue)>>(iter: I) -> Self {
294 Self(iter.into_iter().collect())
295 }
296}
297
298#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(transparent)]
301pub struct Tags(BTreeMap<TagKey, TagValue>);
302
303impl Tags {
304 #[must_use]
306 pub fn new() -> Self {
307 Self::default()
308 }
309
310 pub fn insert(&mut self, key: TagKey, value: TagValue) -> Option<TagValue> {
312 self.0.insert(key, value)
313 }
314
315 #[must_use]
317 pub fn get(&self, key: &TagKey) -> Option<&TagValue> {
318 self.0.get(key)
319 }
320
321 pub fn iter(&self) -> impl Iterator<Item = (&TagKey, &TagValue)> {
323 self.0.iter()
324 }
325
326 pub fn keys(&self) -> impl Iterator<Item = &TagKey> {
328 self.0.keys()
329 }
330
331 #[must_use]
333 pub fn len(&self) -> usize {
334 self.0.len()
335 }
336
337 #[must_use]
339 pub fn is_empty(&self) -> bool {
340 self.0.is_empty()
341 }
342
343 #[must_use]
345 pub fn contains_key(&self, key: &TagKey) -> bool {
346 self.0.contains_key(key)
347 }
348}
349
350impl FromIterator<(TagKey, TagValue)> for Tags {
351 fn from_iter<I: IntoIterator<Item = (TagKey, TagValue)>>(iter: I) -> Self {
352 Self(iter.into_iter().collect())
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
363pub struct Facet {
364 pub key: FacetKey,
366 pub value: FacetValue,
368}
369
370impl Facet {
371 pub fn new(
373 key: impl Into<String>,
374 value: impl Into<String>,
375 ) -> Result<Self, NameError> {
376 Ok(Self {
377 key: FacetKey::new(key)?,
378 value: FacetValue::new(value)?,
379 })
380 }
381}
382
383impl std::fmt::Display for Facet {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 write!(f, "{}:{}", self.key, self.value)
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct Plug {
392 pub name: PortName,
394 pub facets: BTreeSet<Facet>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub description: Option<String>,
399}
400
401impl Plug {
402 pub fn new(name: PortName, facets: BTreeSet<Facet>) -> Result<Self, AttributeError> {
404 if facets.is_empty() {
405 return Err(AttributeError::EmptyFacetSet {
406 port: name.into_inner(),
407 });
408 }
409 Ok(Self {
410 name,
411 facets,
412 description: None,
413 })
414 }
415
416 #[must_use]
418 pub fn with_description(mut self, description: impl Into<String>) -> Self {
419 self.description = Some(description.into());
420 self
421 }
422
423 #[must_use]
426 pub fn fits(&self, socket: &Socket) -> bool {
427 socket.facets.is_superset(&self.facets)
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433pub struct Socket {
434 pub name: PortName,
436 pub facets: BTreeSet<Facet>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub description: Option<String>,
441}
442
443impl Socket {
444 pub fn new(name: PortName, facets: BTreeSet<Facet>) -> Result<Self, AttributeError> {
446 if facets.is_empty() {
447 return Err(AttributeError::EmptyFacetSet {
448 port: name.into_inner(),
449 });
450 }
451 Ok(Self {
452 name,
453 facets,
454 description: None,
455 })
456 }
457
458 #[must_use]
460 pub fn with_description(mut self, description: impl Into<String>) -> Self {
461 self.description = Some(description.into());
462 self
463 }
464}
465
466#[must_use]
469pub fn fits(plug: &Plug, socket: &Socket) -> bool {
470 plug.fits(socket)
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
479pub struct Wire {
480 pub plug: PortName,
482 pub socket: PortName,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum WireMatch {
490 Complete {
493 wires: Vec<Wire>,
495 },
496 Partial {
498 wires: Vec<Wire>,
500 unfitted: Vec<PortName>,
502 ambiguous: Vec<(PortName, Vec<PortName>)>,
505 },
506 None,
509}
510
511#[must_use]
518pub fn wiring_for(dependent_plugs: &[Plug], target_sockets: &[Socket]) -> WireMatch {
519 let mut wires = Vec::new();
520 let mut unfitted = Vec::new();
521 let mut ambiguous = Vec::new();
522
523 for plug in dependent_plugs {
524 let fitting: Vec<&Socket> = target_sockets
525 .iter()
526 .filter(|s| plug.fits(s))
527 .collect();
528 match fitting.len() {
529 0 => unfitted.push(plug.name.clone()),
530 1 => wires.push(Wire {
531 plug: plug.name.clone(),
532 socket: fitting[0].name.clone(),
533 }),
534 _ => {
535 let candidates: Vec<PortName> =
536 fitting.iter().map(|s| s.name.clone()).collect();
537 ambiguous.push((plug.name.clone(), candidates));
538 }
539 }
540 }
541
542 if dependent_plugs.is_empty() || (unfitted.is_empty() && ambiguous.is_empty()) {
543 WireMatch::Complete { wires }
544 } else if wires.is_empty() && ambiguous.is_empty() {
545 WireMatch::None
546 } else {
547 WireMatch::Partial {
548 wires,
549 unfitted,
550 ambiguous,
551 }
552 }
553}
554
555pub fn validate_namespace(
566 labels: &Labels,
567 tags: &Tags,
568 plugs: &[Plug],
569 sockets: &[Socket],
570) -> Result<(), AttributeError> {
571 let mut port_names: BTreeSet<String> = BTreeSet::new();
573 for plug in plugs {
574 if !port_names.insert(plug.name.as_str().to_owned()) {
575 return Err(AttributeError::DuplicatePortName {
576 name: plug.name.as_str().to_owned(),
577 });
578 }
579 }
580 for socket in sockets {
581 if !port_names.insert(socket.name.as_str().to_owned()) {
582 return Err(AttributeError::DuplicatePortName {
583 name: socket.name.as_str().to_owned(),
584 });
585 }
586 }
587
588 let mut by_tier: BTreeMap<String, BTreeSet<Tier>> = BTreeMap::new();
590 for key in labels.keys() {
591 by_tier
592 .entry(key.as_str().to_owned())
593 .or_default()
594 .insert(Tier::Label);
595 }
596 for key in tags.keys() {
597 by_tier
598 .entry(key.as_str().to_owned())
599 .or_default()
600 .insert(Tier::Tag);
601 }
602 for port in &port_names {
603 by_tier.entry(port.clone()).or_default().insert(Tier::Port);
604 }
605
606 for (key, tiers) in by_tier {
607 if tiers.len() > 1 {
608 return Err(AttributeError::DuplicateKey {
609 key,
610 tiers: tiers.into_iter().collect(),
611 });
612 }
613 }
614
615 Ok(())
616}
617
618pub trait Attributed {
624 fn labels(&self) -> &Labels;
626
627 fn tags(&self) -> &Tags;
629}
630
631pub trait Pluggable: Attributed {
635 fn plugs(&self) -> &[Plug];
637
638 fn sockets(&self) -> &[Socket];
640}
641
642#[cfg(test)]
647mod tests {
648 use super::*;
649
650 fn lk(s: &str) -> LabelKey {
651 LabelKey::new(s).unwrap()
652 }
653 fn lv(s: &str) -> LabelValue {
654 LabelValue::new(s).unwrap()
655 }
656 fn tk(s: &str) -> TagKey {
657 TagKey::new(s).unwrap()
658 }
659 fn tv(s: &str) -> TagValue {
660 TagValue::new(s).unwrap()
661 }
662 fn pn(s: &str) -> PortName {
663 PortName::new(s).unwrap()
664 }
665 fn facet(k: &str, v: &str) -> Facet {
666 Facet::new(k, v).unwrap()
667 }
668 fn fset(pairs: &[(&str, &str)]) -> BTreeSet<Facet> {
669 pairs.iter().map(|(k, v)| facet(k, v)).collect()
670 }
671
672 #[test]
675 fn label_value_accepts_utf8_and_rejects_control_and_overlong() {
676 assert!(LabelValue::new("hello world").is_ok());
677 assert!(LabelValue::new("日本語").is_ok());
678 assert!(LabelValue::new("").is_err());
679 assert!(LabelValue::new("has\ncontrol").is_err());
680 let overlong = "x".repeat(ATTRIBUTE_VALUE_MAX_LEN + 1);
681 assert!(LabelValue::new(overlong).is_err());
682 }
683
684 #[test]
685 fn label_value_serde_roundtrip() {
686 let v = LabelValue::new("service").unwrap();
687 let json = serde_json::to_string(&v).unwrap();
688 assert_eq!(json, "\"service\"");
689 let back: LabelValue = serde_json::from_str(&json).unwrap();
690 assert_eq!(v, back);
691 }
692
693 #[test]
694 fn tag_value_mirrors_label_value_rules() {
695 assert!(TagValue::new("staging").is_ok());
696 assert!(TagValue::new("").is_err());
697 assert!(TagValue::new("ctrl\tchar").is_err());
698 }
699
700 #[test]
703 fn label_and_tag_keys_share_name_rules() {
704 LabelKey::new("type").unwrap();
705 TagKey::new("owner").unwrap();
706 assert!(LabelKey::new("1starts").is_err());
707 assert!(TagKey::new("has space").is_err());
708 }
709
710 #[test]
711 fn facet_key_and_value_validation() {
712 assert!(Facet::new("api", "rest").is_ok());
713 assert!(Facet::new("", "x").is_err());
714 assert!(Facet::new("k", "").is_err());
715 }
716
717 #[test]
718 fn port_name_validates() {
719 assert!(PortName::new("vector_service").is_ok());
720 assert!(PortName::new("db-main").is_ok());
721 assert!(PortName::new("").is_err());
722 }
723
724 #[test]
725 fn facet_display_is_key_colon_value() {
726 let f = Facet::new("api", "rest").unwrap();
727 assert_eq!(format!("{f}"), "api:rest");
728 }
729
730 #[test]
733 fn well_known_label_keys_are_valid() {
734 assert_eq!(label::name().as_str(), "name");
735 assert_eq!(label::r#type().as_str(), "type");
736 assert_eq!(label::description().as_str(), "description");
737 }
738
739 #[test]
742 fn labels_insert_and_query() {
743 let mut ls = Labels::new();
744 ls.insert(lk("type"), lv("service"));
745 assert_eq!(ls.len(), 1);
746 assert!(!ls.is_empty());
747 assert!(ls.contains_key(&lk("type")));
748 assert_eq!(ls.get(&lk("type")), Some(&lv("service")));
749 }
750
751 #[test]
752 fn labels_iter_is_sorted_by_key() {
753 let mut ls = Labels::new();
754 ls.insert(lk("zebra"), lv("z"));
755 ls.insert(lk("apple"), lv("a"));
756 ls.insert(lk("mango"), lv("m"));
757 let keys: Vec<&str> = ls.iter().map(|(k, _)| k.as_str()).collect();
758 assert_eq!(keys, vec!["apple", "mango", "zebra"]);
759 }
760
761 #[test]
762 fn tags_from_iterator() {
763 let t: Tags = [(tk("owner"), tv("jshook")), (tk("env"), tv("staging"))]
764 .into_iter()
765 .collect();
766 assert_eq!(t.len(), 2);
767 assert_eq!(t.get(&tk("owner")), Some(&tv("jshook")));
768 }
769
770 #[test]
771 fn labels_serde_roundtrip() {
772 let mut ls = Labels::new();
773 ls.insert(lk("type"), lv("service"));
774 let json = serde_json::to_string(&ls).unwrap();
775 assert_eq!(json, "{\"type\":\"service\"}");
777 let back: Labels = serde_json::from_str(&json).unwrap();
778 assert_eq!(ls, back);
779 }
780
781 #[test]
784 fn plug_rejects_empty_facets() {
785 let err = Plug::new(pn("p"), BTreeSet::new()).unwrap_err();
786 assert!(matches!(err, AttributeError::EmptyFacetSet { .. }));
787 }
788
789 #[test]
790 fn socket_rejects_empty_facets() {
791 let err = Socket::new(pn("s"), BTreeSet::new()).unwrap_err();
792 assert!(matches!(err, AttributeError::EmptyFacetSet { .. }));
793 }
794
795 #[test]
796 fn plug_fits_when_socket_covers_facets() {
797 let plug = Plug::new(
798 pn("vector_service"),
799 fset(&[("api", "rest"), ("protocol", "vectorbench"), ("index", "hnsw")]),
800 )
801 .unwrap();
802 let good_socket = Socket::new(
803 pn("api"),
804 fset(&[
805 ("api", "rest"),
806 ("protocol", "vectorbench"),
807 ("index", "hnsw"),
808 ("index", "diskann"),
809 ("runtime", "jvm"),
810 ]),
811 )
812 .unwrap();
813 assert!(plug.fits(&good_socket));
814 assert!(fits(&plug, &good_socket));
815 }
816
817 #[test]
818 fn plug_fails_to_fit_missing_facet() {
819 let plug = Plug::new(
820 pn("vector_service"),
821 fset(&[("api", "rest"), ("index", "hnsw")]),
822 )
823 .unwrap();
824 let wrong_index = Socket::new(
825 pn("api"),
826 fset(&[("api", "rest"), ("index", "ivf"), ("index", "flat")]),
827 )
828 .unwrap();
829 assert!(!plug.fits(&wrong_index));
830 }
831
832 #[test]
833 fn socket_can_cover_multiple_values_for_same_key() {
834 let plug = Plug::new(pn("db"), fset(&[("engine", "postgres-15")])).unwrap();
835 let socket = Socket::new(
836 pn("db_rw"),
837 fset(&[("engine", "postgres"), ("engine", "postgres-15")]),
838 )
839 .unwrap();
840 assert!(plug.fits(&socket));
841 }
842
843 #[test]
844 fn plug_serde_roundtrip() {
845 let plug = Plug::new(pn("db"), fset(&[("engine", "postgres")]))
846 .unwrap()
847 .with_description("primary datastore");
848 let json = serde_json::to_string(&plug).unwrap();
849 let back: Plug = serde_json::from_str(&json).unwrap();
850 assert_eq!(plug, back);
851 }
852
853 #[test]
856 fn wiring_for_complete_match() {
857 let plug = Plug::new(pn("vector_service"), fset(&[("api", "rest")])).unwrap();
858 let socket = Socket::new(
859 pn("api"),
860 fset(&[("api", "rest"), ("runtime", "jvm")]),
861 )
862 .unwrap();
863 let result = wiring_for(&[plug], &[socket]);
864 match result {
865 WireMatch::Complete { wires } => {
866 assert_eq!(wires.len(), 1);
867 assert_eq!(wires[0].plug.as_str(), "vector_service");
868 assert_eq!(wires[0].socket.as_str(), "api");
869 }
870 other => panic!("expected Complete, got {other:?}"),
871 }
872 }
873
874 #[test]
875 fn wiring_for_none_when_no_sockets_fit_any_plug() {
876 let plug = Plug::new(pn("db"), fset(&[("engine", "postgres")])).unwrap();
877 let socket = Socket::new(pn("api"), fset(&[("engine", "mysql")])).unwrap();
878 let result = wiring_for(&[plug], &[socket]);
879 assert_eq!(result, WireMatch::None);
880 }
881
882 #[test]
883 fn wiring_for_partial_on_mixed_outcome() {
884 let wired = Plug::new(pn("api"), fset(&[("api", "rest")])).unwrap();
885 let unfitted = Plug::new(pn("queue"), fset(&[("protocol", "amqp")])).unwrap();
886 let socket = Socket::new(pn("api_sock"), fset(&[("api", "rest")])).unwrap();
887 let result = wiring_for(&[wired, unfitted], &[socket]);
888 match result {
889 WireMatch::Partial {
890 wires,
891 unfitted,
892 ambiguous,
893 } => {
894 assert_eq!(wires.len(), 1);
895 assert_eq!(unfitted, vec![pn("queue")]);
896 assert!(ambiguous.is_empty());
897 }
898 other => panic!("expected Partial, got {other:?}"),
899 }
900 }
901
902 #[test]
903 fn wiring_for_flags_ambiguous_plug_against_multiple_sockets() {
904 let plug = Plug::new(
907 pn("db"),
908 fset(&[("kind", "database"), ("engine", "postgres")]),
909 )
910 .unwrap();
911 let read_pool = Socket::new(
912 pn("read_pool"),
913 fset(&[
914 ("kind", "database"),
915 ("engine", "postgres"),
916 ("access", "readonly"),
917 ]),
918 )
919 .unwrap();
920 let write_pool = Socket::new(
921 pn("write_pool"),
922 fset(&[
923 ("kind", "database"),
924 ("engine", "postgres"),
925 ("access", "readwrite"),
926 ]),
927 )
928 .unwrap();
929 let result = wiring_for(&[plug], &[read_pool, write_pool]);
930 match result {
931 WireMatch::Partial { ambiguous, .. } => {
932 assert_eq!(ambiguous.len(), 1);
933 let (plug_name, candidates) = &ambiguous[0];
934 assert_eq!(plug_name.as_str(), "db");
935 assert_eq!(candidates.len(), 2);
936 }
937 other => panic!("expected Partial with ambiguous, got {other:?}"),
938 }
939 }
940
941 #[test]
942 fn wiring_for_empty_plugs_is_trivially_complete() {
943 let socket = Socket::new(pn("api"), fset(&[("api", "rest")])).unwrap();
944 assert_eq!(
945 wiring_for(&[], &[socket]),
946 WireMatch::Complete { wires: vec![] }
947 );
948 }
949
950 #[test]
953 fn validate_namespace_accepts_disjoint_keys() {
954 let mut labels = Labels::new();
955 labels.insert(lk("type"), lv("service"));
956 let mut tags = Tags::new();
957 tags.insert(tk("owner"), tv("jshook"));
958 let plugs = vec![Plug::new(pn("db"), fset(&[("engine", "postgres")])).unwrap()];
959 let sockets =
960 vec![Socket::new(pn("api"), fset(&[("api", "rest")])).unwrap()];
961 assert!(validate_namespace(&labels, &tags, &plugs, &sockets).is_ok());
962 }
963
964 #[test]
965 fn validate_namespace_rejects_label_tag_collision() {
966 let mut labels = Labels::new();
967 labels.insert(lk("type"), lv("service"));
968 let mut tags = Tags::new();
969 tags.insert(tk("type"), tv("whatever"));
970 let err = validate_namespace(&labels, &tags, &[], &[]).unwrap_err();
971 match err {
972 AttributeError::DuplicateKey { key, tiers } => {
973 assert_eq!(key, "type");
974 assert!(tiers.contains(&Tier::Label));
975 assert!(tiers.contains(&Tier::Tag));
976 }
977 other => panic!("wrong error: {other:?}"),
978 }
979 }
980
981 #[test]
982 fn validate_namespace_rejects_label_port_collision() {
983 let mut labels = Labels::new();
984 labels.insert(lk("api"), lv("x"));
985 let plugs = vec![Plug::new(pn("api"), fset(&[("x", "y")])).unwrap()];
986 let err = validate_namespace(&labels, &Tags::new(), &plugs, &[]).unwrap_err();
987 assert!(matches!(err, AttributeError::DuplicateKey { .. }));
988 }
989
990 #[test]
991 fn validate_namespace_rejects_duplicate_port_name() {
992 let plug = Plug::new(pn("api"), fset(&[("x", "y")])).unwrap();
993 let socket = Socket::new(pn("api"), fset(&[("x", "y")])).unwrap();
994 let err =
995 validate_namespace(&Labels::new(), &Tags::new(), &[plug], &[socket])
996 .unwrap_err();
997 match err {
998 AttributeError::DuplicatePortName { name } => assert_eq!(name, "api"),
999 other => panic!("wrong error: {other:?}"),
1000 }
1001 }
1002
1003 struct Dummy {
1006 labels: Labels,
1007 tags: Tags,
1008 plugs: Vec<Plug>,
1009 sockets: Vec<Socket>,
1010 }
1011
1012 impl Attributed for Dummy {
1013 fn labels(&self) -> &Labels {
1014 &self.labels
1015 }
1016 fn tags(&self) -> &Tags {
1017 &self.tags
1018 }
1019 }
1020
1021 impl Pluggable for Dummy {
1022 fn plugs(&self) -> &[Plug] {
1023 &self.plugs
1024 }
1025 fn sockets(&self) -> &[Socket] {
1026 &self.sockets
1027 }
1028 }
1029
1030 #[test]
1031 fn traits_read_through() {
1032 let d = Dummy {
1033 labels: Labels::new(),
1034 tags: Tags::new(),
1035 plugs: vec![Plug::new(pn("api"), fset(&[("x", "y")])).unwrap()],
1036 sockets: vec![],
1037 };
1038 assert_eq!(<Dummy as Pluggable>::plugs(&d).len(), 1);
1039 assert!(<Dummy as Attributed>::labels(&d).is_empty());
1040 }
1041}