Skip to main content

use_friend_registry/
lib.rs

1#![doc = include_str!("../README.md")]
2
3//! Collection and query helpers for Fellow Friends fixture records.
4
5use std::{borrow::Borrow, collections::BTreeMap};
6
7use use_friend::{FigureKind, Friend, IdentityKind, TechnologyKind};
8use use_friend_fixture::FriendFixtures;
9
10/// Owned registry of Fellow Friends records.
11#[derive(Clone, Debug, Default)]
12pub struct FriendRegistry {
13    friends: Vec<Friend>,
14}
15
16impl FriendRegistry {
17    /// Builds a registry from owned friends or borrowed fixture records.
18    #[must_use]
19    pub fn new<I>(friends: I) -> Self
20    where
21        I: IntoIterator,
22        I::Item: Borrow<Friend>,
23    {
24        Self {
25            friends: friends.into_iter().map(|friend| *friend.borrow()).collect(),
26        }
27    }
28
29    /// Builds a registry from a static fixture slice.
30    #[must_use]
31    pub fn from_static(friends: &'static [Friend]) -> Self {
32        Self::new(friends)
33    }
34
35    /// Builds a registry containing every bundled fixture record.
36    #[must_use]
37    pub fn all() -> Self {
38        Self::from_static(FriendFixtures::all())
39    }
40
41    /// Returns the number of friends in the registry.
42    #[must_use]
43    pub fn len(&self) -> usize {
44        self.friends.len()
45    }
46
47    /// Returns `true` when the registry has no records.
48    #[must_use]
49    pub fn is_empty(&self) -> bool {
50        self.friends.is_empty()
51    }
52
53    /// Iterates over friends in registry order.
54    pub fn iter(&self) -> impl Iterator<Item = &Friend> {
55        self.friends.iter()
56    }
57
58    /// Returns friends matching an ecosystem name.
59    #[must_use]
60    pub fn by_ecosystem(&self, ecosystem: &str) -> Vec<&Friend> {
61        self.iter()
62            .filter(|friend| friend.matches_ecosystem(ecosystem))
63            .collect()
64    }
65
66    /// Returns friends containing a tag.
67    #[must_use]
68    pub fn by_tag(&self, tag: &str) -> Vec<&Friend> {
69        self.iter().filter(|friend| friend.has_tag(tag)).collect()
70    }
71
72    /// Returns friends with the requested technology kind.
73    #[must_use]
74    pub fn by_technology_kind(&self, kind: TechnologyKind) -> Vec<&Friend> {
75        self.iter()
76            .filter(|friend| friend.technology_kind == kind)
77            .collect()
78    }
79
80    /// Returns friends with the requested identity kind.
81    #[must_use]
82    pub fn by_identity_kind(&self, kind: IdentityKind) -> Vec<&Friend> {
83        self.iter()
84            .filter(|friend| friend.identity_kind == kind)
85            .collect()
86    }
87
88    /// Returns friends with the requested figure kind.
89    #[must_use]
90    pub fn by_figure_kind(&self, kind: FigureKind) -> Vec<&Friend> {
91        self.iter()
92            .filter(|friend| friend.figure_kind == kind)
93            .collect()
94    }
95
96    /// Finds one friend by stable identifier.
97    #[must_use]
98    pub fn find_by_id(&self, id: &str) -> Option<&Friend> {
99        let normalized = id.trim();
100
101        if normalized.is_empty() {
102            return None;
103        }
104
105        self.iter().find(|friend| friend.id == normalized)
106    }
107
108    /// Counts friends by technology kind in deterministic key order.
109    #[must_use]
110    pub fn count_by_technology_kind(&self) -> BTreeMap<TechnologyKind, usize> {
111        let mut counts = BTreeMap::new();
112
113        for friend in &self.friends {
114            *counts.entry(friend.technology_kind).or_default() += 1;
115        }
116
117        counts
118    }
119
120    /// Counts friends by identity kind in deterministic key order.
121    #[must_use]
122    pub fn count_by_identity_kind(&self) -> BTreeMap<IdentityKind, usize> {
123        let mut counts = BTreeMap::new();
124
125        for friend in &self.friends {
126            *counts.entry(friend.identity_kind).or_default() += 1;
127        }
128
129        counts
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::FriendRegistry;
136    use use_friend::{FigureKind, IdentityKind, TechnologyKind};
137    use use_friend_fixture::FriendFixtures;
138
139    #[test]
140    fn builds_from_static_slice_with_new() {
141        let registry = FriendRegistry::new(FriendFixtures::small());
142        let rust_friends = registry.by_ecosystem(" rust ");
143
144        assert_eq!(registry.len(), 6);
145        assert_eq!(rust_friends.len(), 1);
146        assert_eq!(rust_friends[0].name, "Ferris");
147        assert!(!registry.is_empty());
148    }
149
150    #[test]
151    fn filters_by_tag_and_kinds() {
152        let registry = FriendRegistry::all();
153
154        assert_eq!(registry.by_tag("container")[0].ecosystem, "Docker");
155        assert_eq!(
156            registry
157                .by_technology_kind(TechnologyKind::ProgrammingLanguage)
158                .len(),
159            12
160        );
161        assert_eq!(
162            registry.by_identity_kind(IdentityKind::LogoCharacter).len(),
163            8
164        );
165        assert_eq!(registry.by_figure_kind(FigureKind::Creature).len(), 7);
166    }
167
168    #[test]
169    fn filters_new_zig_database_and_language_records() {
170        let registry = FriendRegistry::all();
171        let zig_names: Vec<_> = registry
172            .by_ecosystem("Zig")
173            .into_iter()
174            .map(|friend| friend.name)
175            .collect();
176        let database_names: Vec<_> = registry
177            .by_tag("database")
178            .into_iter()
179            .map(|friend| friend.name)
180            .collect();
181        let language_ecosystems: Vec<_> = registry
182            .by_technology_kind(TechnologyKind::ProgrammingLanguage)
183            .into_iter()
184            .map(|friend| friend.ecosystem)
185            .collect();
186
187        assert!(zig_names.contains(&"Zero the Ziguana"));
188        assert!(zig_names.contains(&"Ziggy the Ziguana"));
189        assert!(zig_names.contains(&"Carmen the Allocgator"));
190        assert!(database_names.contains(&"Sakila"));
191        assert!(database_names.contains(&"MariaDB Sea Lion"));
192
193        for ecosystem in ["Java", "PHP", "Zig", "Raku", "Perl", "Crystal"] {
194            assert!(language_ecosystems.contains(&ecosystem));
195        }
196    }
197
198    #[test]
199    fn finds_by_stable_identifier() {
200        let registry = FriendRegistry::all();
201
202        assert_eq!(
203            registry
204                .find_by_id(" github-octocat ")
205                .map(|friend| friend.name),
206            Some("Octocat")
207        );
208        assert_eq!(registry.find_by_id(""), None);
209        assert_eq!(registry.find_by_id("missing"), None);
210    }
211
212    #[test]
213    fn counts_with_deterministic_maps() {
214        let registry = FriendRegistry::all();
215        let technology_counts = registry.count_by_technology_kind();
216        let identity_counts = registry.count_by_identity_kind();
217
218        assert_eq!(
219            technology_counts.get(&TechnologyKind::ProgrammingLanguage),
220            Some(&12)
221        );
222        assert_eq!(
223            technology_counts.get(&TechnologyKind::OperatingSystem),
224            Some(&7)
225        );
226        assert_eq!(identity_counts.get(&IdentityKind::Mascot), Some(&20));
227        assert_eq!(identity_counts.get(&IdentityKind::LogoSymbol), Some(&4));
228    }
229}