Skip to main content

use_friend/
lib.rs

1#![doc = include_str!("../README.md")]
2
3//! Core data model for RustUse Fellow Friends fixture records.
4
5use std::fmt;
6
7/// A typed metadata record for one Fellow Friend fixture.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10pub struct Friend {
11    /// Stable fixture identifier.
12    pub id: &'static str,
13    /// Human-readable display name.
14    pub name: &'static str,
15    /// Technology ecosystem associated with the record.
16    pub ecosystem: &'static str,
17    /// Broad technology category for the ecosystem.
18    pub technology_kind: TechnologyKind,
19    /// Broad identity category for the friend or symbol.
20    pub identity_kind: IdentityKind,
21    /// Broad figure category for the visual or symbolic form.
22    pub figure_kind: FigureKind,
23    /// Short neutral form label, when one is useful.
24    pub form: Option<&'static str>,
25    /// Stable lowercase tags for examples and filtering.
26    pub tags: &'static [&'static str],
27    /// Short conservative note suitable for docs and sample data.
28    pub notes: &'static str,
29}
30
31impl Friend {
32    /// Returns `true` when this friend has a tag matching `tag`.
33    ///
34    /// Matching trims the input and uses ASCII case-insensitive comparison.
35    #[must_use]
36    pub fn has_tag(&self, tag: &str) -> bool {
37        let normalized = tag.trim();
38
39        !normalized.is_empty()
40            && self
41                .tags
42                .iter()
43                .any(|candidate| candidate.eq_ignore_ascii_case(normalized))
44    }
45
46    /// Returns `true` when this friend belongs to `ecosystem`.
47    ///
48    /// Matching trims the input and uses ASCII case-insensitive comparison.
49    #[must_use]
50    pub fn matches_ecosystem(&self, ecosystem: &str) -> bool {
51        let normalized = ecosystem.trim();
52
53        !normalized.is_empty() && self.ecosystem.eq_ignore_ascii_case(normalized)
54    }
55
56    /// Returns the stable slug for this fixture record.
57    #[must_use]
58    pub const fn slug(&self) -> &'static str {
59        self.id
60    }
61}
62
63/// Broad technology category for a Fellow Friend fixture.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum TechnologyKind {
67    /// Programming language ecosystem.
68    ProgrammingLanguage,
69    /// Browser ecosystem.
70    Browser,
71    /// Operating system ecosystem.
72    OperatingSystem,
73    /// Database ecosystem.
74    Database,
75    /// Developer tool ecosystem.
76    DevTool,
77    /// Platform ecosystem.
78    Platform,
79    /// Framework ecosystem.
80    Framework,
81    /// Runtime ecosystem.
82    Runtime,
83    /// Editor ecosystem.
84    Editor,
85    /// Package manager ecosystem.
86    PackageManager,
87    /// Community ecosystem.
88    Community,
89    /// Unknown or intentionally unspecified technology category.
90    Unknown,
91}
92
93impl fmt::Display for TechnologyKind {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        formatter.write_str(technology_kind_label(*self))
96    }
97}
98
99/// Broad identity category for a Fellow Friend fixture.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub enum IdentityKind {
103    /// Mascot identity.
104    Mascot,
105    /// Unofficial mascot identity.
106    UnofficialMascot,
107    /// Logo character identity.
108    LogoCharacter,
109    /// Logo symbol identity.
110    LogoSymbol,
111    /// Companion character identity.
112    CompanionCharacter,
113    /// Community character identity.
114    CommunityCharacter,
115    /// Symbol identity.
116    Symbol,
117    /// Unknown or intentionally unspecified identity category.
118    Unknown,
119}
120
121impl fmt::Display for IdentityKind {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        formatter.write_str(identity_kind_label(*self))
124    }
125}
126
127/// Broad figure category for a Fellow Friend fixture.
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub enum FigureKind {
131    /// Animal figure.
132    Animal,
133    /// Creature figure.
134    Creature,
135    /// Object figure.
136    Object,
137    /// Symbol figure.
138    Symbol,
139    /// Human-like figure.
140    HumanLike,
141    /// Abstract figure.
142    Abstract,
143    /// Unknown or intentionally unspecified figure category.
144    Unknown,
145}
146
147impl fmt::Display for FigureKind {
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        formatter.write_str(figure_kind_label(*self))
150    }
151}
152
153/// Owned serde-friendly representation of a Fellow Friend record.
154///
155/// `Friend` stores static fixture references. `FriendRecord` is available behind
156/// the `serde` feature for examples that need to deserialize owned sample data.
157#[cfg(feature = "serde")]
158#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
159pub struct FriendRecord {
160    /// Stable fixture identifier.
161    pub id: String,
162    /// Human-readable display name.
163    pub name: String,
164    /// Technology ecosystem associated with the record.
165    pub ecosystem: String,
166    /// Broad technology category for the ecosystem.
167    pub technology_kind: TechnologyKind,
168    /// Broad identity category for the friend or symbol.
169    pub identity_kind: IdentityKind,
170    /// Broad figure category for the visual or symbolic form.
171    pub figure_kind: FigureKind,
172    /// Short neutral form label, when one is useful.
173    pub form: Option<String>,
174    /// Stable tags for examples and filtering.
175    pub tags: Vec<String>,
176    /// Short conservative note suitable for docs and sample data.
177    pub notes: String,
178}
179
180#[cfg(feature = "serde")]
181impl From<&Friend> for FriendRecord {
182    fn from(friend: &Friend) -> Self {
183        Self {
184            id: friend.id.to_owned(),
185            name: friend.name.to_owned(),
186            ecosystem: friend.ecosystem.to_owned(),
187            technology_kind: friend.technology_kind,
188            identity_kind: friend.identity_kind,
189            figure_kind: friend.figure_kind,
190            form: friend.form.map(str::to_owned),
191            tags: friend.tags.iter().map(|tag| (*tag).to_owned()).collect(),
192            notes: friend.notes.to_owned(),
193        }
194    }
195}
196
197#[cfg(feature = "serde")]
198impl From<Friend> for FriendRecord {
199    fn from(friend: Friend) -> Self {
200        Self::from(&friend)
201    }
202}
203
204const fn technology_kind_label(kind: TechnologyKind) -> &'static str {
205    match kind {
206        TechnologyKind::ProgrammingLanguage => "Programming language",
207        TechnologyKind::Browser => "Browser",
208        TechnologyKind::OperatingSystem => "Operating system",
209        TechnologyKind::Database => "Database",
210        TechnologyKind::DevTool => "Developer tool",
211        TechnologyKind::Platform => "Platform",
212        TechnologyKind::Framework => "Framework",
213        TechnologyKind::Runtime => "Runtime",
214        TechnologyKind::Editor => "Editor",
215        TechnologyKind::PackageManager => "Package manager",
216        TechnologyKind::Community => "Community",
217        TechnologyKind::Unknown => "Unknown",
218    }
219}
220
221const fn identity_kind_label(kind: IdentityKind) -> &'static str {
222    match kind {
223        IdentityKind::Mascot => "Mascot",
224        IdentityKind::UnofficialMascot => "Unofficial mascot",
225        IdentityKind::LogoCharacter => "Logo character",
226        IdentityKind::LogoSymbol => "Logo symbol",
227        IdentityKind::CompanionCharacter => "Companion character",
228        IdentityKind::CommunityCharacter => "Community character",
229        IdentityKind::Symbol => "Symbol",
230        IdentityKind::Unknown => "Unknown",
231    }
232}
233
234const fn figure_kind_label(kind: FigureKind) -> &'static str {
235    match kind {
236        FigureKind::Animal => "Animal",
237        FigureKind::Creature => "Creature",
238        FigureKind::Object => "Object",
239        FigureKind::Symbol => "Symbol",
240        FigureKind::HumanLike => "Human-like",
241        FigureKind::Abstract => "Abstract",
242        FigureKind::Unknown => "Unknown",
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::{FigureKind, Friend, IdentityKind, TechnologyKind};
249
250    const FERRIS: Friend = Friend {
251        id: "rust-ferris",
252        name: "Ferris",
253        ecosystem: "Rust",
254        technology_kind: TechnologyKind::ProgrammingLanguage,
255        identity_kind: IdentityKind::UnofficialMascot,
256        figure_kind: FigureKind::Animal,
257        form: Some("crab"),
258        tags: &["rust", "crab", "community"],
259        notes: "Friendly crab associated with the Rust community.",
260    };
261
262    #[test]
263    fn helper_methods_match_trimmed_ascii_case_insensitive_values() {
264        assert!(FERRIS.has_tag(" community "));
265        assert!(FERRIS.has_tag("CRAB"));
266        assert!(!FERRIS.has_tag(""));
267        assert!(FERRIS.matches_ecosystem(" rust "));
268        assert!(!FERRIS.matches_ecosystem("Go"));
269        assert_eq!(FERRIS.slug(), "rust-ferris");
270    }
271
272    #[test]
273    fn display_labels_are_human_readable() {
274        assert_eq!(
275            TechnologyKind::ProgrammingLanguage.to_string(),
276            "Programming language"
277        );
278        assert_eq!(
279            TechnologyKind::PackageManager.to_string(),
280            "Package manager"
281        );
282        assert_eq!(
283            IdentityKind::UnofficialMascot.to_string(),
284            "Unofficial mascot"
285        );
286        assert_eq!(IdentityKind::LogoCharacter.to_string(), "Logo character");
287        assert_eq!(FigureKind::HumanLike.to_string(), "Human-like");
288    }
289
290    #[cfg(feature = "serde")]
291    #[test]
292    fn serde_feature_exposes_serialize_and_owned_deserialize_shapes() {
293        use super::FriendRecord;
294
295        fn assert_serialize<T: serde::Serialize>() {}
296        fn assert_deserialize<'de, T: serde::Deserialize<'de>>() {}
297
298        assert_serialize::<Friend>();
299        assert_serialize::<TechnologyKind>();
300        assert_deserialize::<TechnologyKind>();
301        assert_deserialize::<FriendRecord>();
302
303        let record = FriendRecord::from(FERRIS);
304
305        assert_eq!(record.id, "rust-ferris");
306        assert_eq!(record.tags, ["rust", "crab", "community"]);
307    }
308}