1#![doc = include_str!("../README.md")]
2
3use std::fmt;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10pub struct Friend {
11 pub id: &'static str,
13 pub name: &'static str,
15 pub ecosystem: &'static str,
17 pub technology_kind: TechnologyKind,
19 pub identity_kind: IdentityKind,
21 pub figure_kind: FigureKind,
23 pub form: Option<&'static str>,
25 pub tags: &'static [&'static str],
27 pub notes: &'static str,
29}
30
31impl Friend {
32 #[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 #[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 #[must_use]
58 pub const fn slug(&self) -> &'static str {
59 self.id
60 }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum TechnologyKind {
67 ProgrammingLanguage,
69 Browser,
71 OperatingSystem,
73 Database,
75 DevTool,
77 Platform,
79 Framework,
81 Runtime,
83 Editor,
85 PackageManager,
87 Community,
89 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub enum IdentityKind {
103 Mascot,
105 UnofficialMascot,
107 LogoCharacter,
109 LogoSymbol,
111 CompanionCharacter,
113 CommunityCharacter,
115 Symbol,
117 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub enum FigureKind {
131 Animal,
133 Creature,
135 Object,
137 Symbol,
139 HumanLike,
141 Abstract,
143 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#[cfg(feature = "serde")]
158#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
159pub struct FriendRecord {
160 pub id: String,
162 pub name: String,
164 pub ecosystem: String,
166 pub technology_kind: TechnologyKind,
168 pub identity_kind: IdentityKind,
170 pub figure_kind: FigureKind,
172 pub form: Option<String>,
174 pub tags: Vec<String>,
176 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}