1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::tld::extension::Extension;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum Family {
11 Popularity,
12 Curated,
13 Industry,
14 Region,
15}
16
17impl Family {
18 #[must_use]
19 pub const fn key(self) -> &'static str {
20 match self {
21 Self::Popularity => "popularity",
22 Self::Curated => "curated",
23 Self::Industry => "industry",
24 Self::Region => "region",
25 }
26 }
27
28 #[must_use]
29 pub const fn title(self) -> &'static str {
30 match self {
31 Self::Popularity => "By popularity",
32 Self::Curated => "Hand-picked",
33 Self::Industry => "By industry",
34 Self::Region => "By region",
35 }
36 }
37
38 #[must_use]
39 pub const fn all() -> [Self; 4] {
40 [
41 Self::Popularity,
42 Self::Curated,
43 Self::Industry,
44 Self::Region,
45 ]
46 }
47}
48
49impl fmt::Display for Family {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(self.key())
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(tag = "rule", rename_all = "kebab-case")]
57pub enum Selector {
58 Industry { key: String },
59 Region { key: String },
60 TopRank { max_rank: u32 },
61 CountryCodes,
62 Repurposed,
63 Explicit { suffixes: Vec<String> },
64 Registrable,
65 Everything,
66}
67
68impl Selector {
69 #[must_use]
70 pub fn matches(&self, ext: &Extension) -> bool {
71 match self {
72 Self::Industry { key } => ext.is_in_industry(key),
73 Self::Region { key } => ext.region.as_deref() == Some(key.as_str()),
74 Self::TopRank { max_rank } => ext.rank.is_some_and(|rank| rank <= *max_rank),
75 Self::CountryCodes => ext.suffix.is_country_code(),
76 Self::Repurposed => ext.repurposed,
77 Self::Explicit { suffixes } => {
78 suffixes.iter().any(|listed| listed == ext.suffix.as_str())
79 }
80 Self::Registrable => ext.registrable,
81 Self::Everything => true,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct Group {
88 pub key: String,
89 pub family: Family,
90 pub title: String,
91 pub summary: String,
92 pub selector: Selector,
93 #[serde(default)]
94 pub order: u16,
95}
96
97impl Group {
98 #[must_use]
99 pub fn holds(&self, ext: &Extension) -> bool {
100 self.selector.matches(ext)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::tld::extension::{ExtensionKind, Suffix};
108
109 #[test]
110 fn the_families_are_offered_most_asked_for_first() {
111 assert_eq!(
112 Family::all().map(Family::title),
113 ["By popularity", "Hand-picked", "By industry", "By region"],
114 "the picker draws them in this order, so the order is part of the product"
115 );
116 }
117
118 #[test]
119 fn the_declared_order_matches_the_offered_order() {
120 let mut sorted = Family::all();
121 sorted.sort_unstable();
122 assert_eq!(
123 sorted,
124 Family::all(),
125 "a derived comparison disagreeing with the drawn order would sort a list the wrong way round"
126 );
127 }
128
129 fn ext(suffix: &str) -> Extension {
130 Extension {
131 suffix: Suffix::parse(suffix).unwrap(),
132 kind: ExtensionKind::Generic,
133 rank: Some(5),
134 industries: vec!["tech".to_owned()],
135 region: None,
136 country: None,
137 registrable: true,
138 repurposed: false,
139 }
140 }
141
142 #[test]
143 fn an_industry_selector_reads_the_industry_tags() {
144 let selector = Selector::Industry {
145 key: "tech".to_owned(),
146 };
147 assert!(selector.matches(&ext("dev")));
148 let selector = Selector::Industry {
149 key: "health".to_owned(),
150 };
151 assert!(!selector.matches(&ext("dev")));
152 }
153
154 #[test]
155 fn top_rank_keeps_only_ranks_at_or_under_the_limit() {
156 assert!(Selector::TopRank { max_rank: 10 }.matches(&ext("dev")));
157 assert!(!Selector::TopRank { max_rank: 3 }.matches(&ext("dev")));
158 }
159
160 #[test]
161 fn country_codes_match_on_the_delegated_label() {
162 let mut uk = ext("co.uk");
163 uk.kind = ExtensionKind::Country;
164 assert!(Selector::CountryCodes.matches(&uk));
165 assert!(!Selector::CountryCodes.matches(&ext("dev")));
166 }
167
168 #[test]
169 fn a_region_selector_keeps_only_the_extensions_of_that_region() {
170 let mut bangladesh = ext("bd");
171 bangladesh.region = Some("south-asia".to_owned());
172
173 assert!(
174 Selector::Region {
175 key: "south-asia".to_owned()
176 }
177 .matches(&bangladesh)
178 );
179 assert!(
180 !Selector::Region {
181 key: "europe".to_owned()
182 }
183 .matches(&bangladesh)
184 );
185 assert!(
186 !Selector::Region {
187 key: "south-asia".to_owned()
188 }
189 .matches(&ext("dev")),
190 "an extension with no region must not fall into one"
191 );
192 }
193
194 #[test]
195 fn a_repurposed_selector_keeps_only_the_zones_marked_repurposed() {
196 let mut repurposed = ext("io");
197 repurposed.repurposed = true;
198
199 assert!(Selector::Repurposed.matches(&repurposed));
200 assert!(!Selector::Repurposed.matches(&ext("dev")));
201 }
202
203 #[test]
204 fn an_explicit_selector_matches_the_whole_suffix_and_never_its_parent() {
205 let selector = Selector::Explicit {
206 suffixes: vec!["com.bd".to_owned()],
207 };
208
209 assert!(selector.matches(&ext("com.bd")));
210 assert!(!selector.matches(&ext("bd")));
211 assert!(
212 !Selector::Explicit {
213 suffixes: Vec::new()
214 }
215 .matches(&ext("com.bd"))
216 );
217 }
218
219 #[test]
220 fn a_registrable_selector_drops_a_zone_the_public_cannot_register_in() {
221 let mut closed = ext("gov.bd");
222 closed.registrable = false;
223
224 assert!(!Selector::Registrable.matches(&closed));
225 assert!(Selector::Registrable.matches(&ext("dev")));
226 }
227
228 #[test]
229 fn everything_takes_every_extension_including_a_closed_one() {
230 let mut closed = ext("gov.bd");
231 closed.registrable = false;
232
233 assert!(Selector::Everything.matches(&closed));
234 assert!(Selector::Everything.matches(&ext("dev")));
235 }
236
237 #[test]
238 fn top_rank_never_admits_an_extension_that_has_no_rank() {
239 let mut unranked = ext("dev");
240 unranked.rank = None;
241
242 assert!(!Selector::TopRank { max_rank: u32::MAX }.matches(&unranked));
243 }
244
245 #[test]
246 fn a_group_answers_with_its_own_selector_and_nothing_else() {
247 let group = Group {
248 key: "south-asia".to_owned(),
249 family: Family::Region,
250 title: "South Asia".to_owned(),
251 summary: "Extensions used across South Asia".to_owned(),
252 selector: Selector::Explicit {
253 suffixes: vec!["bd".to_owned()],
254 },
255 order: 0,
256 };
257
258 assert!(group.holds(&ext("bd")));
259 assert!(!group.holds(&ext("dev")));
260 }
261}