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 } => suffixes.iter().any(|s| s == ext.suffix.as_str()),
78 Self::Registrable => ext.registrable,
79 Self::Everything => true,
80 }
81 }
82
83 #[must_use]
84 pub const fn includes_restricted(&self) -> bool {
85 matches!(self, Self::Everything)
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct Group {
91 pub key: String,
92 pub family: Family,
93 pub title: String,
94 pub summary: String,
95 pub selector: Selector,
96 #[serde(default)]
97 pub order: u16,
98}
99
100impl Group {
101 #[must_use]
102 pub fn holds(&self, ext: &Extension) -> bool {
103 self.selector.matches(ext)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::tld::extension::{ExtensionKind, Suffix};
111
112 #[test]
113 fn the_families_are_offered_most_asked_for_first() {
114 assert_eq!(
115 Family::all().map(Family::title),
116 ["By popularity", "Hand-picked", "By industry", "By region"],
117 "the picker draws them in this order, so the order is part of the product"
118 );
119 }
120
121 #[test]
122 fn the_declared_order_matches_the_offered_order() {
123 let mut sorted = Family::all();
124 sorted.sort_unstable();
125 assert_eq!(
126 sorted,
127 Family::all(),
128 "a derived comparison disagreeing with the drawn order would sort a list the wrong way round"
129 );
130 }
131
132 fn ext(suffix: &str) -> Extension {
133 Extension {
134 suffix: Suffix::parse(suffix).unwrap(),
135 kind: ExtensionKind::Generic,
136 rank: Some(5),
137 industries: vec!["tech".to_owned()],
138 region: None,
139 country: None,
140 registrable: true,
141 repurposed: false,
142 }
143 }
144
145 #[test]
146 fn an_industry_selector_reads_the_industry_tags() {
147 let selector = Selector::Industry {
148 key: "tech".to_owned(),
149 };
150 assert!(selector.matches(&ext("dev")));
151 let selector = Selector::Industry {
152 key: "health".to_owned(),
153 };
154 assert!(!selector.matches(&ext("dev")));
155 }
156
157 #[test]
158 fn top_rank_keeps_only_ranks_at_or_under_the_limit() {
159 assert!(Selector::TopRank { max_rank: 10 }.matches(&ext("dev")));
160 assert!(!Selector::TopRank { max_rank: 3 }.matches(&ext("dev")));
161 }
162
163 #[test]
164 fn country_codes_match_on_the_delegated_label() {
165 let mut uk = ext("co.uk");
166 uk.kind = ExtensionKind::Country;
167 assert!(Selector::CountryCodes.matches(&uk));
168 assert!(!Selector::CountryCodes.matches(&ext("dev")));
169 }
170
171 #[test]
172 fn a_region_selector_keeps_only_the_extensions_of_that_region() {
173 let mut bangladesh = ext("bd");
174 bangladesh.region = Some("south-asia".to_owned());
175
176 assert!(
177 Selector::Region {
178 key: "south-asia".to_owned()
179 }
180 .matches(&bangladesh)
181 );
182 assert!(
183 !Selector::Region {
184 key: "europe".to_owned()
185 }
186 .matches(&bangladesh)
187 );
188 assert!(
189 !Selector::Region {
190 key: "south-asia".to_owned()
191 }
192 .matches(&ext("dev")),
193 "an extension with no region must not fall into one"
194 );
195 }
196
197 #[test]
198 fn a_repurposed_selector_keeps_only_the_zones_marked_repurposed() {
199 let mut repurposed = ext("io");
200 repurposed.repurposed = true;
201
202 assert!(Selector::Repurposed.matches(&repurposed));
203 assert!(!Selector::Repurposed.matches(&ext("dev")));
204 }
205
206 #[test]
207 fn an_explicit_selector_matches_the_whole_suffix_and_never_its_parent() {
208 let selector = Selector::Explicit {
209 suffixes: vec!["com.bd".to_owned()],
210 };
211
212 assert!(selector.matches(&ext("com.bd")));
213 assert!(!selector.matches(&ext("bd")));
214 assert!(
215 !Selector::Explicit {
216 suffixes: Vec::new()
217 }
218 .matches(&ext("com.bd"))
219 );
220 }
221
222 #[test]
223 fn a_registrable_selector_drops_a_zone_the_public_cannot_register_in() {
224 let mut closed = ext("gov.bd");
225 closed.registrable = false;
226
227 assert!(!Selector::Registrable.matches(&closed));
228 assert!(Selector::Registrable.matches(&ext("dev")));
229 }
230
231 #[test]
232 fn everything_takes_every_extension_including_a_closed_one() {
233 let mut closed = ext("gov.bd");
234 closed.registrable = false;
235
236 assert!(Selector::Everything.matches(&closed));
237 assert!(Selector::Everything.matches(&ext("dev")));
238 }
239
240 #[test]
241 fn top_rank_never_admits_an_extension_that_has_no_rank() {
242 let mut unranked = ext("dev");
243 unranked.rank = None;
244
245 assert!(!Selector::TopRank { max_rank: u32::MAX }.matches(&unranked));
246 }
247
248 #[test]
249 fn a_group_answers_with_its_own_selector_and_nothing_else() {
250 let group = Group {
251 key: "south-asia".to_owned(),
252 family: Family::Region,
253 title: "South Asia".to_owned(),
254 summary: "Extensions used across South Asia".to_owned(),
255 selector: Selector::Explicit {
256 suffixes: vec!["bd".to_owned()],
257 },
258 order: 0,
259 };
260
261 assert!(group.holds(&ext("bd")));
262 assert!(!group.holds(&ext("dev")));
263 }
264
265 #[test]
266 fn only_everything_reaches_restricted_zones() {
267 assert!(Selector::Everything.includes_restricted());
268 assert!(!Selector::Registrable.includes_restricted());
269 assert!(!Selector::CountryCodes.includes_restricted());
270 }
271}