Skip to main content

reserve_core/tld/
group.rs

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")]
9/// @docgen Declaration order is the order the picker draws them in, so the most-asked-for list comes first.
10pub 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}