1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub enum Category {
20 Existence,
21 Content,
22 StructuredQuery,
23 Naming,
24 TextHygiene,
25 SecurityUnicodeSanity,
26 Encoding,
27 Structure,
28 PortableMetadata,
29 UnixMetadata,
30 GitHygiene,
31 CrossFile,
32 PluginTier1,
33}
34
35impl Category {
36 pub const ALL: [Category; 13] = [
38 Category::Existence,
39 Category::Content,
40 Category::StructuredQuery,
41 Category::Naming,
42 Category::TextHygiene,
43 Category::SecurityUnicodeSanity,
44 Category::Encoding,
45 Category::Structure,
46 Category::PortableMetadata,
47 Category::UnixMetadata,
48 Category::GitHygiene,
49 Category::CrossFile,
50 Category::PluginTier1,
51 ];
52
53 pub fn all() -> &'static [Category] {
55 &Self::ALL
56 }
57
58 pub fn title(self) -> &'static str {
60 match self {
61 Category::Existence => "Existence",
62 Category::Content => "Content",
63 Category::StructuredQuery => "Structured query",
64 Category::Naming => "Naming",
65 Category::TextHygiene => "Text hygiene",
66 Category::SecurityUnicodeSanity => "Security / Unicode sanity",
67 Category::Encoding => "Encoding",
68 Category::Structure => "Structure",
69 Category::PortableMetadata => "Portable metadata",
70 Category::UnixMetadata => "Unix metadata",
71 Category::GitHygiene => "Git hygiene",
72 Category::CrossFile => "Cross-file",
73 Category::PluginTier1 => "Plugin (tier 1)",
74 }
75 }
76
77 pub fn slug(self) -> &'static str {
79 match self {
80 Category::Existence => "existence",
81 Category::Content => "content",
82 Category::StructuredQuery => "structured-query",
83 Category::Naming => "naming",
84 Category::TextHygiene => "text-hygiene",
85 Category::SecurityUnicodeSanity => "security-unicode-sanity",
86 Category::Encoding => "encoding",
87 Category::Structure => "structure",
88 Category::PortableMetadata => "portable-metadata",
89 Category::UnixMetadata => "unix-metadata",
90 Category::GitHygiene => "git-hygiene",
91 Category::CrossFile => "cross-file",
92 Category::PluginTier1 => "plugin-tier-1",
93 }
94 }
95
96 pub fn order(self) -> usize {
103 match self {
104 Category::Existence => 0,
105 Category::Content => 1,
106 Category::StructuredQuery => 2,
107 Category::Naming => 3,
108 Category::TextHygiene => 4,
109 Category::SecurityUnicodeSanity => 5,
110 Category::Encoding => 6,
111 Category::Structure => 7,
112 Category::PortableMetadata => 8,
113 Category::UnixMetadata => 9,
114 Category::GitHygiene => 10,
115 Category::CrossFile => 11,
116 Category::PluginTier1 => 12,
117 }
118 }
119
120 pub fn from_title(title: &str) -> Option<Category> {
124 let t = title.trim();
125 Self::ALL.iter().copied().find(|c| c.title() == t)
126 }
127
128 pub fn from_slug(slug: &str) -> Option<Category> {
130 Self::ALL.iter().copied().find(|c| c.slug() == slug)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn all_has_thirteen_in_declaration_order() {
140 assert_eq!(Category::ALL.len(), 13);
141 for (i, c) in Category::ALL.iter().enumerate() {
142 assert_eq!(c.order(), i, "order() must equal ALL index for {c:?}");
143 }
144 }
145
146 #[test]
147 fn ord_matches_display_order() {
148 let mut sorted = Category::ALL;
149 sorted.sort();
150 assert_eq!(
151 sorted,
152 Category::ALL,
153 "derived Ord must equal display order"
154 );
155 assert!(Category::Existence < Category::PluginTier1);
156 }
157
158 #[test]
159 fn titles_and_slugs_are_unique() {
160 let mut titles: Vec<&str> = Category::ALL.iter().map(|c| c.title()).collect();
161 titles.sort_unstable();
162 titles.dedup();
163 assert_eq!(titles.len(), 13, "titles must be unique");
164
165 let mut slugs: Vec<&str> = Category::ALL.iter().map(|c| c.slug()).collect();
166 slugs.sort_unstable();
167 slugs.dedup();
168 assert_eq!(slugs.len(), 13, "slugs must be unique");
169 }
170
171 #[test]
172 fn from_title_round_trips_and_trims() {
173 for c in Category::ALL {
174 assert_eq!(Category::from_title(c.title()), Some(c));
175 assert_eq!(Category::from_title(&format!(" {} ", c.title())), Some(c));
176 }
177 assert_eq!(Category::from_title("security / unicode sanity"), None); assert_eq!(Category::from_title("Nonsense"), None);
179 }
180
181 #[test]
182 fn from_slug_round_trips() {
183 for c in Category::ALL {
184 assert_eq!(Category::from_slug(c.slug()), Some(c));
185 }
186 assert_eq!(Category::from_slug("nope"), None);
187 }
188}