use std::fmt;
use serde::{Deserialize, Serialize};
use crate::tld::extension::Extension;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Family {
Popularity,
Curated,
Industry,
Region,
}
impl Family {
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Popularity => "popularity",
Self::Curated => "curated",
Self::Industry => "industry",
Self::Region => "region",
}
}
#[must_use]
pub const fn title(self) -> &'static str {
match self {
Self::Popularity => "By popularity",
Self::Curated => "Hand-picked",
Self::Industry => "By industry",
Self::Region => "By region",
}
}
#[must_use]
pub const fn all() -> [Self; 4] {
[
Self::Popularity,
Self::Curated,
Self::Industry,
Self::Region,
]
}
}
impl fmt::Display for Family {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.key())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "rule", rename_all = "kebab-case")]
pub enum Selector {
Industry { key: String },
Region { key: String },
TopRank { max_rank: u32 },
CountryCodes,
Repurposed,
Explicit { suffixes: Vec<String> },
Registrable,
Everything,
}
impl Selector {
#[must_use]
pub fn matches(&self, ext: &Extension) -> bool {
match self {
Self::Industry { key } => ext.is_in_industry(key),
Self::Region { key } => ext.region.as_deref() == Some(key.as_str()),
Self::TopRank { max_rank } => ext.rank.is_some_and(|rank| rank <= *max_rank),
Self::CountryCodes => ext.suffix.is_country_code(),
Self::Repurposed => ext.repurposed,
Self::Explicit { suffixes } => {
suffixes.iter().any(|listed| listed == ext.suffix.as_str())
}
Self::Registrable => ext.registrable,
Self::Everything => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Group {
pub key: String,
pub family: Family,
pub title: String,
pub summary: String,
pub selector: Selector,
#[serde(default)]
pub order: u16,
}
impl Group {
#[must_use]
pub fn holds(&self, ext: &Extension) -> bool {
self.selector.matches(ext)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tld::extension::{ExtensionKind, Suffix};
#[test]
fn the_families_are_offered_most_asked_for_first() {
assert_eq!(
Family::all().map(Family::title),
["By popularity", "Hand-picked", "By industry", "By region"],
"the picker draws them in this order, so the order is part of the product"
);
}
#[test]
fn the_declared_order_matches_the_offered_order() {
let mut sorted = Family::all();
sorted.sort_unstable();
assert_eq!(
sorted,
Family::all(),
"a derived comparison disagreeing with the drawn order would sort a list the wrong way round"
);
}
fn ext(suffix: &str) -> Extension {
Extension {
suffix: Suffix::parse(suffix).unwrap(),
kind: ExtensionKind::Generic,
rank: Some(5),
industries: vec!["tech".to_owned()],
region: None,
country: None,
registrable: true,
repurposed: false,
}
}
#[test]
fn an_industry_selector_reads_the_industry_tags() {
let selector = Selector::Industry {
key: "tech".to_owned(),
};
assert!(selector.matches(&ext("dev")));
let selector = Selector::Industry {
key: "health".to_owned(),
};
assert!(!selector.matches(&ext("dev")));
}
#[test]
fn top_rank_keeps_only_ranks_at_or_under_the_limit() {
assert!(Selector::TopRank { max_rank: 10 }.matches(&ext("dev")));
assert!(!Selector::TopRank { max_rank: 3 }.matches(&ext("dev")));
}
#[test]
fn country_codes_match_on_the_delegated_label() {
let mut uk = ext("co.uk");
uk.kind = ExtensionKind::Country;
assert!(Selector::CountryCodes.matches(&uk));
assert!(!Selector::CountryCodes.matches(&ext("dev")));
}
#[test]
fn a_region_selector_keeps_only_the_extensions_of_that_region() {
let mut bangladesh = ext("bd");
bangladesh.region = Some("south-asia".to_owned());
assert!(
Selector::Region {
key: "south-asia".to_owned()
}
.matches(&bangladesh)
);
assert!(
!Selector::Region {
key: "europe".to_owned()
}
.matches(&bangladesh)
);
assert!(
!Selector::Region {
key: "south-asia".to_owned()
}
.matches(&ext("dev")),
"an extension with no region must not fall into one"
);
}
#[test]
fn a_repurposed_selector_keeps_only_the_zones_marked_repurposed() {
let mut repurposed = ext("io");
repurposed.repurposed = true;
assert!(Selector::Repurposed.matches(&repurposed));
assert!(!Selector::Repurposed.matches(&ext("dev")));
}
#[test]
fn an_explicit_selector_matches_the_whole_suffix_and_never_its_parent() {
let selector = Selector::Explicit {
suffixes: vec!["com.bd".to_owned()],
};
assert!(selector.matches(&ext("com.bd")));
assert!(!selector.matches(&ext("bd")));
assert!(
!Selector::Explicit {
suffixes: Vec::new()
}
.matches(&ext("com.bd"))
);
}
#[test]
fn a_registrable_selector_drops_a_zone_the_public_cannot_register_in() {
let mut closed = ext("gov.bd");
closed.registrable = false;
assert!(!Selector::Registrable.matches(&closed));
assert!(Selector::Registrable.matches(&ext("dev")));
}
#[test]
fn everything_takes_every_extension_including_a_closed_one() {
let mut closed = ext("gov.bd");
closed.registrable = false;
assert!(Selector::Everything.matches(&closed));
assert!(Selector::Everything.matches(&ext("dev")));
}
#[test]
fn top_rank_never_admits_an_extension_that_has_no_rank() {
let mut unranked = ext("dev");
unranked.rank = None;
assert!(!Selector::TopRank { max_rank: u32::MAX }.matches(&unranked));
}
#[test]
fn a_group_answers_with_its_own_selector_and_nothing_else() {
let group = Group {
key: "south-asia".to_owned(),
family: Family::Region,
title: "South Asia".to_owned(),
summary: "Extensions used across South Asia".to_owned(),
selector: Selector::Explicit {
suffixes: vec!["bd".to_owned()],
},
order: 0,
};
assert!(group.holds(&ext("bd")));
assert!(!group.holds(&ext("dev")));
}
}