reserve-core 0.1.0

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")]
pub enum Family {
    Industry,
    Region,
    Popularity,
    Curated,
}

impl Family {
    #[must_use]
    pub const fn key(self) -> &'static str {
        match self {
            Self::Industry => "industry",
            Self::Region => "region",
            Self::Popularity => "popularity",
            Self::Curated => "curated",
        }
    }

    #[must_use]
    pub const fn title(self) -> &'static str {
        match self {
            Self::Industry => "By industry",
            Self::Region => "By region",
            Self::Popularity => "By popularity",
            Self::Curated => "Hand-picked",
        }
    }

    #[must_use]
    pub const fn all() -> [Self; 4] {
        [
            Self::Industry,
            Self::Region,
            Self::Popularity,
            Self::Curated,
        ]
    }
}

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(|s| s == ext.suffix.as_str()),
            Self::Registrable => ext.registrable,
            Self::Everything => true,
        }
    }

    #[must_use]
    pub const fn includes_restricted(&self) -> bool {
        matches!(self, Self::Everything)
    }
}

#[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};

    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")));
    }

    #[test]
    fn only_everything_reaches_restricted_zones() {
        assert!(Selector::Everything.includes_restricted());
        assert!(!Selector::Registrable.includes_restricted());
        assert!(!Selector::CountryCodes.includes_restricted());
    }
}