ens-normalize-rs 0.2.0

Ethereum Name Service (ENS) name normalization
Documentation
use ens_normalize_rs::EnsNameNormalizer;
use lazy_static::lazy_static;
use rayon::prelude::*;
use rstest::rstest;
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Entry {
    VersionInfo {
        name: String,
        validated: String,
        built: String,
        cldr: String,
        derived: String,
        ens_hash_base64: String,
        nf_hash_base64: String,
        spec_hash: String,
        unicode: String,
        version: String,
    },
    TestCase(TestCase),
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct TestCase {
    name: String,
    comment: Option<String>,
    #[serde(default)]
    error: bool,
    norm: Option<String>,
}

pub type IndexedTestCase<'a> = (usize, &'a TestCase);

lazy_static! {
    pub static ref ENS_TESTS: Vec<Entry> =
        serde_json::from_str(include_str!("tests.json")).unwrap();
}

#[rstest]
fn ens_tests() {
    test_cases_parallel(&only_cases(&ENS_TESTS))
}

fn test_cases_parallel(cases: &[IndexedTestCase]) {
    let normalizer = EnsNameNormalizer::default();
    let results = cases
        .par_iter() // Parallel iterator from Rayon
        .map(|(i, test_case)| (i, process_test_case(&normalizer, test_case)))
        .filter_map(|(i, r)| r.err().map(|e| (i, e)))
        .collect::<Vec<_>>();

    if !results.is_empty() {
        let info = results
            .iter()
            .map(|(i, e)| format!("{}: {}", i, e))
            .collect::<Vec<_>>()
            .join("\n");
        panic!("{} cases failed:\n{}", results.len(), info);
    }
}

fn process_test_case(normalizer: &EnsNameNormalizer, case: &TestCase) -> Result<(), anyhow::Error> {
    let test_name = match (case.comment.as_ref(), case.name.as_str()) {
        (Some(comment), name) if name.len() < 64 => format!("{comment} (`{name}`)"),
        (Some(comment), _) => comment.clone(),
        (None, name) => name.to_string(),
    };
    let result = normalizer.process(&case.name);

    match result {
        Err(_e) if case.error => (),
        Ok(processed) if !case.error => {
            let actual = processed.normalize();
            if let Some(expected) = &case.norm {
                assert_eq!(
                    actual,
                    expected.to_string(),
                    "in test case '{test_name}': expected '{expected}', got '{actual}'"
                );
            } else {
                assert_eq!(
                    actual, case.name,
                    "in test case '{test_name}': expected '{}', got '{actual}'",
                    case.name
                );
            }
        }
        Err(e) => anyhow::bail!("in test case '{test_name}': expected no error, got {e}"),
        Ok(_) => anyhow::bail!("in test case '{test_name}': expected error, got success"),
    }

    Ok(())
}

fn only_cases(entries: &[Entry]) -> Vec<IndexedTestCase<'_>> {
    entries
        .iter()
        .filter_map(|e| match e {
            Entry::TestCase(t) => Some(t),
            _ => None,
        })
        .enumerate()
        .collect()
}