1use std::{fmt, str::FromStr};
7
8const CONFIDENCE_THRESHOLD: f64 = 0.8;
9
10const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
11
12#[allow(missing_docs)]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19#[non_exhaustive]
20pub enum AppCategory {
21 Business,
22 DeveloperTool,
23 Education,
24 Entertainment,
25 Finance,
26 Game,
27 ActionGame,
28 AdventureGame,
29 ArcadeGame,
30 BoardGame,
31 CardGame,
32 CasinoGame,
33 DiceGame,
34 EducationalGame,
35 FamilyGame,
36 KidsGame,
37 MusicGame,
38 PuzzleGame,
39 RacingGame,
40 RolePlayingGame,
41 SimulationGame,
42 SportsGame,
43 StrategyGame,
44 TriviaGame,
45 WordGame,
46 GraphicsAndDesign,
47 HealthcareAndFitness,
48 Lifestyle,
49 Medical,
50 Music,
51 News,
52 Photography,
53 Productivity,
54 Reference,
55 SocialNetworking,
56 Sports,
57 Travel,
58 Utility,
59 Video,
60 Weather,
61}
62
63impl FromStr for AppCategory {
64 type Err = Option<&'static str>;
65
66 fn from_str(input: &str) -> Result<AppCategory, Self::Err> {
69 let mut input = input.to_ascii_lowercase();
71 if input.starts_with(MACOS_APP_CATEGORY_PREFIX) {
72 input = input
73 .split_at(MACOS_APP_CATEGORY_PREFIX.len())
74 .1
75 .to_string();
76 }
77 input = input.replace(' ', "");
78 input = input.replace('-', "");
79
80 let mut best_confidence = 0.0;
82 let mut best_category: Option<AppCategory> = None;
83 for &(string, category) in CATEGORY_STRINGS.iter() {
84 if input == string {
85 return Ok(category);
86 }
87 let confidence = strsim::jaro_winkler(&input, string);
88 if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence {
89 best_confidence = confidence;
90 best_category = Some(category);
91 }
92 }
93 Err(best_category.map(AppCategory::canonical))
94 }
95}
96
97impl AppCategory {
98 fn canonical(self) -> &'static str {
101 match self {
102 AppCategory::Business => "Business",
103 AppCategory::DeveloperTool => "Developer Tool",
104 AppCategory::Education => "Education",
105 AppCategory::Entertainment => "Entertainment",
106 AppCategory::Finance => "Finance",
107 AppCategory::Game => "Game",
108 AppCategory::ActionGame => "Action Game",
109 AppCategory::AdventureGame => "Adventure Game",
110 AppCategory::ArcadeGame => "Arcade Game",
111 AppCategory::BoardGame => "Board Game",
112 AppCategory::CardGame => "Card Game",
113 AppCategory::CasinoGame => "Casino Game",
114 AppCategory::DiceGame => "Dice Game",
115 AppCategory::EducationalGame => "Educational Game",
116 AppCategory::FamilyGame => "Family Game",
117 AppCategory::KidsGame => "Kids Game",
118 AppCategory::MusicGame => "Music Game",
119 AppCategory::PuzzleGame => "Puzzle Game",
120 AppCategory::RacingGame => "Racing Game",
121 AppCategory::RolePlayingGame => "Role-Playing Game",
122 AppCategory::SimulationGame => "Simulation Game",
123 AppCategory::SportsGame => "Sports Game",
124 AppCategory::StrategyGame => "Strategy Game",
125 AppCategory::TriviaGame => "Trivia Game",
126 AppCategory::WordGame => "Word Game",
127 AppCategory::GraphicsAndDesign => "Graphics and Design",
128 AppCategory::HealthcareAndFitness => "Healthcare and Fitness",
129 AppCategory::Lifestyle => "Lifestyle",
130 AppCategory::Medical => "Medical",
131 AppCategory::Music => "Music",
132 AppCategory::News => "News",
133 AppCategory::Photography => "Photography",
134 AppCategory::Productivity => "Productivity",
135 AppCategory::Reference => "Reference",
136 AppCategory::SocialNetworking => "Social Networking",
137 AppCategory::Sports => "Sports",
138 AppCategory::Travel => "Travel",
139 AppCategory::Utility => "Utility",
140 AppCategory::Video => "Video",
141 AppCategory::Weather => "Weather",
142 }
143 }
144
145 pub fn freedesktop_categories(self) -> &'static str {
150 match &self {
151 AppCategory::Business => "Office;",
152 AppCategory::DeveloperTool => "Development;",
153 AppCategory::Education => "Education;",
154 AppCategory::Entertainment => "Network;",
155 AppCategory::Finance => "Office;Finance;",
156 AppCategory::Game => "Game;",
157 AppCategory::ActionGame => "Game;ActionGame;",
158 AppCategory::AdventureGame => "Game;AdventureGame;",
159 AppCategory::ArcadeGame => "Game;ArcadeGame;",
160 AppCategory::BoardGame => "Game;BoardGame;",
161 AppCategory::CardGame => "Game;CardGame;",
162 AppCategory::CasinoGame => "Game;",
163 AppCategory::DiceGame => "Game;",
164 AppCategory::EducationalGame => "Game;Education;",
165 AppCategory::FamilyGame => "Game;",
166 AppCategory::KidsGame => "Game;KidsGame;",
167 AppCategory::MusicGame => "Game;",
168 AppCategory::PuzzleGame => "Game;LogicGame;",
169 AppCategory::RacingGame => "Game;",
170 AppCategory::RolePlayingGame => "Game;RolePlaying;",
171 AppCategory::SimulationGame => "Game;Simulation;",
172 AppCategory::SportsGame => "Game;SportsGame;",
173 AppCategory::StrategyGame => "Game;StrategyGame;",
174 AppCategory::TriviaGame => "Game;",
175 AppCategory::WordGame => "Game;",
176 AppCategory::GraphicsAndDesign => "Graphics;",
177 AppCategory::HealthcareAndFitness => "Science;",
178 AppCategory::Lifestyle => "Education;",
179 AppCategory::Medical => "Science;MedicalSoftware;",
180 AppCategory::Music => "AudioVideo;Audio;Music;",
181 AppCategory::News => "Network;News;",
182 AppCategory::Photography => "Graphics;Photography;",
183 AppCategory::Productivity => "Office;",
184 AppCategory::Reference => "Education;",
185 AppCategory::SocialNetworking => "Network;",
186 AppCategory::Sports => "Education;Sports;",
187 AppCategory::Travel => "Education;",
188 AppCategory::Utility => "Utility;",
189 AppCategory::Video => "AudioVideo;Video;",
190 AppCategory::Weather => "Science;",
191 }
192 }
193
194 pub fn macos_application_category_type(self) -> &'static str {
197 match &self {
198 AppCategory::Business => "public.app-category.business",
199 AppCategory::DeveloperTool => "public.app-category.developer-tools",
200 AppCategory::Education => "public.app-category.education",
201 AppCategory::Entertainment => "public.app-category.entertainment",
202 AppCategory::Finance => "public.app-category.finance",
203 AppCategory::Game => "public.app-category.games",
204 AppCategory::ActionGame => "public.app-category.action-games",
205 AppCategory::AdventureGame => "public.app-category.adventure-games",
206 AppCategory::ArcadeGame => "public.app-category.arcade-games",
207 AppCategory::BoardGame => "public.app-category.board-games",
208 AppCategory::CardGame => "public.app-category.card-games",
209 AppCategory::CasinoGame => "public.app-category.casino-games",
210 AppCategory::DiceGame => "public.app-category.dice-games",
211 AppCategory::EducationalGame => "public.app-category.educational-games",
212 AppCategory::FamilyGame => "public.app-category.family-games",
213 AppCategory::KidsGame => "public.app-category.kids-games",
214 AppCategory::MusicGame => "public.app-category.music-games",
215 AppCategory::PuzzleGame => "public.app-category.puzzle-games",
216 AppCategory::RacingGame => "public.app-category.racing-games",
217 AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
218 AppCategory::SimulationGame => "public.app-category.simulation-games",
219 AppCategory::SportsGame => "public.app-category.sports-games",
220 AppCategory::StrategyGame => "public.app-category.strategy-games",
221 AppCategory::TriviaGame => "public.app-category.trivia-games",
222 AppCategory::WordGame => "public.app-category.word-games",
223 AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
224 AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
225 AppCategory::Lifestyle => "public.app-category.lifestyle",
226 AppCategory::Medical => "public.app-category.medical",
227 AppCategory::Music => "public.app-category.music",
228 AppCategory::News => "public.app-category.news",
229 AppCategory::Photography => "public.app-category.photography",
230 AppCategory::Productivity => "public.app-category.productivity",
231 AppCategory::Reference => "public.app-category.reference",
232 AppCategory::SocialNetworking => "public.app-category.social-networking",
233 AppCategory::Sports => "public.app-category.sports",
234 AppCategory::Travel => "public.app-category.travel",
235 AppCategory::Utility => "public.app-category.utilities",
236 AppCategory::Video => "public.app-category.video",
237 AppCategory::Weather => "public.app-category.weather",
238 }
239 }
240}
241
242impl<'d> serde::Deserialize<'d> for AppCategory {
243 fn deserialize<D: serde::Deserializer<'d>>(deserializer: D) -> Result<AppCategory, D::Error> {
244 deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None })
245 }
246}
247
248struct AppCategoryVisitor {
249 did_you_mean: Option<&'static str>,
250}
251
252impl serde::de::Visitor<'_> for AppCategoryVisitor {
253 type Value = AppCategory;
254
255 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self.did_you_mean {
257 Some(string) => write!(
258 formatter,
259 "a valid app category string (did you mean \"{string}\"?)"
260 ),
261 None => write!(formatter, "a valid app category string"),
262 }
263 }
264
265 fn visit_str<E: serde::de::Error>(mut self, value: &str) -> Result<AppCategory, E> {
266 match AppCategory::from_str(value) {
267 Ok(category) => Ok(category),
268 Err(did_you_mean) => {
269 self.did_you_mean = did_you_mean;
270 let unexp = serde::de::Unexpected::Str(value);
271 Err(serde::de::Error::invalid_value(unexp, &self))
272 }
273 }
274 }
275}
276
277const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
278 ("actiongame", AppCategory::ActionGame),
279 ("actiongames", AppCategory::ActionGame),
280 ("adventuregame", AppCategory::AdventureGame),
281 ("adventuregames", AppCategory::AdventureGame),
282 ("arcadegame", AppCategory::ArcadeGame),
283 ("arcadegames", AppCategory::ArcadeGame),
284 ("boardgame", AppCategory::BoardGame),
285 ("boardgames", AppCategory::BoardGame),
286 ("business", AppCategory::Business),
287 ("cardgame", AppCategory::CardGame),
288 ("cardgames", AppCategory::CardGame),
289 ("casinogame", AppCategory::CasinoGame),
290 ("casinogames", AppCategory::CasinoGame),
291 ("developer", AppCategory::DeveloperTool),
292 ("developertool", AppCategory::DeveloperTool),
293 ("developertools", AppCategory::DeveloperTool),
294 ("development", AppCategory::DeveloperTool),
295 ("dicegame", AppCategory::DiceGame),
296 ("dicegames", AppCategory::DiceGame),
297 ("education", AppCategory::Education),
298 ("educationalgame", AppCategory::EducationalGame),
299 ("educationalgames", AppCategory::EducationalGame),
300 ("entertainment", AppCategory::Entertainment),
301 ("familygame", AppCategory::FamilyGame),
302 ("familygames", AppCategory::FamilyGame),
303 ("finance", AppCategory::Finance),
304 ("fitness", AppCategory::HealthcareAndFitness),
305 ("game", AppCategory::Game),
306 ("games", AppCategory::Game),
307 ("graphicdesign", AppCategory::GraphicsAndDesign),
308 ("graphicsanddesign", AppCategory::GraphicsAndDesign),
309 ("graphicsdesign", AppCategory::GraphicsAndDesign),
310 ("healthcareandfitness", AppCategory::HealthcareAndFitness),
311 ("healthcarefitness", AppCategory::HealthcareAndFitness),
312 ("kidsgame", AppCategory::KidsGame),
313 ("kidsgames", AppCategory::KidsGame),
314 ("lifestyle", AppCategory::Lifestyle),
315 ("logicgame", AppCategory::PuzzleGame),
316 ("medical", AppCategory::Medical),
317 ("medicalsoftware", AppCategory::Medical),
318 ("music", AppCategory::Music),
319 ("musicgame", AppCategory::MusicGame),
320 ("musicgames", AppCategory::MusicGame),
321 ("news", AppCategory::News),
322 ("photography", AppCategory::Photography),
323 ("productivity", AppCategory::Productivity),
324 ("puzzlegame", AppCategory::PuzzleGame),
325 ("puzzlegames", AppCategory::PuzzleGame),
326 ("racinggame", AppCategory::RacingGame),
327 ("racinggames", AppCategory::RacingGame),
328 ("reference", AppCategory::Reference),
329 ("roleplaying", AppCategory::RolePlayingGame),
330 ("roleplayinggame", AppCategory::RolePlayingGame),
331 ("roleplayinggames", AppCategory::RolePlayingGame),
332 ("rpg", AppCategory::RolePlayingGame),
333 ("simulationgame", AppCategory::SimulationGame),
334 ("simulationgames", AppCategory::SimulationGame),
335 ("socialnetwork", AppCategory::SocialNetworking),
336 ("socialnetworking", AppCategory::SocialNetworking),
337 ("sports", AppCategory::Sports),
338 ("sportsgame", AppCategory::SportsGame),
339 ("sportsgames", AppCategory::SportsGame),
340 ("strategygame", AppCategory::StrategyGame),
341 ("strategygames", AppCategory::StrategyGame),
342 ("travel", AppCategory::Travel),
343 ("triviagame", AppCategory::TriviaGame),
344 ("triviagames", AppCategory::TriviaGame),
345 ("utilities", AppCategory::Utility),
346 ("utility", AppCategory::Utility),
347 ("video", AppCategory::Video),
348 ("weather", AppCategory::Weather),
349 ("wordgame", AppCategory::WordGame),
350 ("wordgames", AppCategory::WordGame),
351];
352
353#[cfg(test)]
354mod tests {
355 use super::AppCategory;
356 use std::str::FromStr;
357
358 #[test]
359 fn category_from_string_ok() {
360 assert_eq!(
362 AppCategory::from_str("Education"),
363 Ok(AppCategory::Education)
364 );
365 assert_eq!(
366 AppCategory::from_str("Developer Tool"),
367 Ok(AppCategory::DeveloperTool)
368 );
369 assert_eq!(
371 AppCategory::from_str(" puzzle game "),
372 Ok(AppCategory::PuzzleGame)
373 );
374 assert_eq!(
375 AppCategory::from_str("Role-playing game"),
376 Ok(AppCategory::RolePlayingGame)
377 );
378 assert_eq!(
380 AppCategory::from_str("public.app-category.developer-tools"),
381 Ok(AppCategory::DeveloperTool)
382 );
383 assert_eq!(
384 AppCategory::from_str("public.app-category.role-playing-games"),
385 Ok(AppCategory::RolePlayingGame)
386 );
387 assert_eq!(
389 AppCategory::from_str("Development"),
390 Ok(AppCategory::DeveloperTool)
391 );
392 assert_eq!(
393 AppCategory::from_str("LogicGame"),
394 Ok(AppCategory::PuzzleGame)
395 );
396 assert_eq!(
398 AppCategory::from_str("RPG"),
399 Ok(AppCategory::RolePlayingGame)
400 );
401 }
402
403 #[test]
404 fn category_from_string_did_you_mean() {
405 assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game")));
406 assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography")));
407 assert_eq!(
408 AppCategory::from_str("strategery"),
409 Err(Some("Strategy Game"))
410 );
411 }
412
413 #[test]
414 fn category_from_string_totally_wrong() {
415 assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None));
416 assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None));
417 }
418
419 #[test]
420 fn ls_application_category_type_round_trip() {
421 let values = &[
422 "public.app-category.business",
423 "public.app-category.developer-tools",
424 "public.app-category.education",
425 "public.app-category.entertainment",
426 "public.app-category.finance",
427 "public.app-category.games",
428 "public.app-category.action-games",
429 "public.app-category.adventure-games",
430 "public.app-category.arcade-games",
431 "public.app-category.board-games",
432 "public.app-category.card-games",
433 "public.app-category.casino-games",
434 "public.app-category.dice-games",
435 "public.app-category.educational-games",
436 "public.app-category.family-games",
437 "public.app-category.kids-games",
438 "public.app-category.music-games",
439 "public.app-category.puzzle-games",
440 "public.app-category.racing-games",
441 "public.app-category.role-playing-games",
442 "public.app-category.simulation-games",
443 "public.app-category.sports-games",
444 "public.app-category.strategy-games",
445 "public.app-category.trivia-games",
446 "public.app-category.word-games",
447 "public.app-category.graphics-design",
448 "public.app-category.healthcare-fitness",
449 "public.app-category.lifestyle",
450 "public.app-category.medical",
451 "public.app-category.music",
452 "public.app-category.news",
453 "public.app-category.photography",
454 "public.app-category.productivity",
455 "public.app-category.reference",
456 "public.app-category.social-networking",
457 "public.app-category.sports",
458 "public.app-category.travel",
459 "public.app-category.utilities",
460 "public.app-category.video",
461 "public.app-category.weather",
462 ];
463 for &value in values.iter() {
467 let category = AppCategory::from_str(value).expect(value);
468 assert_eq!(category.macos_application_category_type(), value);
469 }
470 }
471}