Skip to main content

paramodel_elements/
attributes.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Labels, tags, plugs, sockets, and wires.
5//!
6//! Three tiers per SRD-0005:
7//!
8//! - **Labels** — intrinsic facts ("what this entity *is*"). Participate
9//!   in identity and fingerprinting.
10//! - **Tags** — extrinsic organisation ("how I group/filter this").
11//!   Do *not* participate in identity.
12//! - **Plugs and sockets** — structured compatibility surface on
13//!   elements. Plugs name the upstreams an element *needs*; sockets
14//!   name the points where downstreams *connect to* it. Each carries a
15//!   non-empty set of `(key, value)` facets; a plug fits a socket iff
16//!   the socket's facets cover (superset) the plug's.
17//!
18//! Wires are the concrete plug↔socket connections the compiler produces
19//! when it resolves an authored dependency — [`wiring_for`] returns the
20//! [`WireMatch`] for a dependent's plugs against a target's sockets.
21//!
22//! Namespace-uniqueness across tiers is enforced by
23//! [`validate_namespace`]: a single key cannot appear as both a label
24//! key and a tag key, nor as both an attribute key and a port name.
25
26use std::collections::{BTreeMap, BTreeSet};
27
28use serde::{Deserialize, Serialize, de};
29
30use crate::name_type;
31use crate::names::NameError;
32
33// ---------------------------------------------------------------------------
34// AttributeError + Tier.
35// ---------------------------------------------------------------------------
36
37/// Tier a given key was authored on.
38///
39/// Used in diagnostics when a key is found in more than one tier at
40/// once. `Port` covers both plug and socket names — they share the
41/// per-element name namespace (SRD-0005 D13).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum Tier {
45    /// Labels: intrinsic facts.
46    Label,
47    /// Tags: extrinsic organisation.
48    Tag,
49    /// Port (plug or socket) name.
50    Port,
51}
52
53/// Errors raised by attribute-layer construction and validation.
54#[derive(Debug, thiserror::Error, PartialEq, Eq)]
55pub enum AttributeError {
56    /// The same key appears in multiple tiers on one entity.
57    #[error("attribute key '{key}' appears in multiple tiers: {tiers:?}")]
58    DuplicateKey {
59        /// The conflicting key.
60        key:   String,
61        /// Every tier the key was seen in.
62        tiers: Vec<Tier>,
63    },
64
65    /// A plug or socket declared no facets.
66    #[error("port '{port}' has an empty facet set; at least one facet is required")]
67    EmptyFacetSet {
68        /// The offending port name.
69        port: String,
70    },
71
72    /// Two plugs, two sockets, or a plug and a socket share a name.
73    #[error("port name '{name}' is used more than once across plugs and sockets")]
74    DuplicatePortName {
75        /// The duplicated port name.
76        name: String,
77    },
78}
79
80// ---------------------------------------------------------------------------
81// Attribute-value validation — distinct from Name validation because
82// values are UTF-8 strings with control-character exclusion only, not
83// identifier-safe ASCII. LabelValue and TagValue share this rule.
84// ---------------------------------------------------------------------------
85
86const 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
106/// Stamp out a validated attribute-value newtype (UTF-8, ≤ 256 bytes,
107/// no control characters).
108macro_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            /// Construct a new value, validating the candidate.
115            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            /// Borrow the inner string.
122            #[must_use]
123            pub fn as_str(&self) -> &str {
124                &self.0
125            }
126
127            /// Consume and return the inner string.
128            #[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
175// ---------------------------------------------------------------------------
176// Name-style newtypes (ASCII identifier + '-' '.' '_').
177// ---------------------------------------------------------------------------
178
179name_type! {
180    /// Intrinsic-fact key on an attributable entity. Canonical
181    /// well-known keys live in [`label`].
182    pub struct LabelKey { kind: "LabelKey" }
183}
184
185name_type! {
186    /// Organisational tag key. Adopter-defined; paramodel ships no
187    /// canonical keys at this layer.
188    pub struct TagKey { kind: "TagKey" }
189}
190
191name_type! {
192    /// Key half of a `(key, value)` facet pair on a plug or socket.
193    pub struct FacetKey { kind: "FacetKey" }
194}
195
196name_type! {
197    /// Value half of a `(key, value)` facet pair.
198    pub struct FacetValue { kind: "FacetValue" }
199}
200
201name_type! {
202    /// Shared name for plugs and sockets. The two kinds live in the
203    /// same per-element namespace (SRD-0005 D13): an element may not
204    /// have a plug and a socket with the same `PortName`.
205    pub struct PortName { kind: "PortName" }
206}
207
208// ---------------------------------------------------------------------------
209// Well-known label keys.
210// ---------------------------------------------------------------------------
211
212/// Canonical label keys. Not *required* on every entity — authoring
213/// these spellings lets tooling treat them uniformly.
214pub mod label {
215    use super::LabelKey;
216
217    /// `"name"` — the human-readable name of the entity.
218    #[must_use]
219    pub fn name() -> LabelKey {
220        LabelKey::new("name").expect("`name` is a valid label key")
221    }
222
223    /// `"type"` — the entity's role / kind.
224    #[must_use]
225    pub fn r#type() -> LabelKey {
226        LabelKey::new("type").expect("`type` is a valid label key")
227    }
228
229    /// `"description"` — a free-form description of the entity.
230    #[must_use]
231    pub fn description() -> LabelKey {
232        LabelKey::new("description").expect("`description` is a valid label key")
233    }
234}
235
236// ---------------------------------------------------------------------------
237// Labels and Tags.
238// ---------------------------------------------------------------------------
239
240/// Intrinsic-fact map. See module docs and SRD-0005.
241#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(transparent)]
243pub struct Labels(BTreeMap<LabelKey, LabelValue>);
244
245impl Labels {
246    /// Empty label map.
247    #[must_use]
248    pub fn new() -> Self {
249        Self::default()
250    }
251
252    /// Insert a label. Returns the previous value for `key`, if any.
253    pub fn insert(&mut self, key: LabelKey, value: LabelValue) -> Option<LabelValue> {
254        self.0.insert(key, value)
255    }
256
257    /// Look up a label.
258    #[must_use]
259    pub fn get(&self, key: &LabelKey) -> Option<&LabelValue> {
260        self.0.get(key)
261    }
262
263    /// Sorted-by-key iterator over `(key, value)` pairs.
264    pub fn iter(&self) -> impl Iterator<Item = (&LabelKey, &LabelValue)> {
265        self.0.iter()
266    }
267
268    /// Key-only iterator, sorted.
269    pub fn keys(&self) -> impl Iterator<Item = &LabelKey> {
270        self.0.keys()
271    }
272
273    /// Number of labels.
274    #[must_use]
275    pub fn len(&self) -> usize {
276        self.0.len()
277    }
278
279    /// `true` when no labels are set.
280    #[must_use]
281    pub fn is_empty(&self) -> bool {
282        self.0.is_empty()
283    }
284
285    /// Key-membership test.
286    #[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/// Extrinsic-organisation map. See module docs and SRD-0005.
299#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(transparent)]
301pub struct Tags(BTreeMap<TagKey, TagValue>);
302
303impl Tags {
304    /// Empty tag map.
305    #[must_use]
306    pub fn new() -> Self {
307        Self::default()
308    }
309
310    /// Insert a tag. Returns the previous value for `key`, if any.
311    pub fn insert(&mut self, key: TagKey, value: TagValue) -> Option<TagValue> {
312        self.0.insert(key, value)
313    }
314
315    /// Look up a tag.
316    #[must_use]
317    pub fn get(&self, key: &TagKey) -> Option<&TagValue> {
318        self.0.get(key)
319    }
320
321    /// Sorted-by-key iterator over `(key, value)` pairs.
322    pub fn iter(&self) -> impl Iterator<Item = (&TagKey, &TagValue)> {
323        self.0.iter()
324    }
325
326    /// Key-only iterator, sorted.
327    pub fn keys(&self) -> impl Iterator<Item = &TagKey> {
328        self.0.keys()
329    }
330
331    /// Number of tags.
332    #[must_use]
333    pub fn len(&self) -> usize {
334        self.0.len()
335    }
336
337    /// `true` when no tags are set.
338    #[must_use]
339    pub fn is_empty(&self) -> bool {
340        self.0.is_empty()
341    }
342
343    /// Key-membership test.
344    #[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// ---------------------------------------------------------------------------
357// Facet, Plug, Socket.
358// ---------------------------------------------------------------------------
359
360/// Structured `(key, value)` pair on a plug or socket. Canonical human
361/// rendering is `"key:value"`.
362#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
363pub struct Facet {
364    /// Dimension of classification.
365    pub key:   FacetKey,
366    /// Point on that dimension.
367    pub value: FacetValue,
368}
369
370impl Facet {
371    /// Construct a new facet.
372    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/// A point where an element needs to connect to an upstream.
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct Plug {
392    /// Name of this plug on the owning element.
393    pub name:        PortName,
394    /// Facets that any fitting socket must cover.
395    pub facets:      BTreeSet<Facet>,
396    /// Optional human-readable description.
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub description: Option<String>,
399}
400
401impl Plug {
402    /// Construct a plug. Rejects an empty facet set per SRD-0005 D8.
403    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    /// Builder-style setter for the description.
417    #[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    /// This plug fits the given socket iff the socket's facet set is a
424    /// superset of the plug's.
425    #[must_use]
426    pub fn fits(&self, socket: &Socket) -> bool {
427        socket.facets.is_superset(&self.facets)
428    }
429}
430
431/// A point where downstream elements connect to this element.
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433pub struct Socket {
434    /// Name of this socket on the owning element.
435    pub name:        PortName,
436    /// Facets this socket covers.
437    pub facets:      BTreeSet<Facet>,
438    /// Optional human-readable description.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub description: Option<String>,
441}
442
443impl Socket {
444    /// Construct a socket. Rejects an empty facet set per SRD-0005 D8.
445    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    /// Builder-style setter for the description.
459    #[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/// Does the plug fit the socket? True iff `socket.facets` is a
467/// superset of `plug.facets`.
468#[must_use]
469pub fn fits(plug: &Plug, socket: &Socket) -> bool {
470    plug.fits(socket)
471}
472
473// ---------------------------------------------------------------------------
474// Wire, WireMatch, wiring_for.
475// ---------------------------------------------------------------------------
476
477/// One concrete plug↔socket connection.
478#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
479pub struct Wire {
480    /// The plug on the dependent element.
481    pub plug:   PortName,
482    /// The socket on the target element.
483    pub socket: PortName,
484}
485
486/// Outcome of matching a dependent element's plugs against a target
487/// element's sockets.
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum WireMatch {
490    /// Every plug on the dependent has exactly one fitting socket on
491    /// the target.
492    Complete {
493        /// Resolved connections.
494        wires: Vec<Wire>,
495    },
496    /// Mixed outcome: some plugs wired, others unfitted or ambiguous.
497    Partial {
498        /// Plugs with exactly one fitting socket.
499        wires:     Vec<Wire>,
500        /// Plugs with no fitting socket.
501        unfitted:  Vec<PortName>,
502        /// Plugs with more than one fitting socket — each entry names
503        /// the plug and the candidate socket list.
504        ambiguous: Vec<(PortName, Vec<PortName>)>,
505    },
506    /// The dependent has at least one plug and none of its plugs have
507    /// any fitting socket on the target.
508    None,
509}
510
511/// Match each plug on the dependent against the target's sockets,
512/// returning a [`WireMatch`] describing the outcome.
513///
514/// Per SRD-0005 D15, a plug with more than one fitting socket is an
515/// authoring error — the caller (the compiler) escalates an `Ambiguous`
516/// entry into a compile failure.
517#[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
555// ---------------------------------------------------------------------------
556// Namespace validation across tiers.
557// ---------------------------------------------------------------------------
558
559/// Validate the three-tier namespace-uniqueness rule (SRD-0005 D5).
560///
561/// Enforces:
562///
563/// - Port names are unique across plugs and sockets on one element.
564/// - No key appears in more than one tier (label / tag / port).
565pub fn validate_namespace(
566    labels:  &Labels,
567    tags:    &Tags,
568    plugs:   &[Plug],
569    sockets: &[Socket],
570) -> Result<(), AttributeError> {
571    // 1. Port names must be unique across plugs and sockets.
572    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    // 2. A single key may not appear in more than one tier.
589    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
618// ---------------------------------------------------------------------------
619// Attributed / Pluggable traits.
620// ---------------------------------------------------------------------------
621
622/// Read interface for any entity carrying [`Labels`] and [`Tags`].
623pub trait Attributed {
624    /// Intrinsic facts.
625    fn labels(&self) -> &Labels;
626
627    /// Extrinsic organisation.
628    fn tags(&self) -> &Tags;
629}
630
631/// Read interface for elements — adds plugs and sockets on top of
632/// [`Attributed`]. Only elements implement `Pluggable` at this layer
633/// (SRD-0005 D11).
634pub trait Pluggable: Attributed {
635    /// The element's plugs.
636    fn plugs(&self) -> &[Plug];
637
638    /// The element's sockets.
639    fn sockets(&self) -> &[Socket];
640}
641
642// ---------------------------------------------------------------------------
643// Tests.
644// ---------------------------------------------------------------------------
645
646#[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    // ---------- LabelValue / TagValue validation ----------
673
674    #[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    // ---------- Name-style newtypes ----------
701
702    #[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    // ---------- label:: module ----------
731
732    #[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    // ---------- Labels / Tags ----------
740
741    #[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        // Serializes transparently as a JSON object keyed by LabelKey.
776        assert_eq!(json, "{\"type\":\"service\"}");
777        let back: Labels = serde_json::from_str(&json).unwrap();
778        assert_eq!(ls, back);
779    }
780
781    // ---------- Plug / Socket / fits ----------
782
783    #[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    // ---------- wiring_for / WireMatch ----------
854
855    #[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        // The SRD-0005 D15 example: a `db` plug that fits both
905        // `read_pool` and `write_pool` sockets.
906        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    // ---------- validate_namespace ----------
951
952    #[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    // ---------- Attributed / Pluggable traits ----------
1004
1005    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}