reserve-core 0.5.1

Core lookup, catalog, and rate-limiting engine behind the reserve domain finder
Documentation
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")]
/// @docgen Declaration order is the order the picker draws them in, so the most-asked-for list comes first.
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")));
    }
}